From 56a48185c2dc902bc15f9dd6433cc624c464ea4f Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sat, 9 Aug 2025 15:34:03 +0200 Subject: [PATCH 001/394] Create rebuild branch --- README.md | 241 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..c45dd33 --- /dev/null +++ b/README.md @@ -0,0 +1,241 @@ +# Noctalia + +**_quiet by design_** + +

+ + Last commit + + + GitHub stars + + + GitHub contributors + + + Discord + + + + +

+ +A sleek, minimal, and thoughtfully crafted setup for Wayland using **Quickshell**. This setup includes a status bar, notification system, control panel, wifi & bluetooth support, power profiles, lockscreen, tray, workspaces, and more — all styled with a warm lavender palette. + +## Preview + +
+Click to expand preview images + +![Main](https://i.imgur.com/5mOIGD2.jpeg) +
+ +![Control Panel](https://i.imgur.com/fJmCV6m.jpeg) +
+ +![Applauncher](https://i.imgur.com/9OPV30q.jpeg) + +
+
+ +--- + +> ⚠️ **Note:** +> This setup currently only supports **Niri** and **Hyprland** (for the most part), mostly due to the workspace integration. For anything else you will have to add your own workspace logic. + +--- + +## Features + +- **Status Bar:** Modular and informative with smooth animations. +- **Notifications:** Non-intrusive alerts styled to blend naturally. +- **Control Panel:** Centralized system controls for quick adjustments. +- **Connectivity:** Easy management of WiFi and Bluetooth devices. +- **Power Profiles:** Quick toggles for CPU performance. +- **Lockscreen:** Secure and visually consistent lock experience. +- **Tray & Workspaces:** Efficient workspace switching and tray icons. +- **Applauncher:** Stylized Applauncher to fit into the setup. + +--- + +
+Theme Colors + +| Color Role | Color | Description | +| -------------------- | ----------- | -------------------------- | +| Background Primary | `#0C0D11` | Deep indigo-black | +| Background Secondary | `#151720` | Slightly lifted dark | +| Background Tertiary | `#1D202B` | Soft contrast surface | +| Surface | `#1A1C26` | Material-like base layer | +| Surface Variant | `#2A2D3A` | Lightly elevated | +| Text Primary | `#CACEE2` | Gentle off-white | +| Text Secondary | `#B7BBD0` | Muted lavender-blue | +| Text Disabled | `#6B718A` | Dimmed blue-gray | +| Accent Primary | `#A8AEFF` | Light enchanted lavender | +| Accent Secondary | `#9EA0FF` | Softer lavender hue | +| Accent Tertiary | `#8EABFF` | Warm golden glow | +| Error | `#FF6B81` | Soft rose red | +| Warning | `#FFBB66` | Candlelight amber-orange | +| Highlight | `#E3C2FF` | Bright magical lavender | +| Ripple Effect | `#F3DEFF` | Gentle soft splash | +| On Accent | `#1A1A1A` | Text on accent background | +| Outline | `#44485A` | Subtle bluish-gray line | +| Shadow | `#000000B3` | Standard soft black shadow | +| Overlay | `#11121ACC` | Deep bluish overlay | + +
+ +--- + +## Installation & Usage + +
+Installation + +Install quickshell: + +``` +yay -S quickshell-git +``` + +or use any other way of installing quickshell-git (flake, paru etc). + +_Download and install the latest release:_ + +``` +mkdir -p ~/.config/quickshell && curl -sL https://github.com/Ly-sec/Noctalia/releases/latest/download/noctalia-latest.tar.gz | tar -xz --strip-components=1 -C ~/.config/quickshell/ +``` + +Or download manually from [releases](https://github.com/Ly-sec/Noctalia/releases) and extract: + +``` +mkdir -p ~/.config/quickshell && tar -xzf noctalia-*.tar.gz --strip-components=1 -C ~/.config/quickshell/ +``` + +### _niri only_ + +Add this to your `layout` section: + +`background-color "transparent"` + +That is to make swww work properly. + +
+
+ +
+Usage + +### Start quickshell: + +``` +qs +``` + +(If you want to autostart it, just add it to your niri configuration.) + +It is recommended to set the following in your Niri configuration (hyprland equivalent): + +``` +window-rule { + geometry-corner-radius 20 + clip-to-geometry true +} +``` + +### Settings: + +To make the weather widget, wallpaper manager and record button work you will have to open up the settings menu in to right panel (top right button to open panel) and edit said things accordingly. + +### Launcher: + +The launcher supports special commands for math calculation and clipboard history. +Once the launcher open you can invoke those special command by typing ">" +* \>calc : lets you do simple math +* \>clip : shows clipboard history + +
+ +
+
+Keybinds + +### Toggle Applauncher: + +``` + qs ipc call globalIPC toggleLauncher +``` + +### Toggle Lockscreen: + +``` + qs ipc call globalIPC toggleLock +``` + +### Toggle Notification Popup: + +``` +qs ipc call globalIPC toggleNotificationPopup +``` + +### Toggle Idle Inhibitor: + +``` +qs ipc call globalIPC toggleIdleInhibitor +``` +
+ +--- + +## Dependencies + +You will need to install a few things to get everything working: + +- `cava` so the audio visualizer works +- `gpu-screen-recorder` so that the record button works +- `xdg-desktop-portal-gnome` or any other xdg-desktop-portal (for `gpu-screen-recorder`) +- `material-symbols-git` so the icons properly show up +- `swww` to add fancy wallpaper animations (optional) +- `wallust` to theme the setup based on wallpaper (optional) + +## zigstat and zigbrightness + +The zigstat and zigbrightness utilities are automatically built from source during release creation. Source code can be found [here](https://git.pika-os.com/wm-packages/pikabar/src/branch/main/src). + +## Known issues + +It is perfect now + +--- + +## 💜 Credits + +Huge thanks to [**@ferrreo**](https://github.com/ferrreo) and [**@quadbyte**](https://github.com/quadbyte) for all the changes they did and all the cool features they added! + +--- + +## Contributing + +Contributions are welcome! Feel free to open issues or submit pull requests. + +--- + +#### Donation + +--- +While I actually didn't want to accept donations, more and more people are asking to donate so... I don't know, if you really feel like donating then I obviously highly appreciate it but **PLEASE** never feel forced to donate or anything. It won't change how I work on Noctalia, it's a project that I work on for fun in the end. + +[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R01IX85B) + +--- + +#### Special Thanks + +Thank you to everyone who supports me and this project 💜! +* Gohma + +--- + +## License + +This project is licensed under the terms of the [MIT License](./LICENSE). From f4051c9c515d01b7e4f40f9de263c8d3d3876285 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sat, 9 Aug 2025 16:17:07 +0200 Subject: [PATCH 002/394] First little steps --- Theme/Style.qml | 45 +++++++++++++++++++++++++++++++++++++++++++++ shell.qml | 9 +++++++++ 2 files changed, 54 insertions(+) create mode 100644 Theme/Style.qml create mode 100644 shell.qml diff --git a/Theme/Style.qml b/Theme/Style.qml new file mode 100644 index 0000000..3d8c8f0 --- /dev/null +++ b/Theme/Style.qml @@ -0,0 +1,45 @@ +pragma Singleton +import QtQuick +import Quickshell +import Quickshell.Io + +Singleton { + id: root + + /* + Preset sizes for font, radii, ? + */ + + // Font + property int fontExtraLarge: 32 + property int fontLarge: 16 + property int fontMedium: 14 + property int fontSmall: 12 + + // Font weight + property int fontWeightRegular: 400 + property int fontWeightMedium: 500 + property int fontWeightBold: 700 + + // Radii + property int radiusLarge: 20 + property int radiusMedium: 16 + property int radiusSmall: 12 + + // Border + property int borderThin: 1 + property int borderMedium: 2 + property int borderThick: 3 + + // Spacing + property int spacingExtraLarge: 20 + property int spacingLarge: 16 + property int spacingMedium: 12 + property int spacingSmall: 8 + + // Animation duration (ms) + property int animationFast: 150 + property int animationNormal: 300 + property int animationSlow: 500 + } + diff --git a/shell.qml b/shell.qml new file mode 100644 index 0000000..da003ef --- /dev/null +++ b/shell.qml @@ -0,0 +1,9 @@ +import Quickshell + +ShellRoot { + /* + Here we go, this is it. Rebuild time... + No spaghetti code, preset sizing, proper project structure + only "spawn" UI, do not do anything else here. + */ +} \ No newline at end of file From 48a32b01fdcbf69637f8a49c8e159d823dec4064 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sat, 9 Aug 2025 16:22:57 +0200 Subject: [PATCH 003/394] Change font size int to real --- Theme/Style.qml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Theme/Style.qml b/Theme/Style.qml index 3d8c8f0..8bb63c1 100644 --- a/Theme/Style.qml +++ b/Theme/Style.qml @@ -11,10 +11,10 @@ Singleton { */ // Font - property int fontExtraLarge: 32 - property int fontLarge: 16 - property int fontMedium: 14 - property int fontSmall: 12 + property real fontExtraLarge: 32 + property real fontLarge: 16 + property real fontMedium: 14 + property real fontSmall: 12 // Font weight property int fontWeightRegular: 400 From b65a8474c82760089ca959579cc061055f1192ce Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sat, 9 Aug 2025 11:58:17 -0400 Subject: [PATCH 004/394] Bar: an empty bar on all monitors --- Modules/Bar/Bar.qml | 28 ++++++++++++++++++++++++++++ shell.qml | 28 ++++++++++++++++++++++------ 2 files changed, 50 insertions(+), 6 deletions(-) create mode 100644 Modules/Bar/Bar.qml diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml new file mode 100644 index 0000000..83cd3f1 --- /dev/null +++ b/Modules/Bar/Bar.qml @@ -0,0 +1,28 @@ +import QtQuick +import Quickshell + +PanelWindow { + id: root + + property var modelData + + screen: modelData + implicitHeight: 36 + color: "transparent" + + anchors { + top: true + left: true + right: true + } + + Item { + anchors.fill: parent + + Rectangle { + anchors.fill: parent + color: "purple" + layer.enabled: true + } + } +} diff --git a/shell.qml b/shell.qml index da003ef..bf70526 100644 --- a/shell.qml +++ b/shell.qml @@ -1,9 +1,25 @@ -import Quickshell - -ShellRoot { - /* +/* Here we go, this is it. Rebuild time... No spaghetti code, preset sizing, proper project structure only "spawn" UI, do not do anything else here. - */ -} \ No newline at end of file +*/ + +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Widgets +import qs.Modules.Bar + +ShellRoot { + id: root + + Variants { + model: Quickshell.screens + + delegate: Bar { + modelData: item + } + + } + +} From b08944988d74f1e44c2d8c0688a25f5e9f78630f Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sat, 9 Aug 2025 12:07:58 -0400 Subject: [PATCH 005/394] Project structure and default colors --- Assets/Colors/default.json | 28 ++++++++++++++++++++++++++++ Modules/Settings/.gitkeep | 0 Modules/SidePanel/.gitkeep | 0 Services/.gitkeep | 0 4 files changed, 28 insertions(+) create mode 100644 Assets/Colors/default.json create mode 100644 Modules/Settings/.gitkeep create mode 100644 Modules/SidePanel/.gitkeep create mode 100644 Services/.gitkeep diff --git a/Assets/Colors/default.json b/Assets/Colors/default.json new file mode 100644 index 0000000..a89ca83 --- /dev/null +++ b/Assets/Colors/default.json @@ -0,0 +1,28 @@ +{ + "backgroundPrimary": "#191724", + "backgroundSecondary": "#1f1d2e", + "backgroundTertiary": "#26233a", + + "surface": "#1f1d2e", + "surfaceVariant": "#37354c", + + "textPrimary": "#e0def4", + "textSecondary": "#908caa", + "textDisabled": "#6e6a86", + + "accentPrimary": "#ebbcba", + "accentSecondary": "#31748f", + "accentTertiary": "#9ccfd8", + + "error": "#eb6f92", + "warning": "#f6c177", + + "highlight": "#c4a7e7", + "rippleEffect": "#31748f", + + "onAccent": "#191724", + "outline": "#44415a", + + "shadow": "#191724", + "overlay": "#191724" +} diff --git a/Modules/Settings/.gitkeep b/Modules/Settings/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Modules/SidePanel/.gitkeep b/Modules/SidePanel/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Services/.gitkeep b/Services/.gitkeep new file mode 100644 index 0000000..e69de29 From 821b4e8cad8d433a826575e5749a2dc766f9d607 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sat, 9 Aug 2025 12:53:10 -0400 Subject: [PATCH 006/394] Very basic theming support --- Modules/Bar/Bar.qml | 41 ++++++----- Services/Settings.qml | 27 +++++++ Theme/Style.qml | 63 ++++++++-------- Theme/Theme.qml | 143 +++++++++++++++++++++++++++++++++++++ Widgets/NoctaliaToggle.qml | 76 ++++++++++++++++++++ shell.qml | 17 +++-- 6 files changed, 309 insertions(+), 58 deletions(-) create mode 100644 Services/Settings.qml create mode 100644 Theme/Theme.qml create mode 100644 Widgets/NoctaliaToggle.qml diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 83cd3f1..47fb6c1 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -1,28 +1,33 @@ import QtQuick import Quickshell +import qs.Widgets +import qs.Theme PanelWindow { - id: root + id: root - property var modelData + property var modelData - screen: modelData - implicitHeight: 36 - color: "transparent" + screen: modelData + implicitHeight: 36 + color: "transparent" - anchors { - top: true - left: true - right: true + anchors { + top: true + left: true + right: true + } + + Item { + anchors.fill: parent + + Rectangle { + anchors.fill: parent + color: Theme.backgroundPrimary + layer.enabled: true } - Item { - anchors.fill: parent - - Rectangle { - anchors.fill: parent - color: "purple" - layer.enabled: true - } - } + // Just testing + NoctaliaToggle {} + } } diff --git a/Services/Settings.qml b/Services/Settings.qml new file mode 100644 index 0000000..fd6bfb9 --- /dev/null +++ b/Services/Settings.qml @@ -0,0 +1,27 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Services + +Singleton { + id: root + + property string shellName: "Noctalia" + property string settingsDir: Quickshell.env("NOCTALIA_SETTINGS_DIR") + || (Quickshell.env("XDG_CONFIG_HOME") + || Quickshell.env( + "HOME") + "/.config") + "/" + shellName + "/" + property string settingsFile: Quickshell.env("NOCTALIA_SETTINGS_FILE") + || (settingsDir + "Settings.json") + property string themeFile: Quickshell.env("NOCTALIA_THEME_FILE") + || (settingsDir + "Theme.json") + + Item { + Component.onCompleted: { + // ensure settings dir + Quickshell.execDetached(["mkdir", "-p", settingsDir]) + } + } +} diff --git a/Theme/Style.qml b/Theme/Style.qml index 8bb63c1..1479346 100644 --- a/Theme/Style.qml +++ b/Theme/Style.qml @@ -1,45 +1,46 @@ pragma Singleton + import QtQuick import Quickshell import Quickshell.Io Singleton { - id: root + id: root - /* - Preset sizes for font, radii, ? + + /* + Preset sizes for font, radii, ? */ - // Font - property real fontExtraLarge: 32 - property real fontLarge: 16 - property real fontMedium: 14 - property real fontSmall: 12 + // Font + property real fontExtraLarge: 32 + property real fontLarge: 16 + property real fontMedium: 14 + property real fontSmall: 12 - // Font weight - property int fontWeightRegular: 400 - property int fontWeightMedium: 500 - property int fontWeightBold: 700 + // Font weight + property int fontWeightRegular: 400 + property int fontWeightMedium: 500 + property int fontWeightBold: 700 - // Radii - property int radiusLarge: 20 - property int radiusMedium: 16 - property int radiusSmall: 12 + // Radii + property int radiusLarge: 20 + property int radiusMedium: 16 + property int radiusSmall: 12 - // Border - property int borderThin: 1 - property int borderMedium: 2 - property int borderThick: 3 + // Border + property int borderThin: 1 + property int borderMedium: 2 + property int borderThick: 3 - // Spacing - property int spacingExtraLarge: 20 - property int spacingLarge: 16 - property int spacingMedium: 12 - property int spacingSmall: 8 - - // Animation duration (ms) - property int animationFast: 150 - property int animationNormal: 300 - property int animationSlow: 500 - } + // Spacing + property int spacingExtraLarge: 20 + property int spacingLarge: 16 + property int spacingMedium: 12 + property int spacingSmall: 8 + // Animation duration (ms) + property int animationFast: 150 + property int animationNormal: 300 + property int animationSlow: 500 +} diff --git a/Theme/Theme.qml b/Theme/Theme.qml new file mode 100644 index 0000000..93a9f2a --- /dev/null +++ b/Theme/Theme.qml @@ -0,0 +1,143 @@ +// Theme.qml +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Services + +Singleton { + id: root + + // Backgrounds + property color backgroundPrimary: themeData.backgroundPrimary + property color backgroundSecondary: themeData.backgroundSecondary + property color backgroundTertiary: themeData.backgroundTertiary + + // Surfaces & Elevation + property color surface: themeData.surface + property color surfaceVariant: themeData.surfaceVariant + + // Text Colors + property color textPrimary: themeData.textPrimary + property color textSecondary: themeData.textSecondary + property color textDisabled: themeData.textDisabled + + // Accent Colors + property color accentPrimary: themeData.accentPrimary + property color accentSecondary: themeData.accentSecondary + property color accentTertiary: themeData.accentTertiary + + // Error/Warning + property color error: themeData.error + property color warning: themeData.warning + + // Highlights & Focus + property color highlight: themeData.highlight + property color rippleEffect: themeData.rippleEffect + + // Additional Theme Properties + property color onAccent: themeData.onAccent + property color outline: themeData.outline + + // Shadows & Overlays + property color shadow: applyOpacity(themeData.shadow, "B3") + property color overlay: applyOpacity(themeData.overlay, "66") + + // Font Properties + property string fontFamily: "Roboto" // Family for all text + + // Design reference resolution (for scale = 1.0) + readonly property int designScreenWidth: 2560 + readonly property int designScreenHeight: 1440 + + // Automatic, orientation-agnostic scaling + function scale(currentScreen) { + return 1.0 + // // 1) Per-monitor override wins + // try { + // const overrides = Settings.settings.monitorScaleOverrides || {}; + // if (currentScreen && currentScreen.name && overrides[currentScreen.name] !== undefined) { + // const overrideValue = overrides[currentScreen.name] + // if (isFinite(overrideValue)) return overrideValue + // } + // } catch (e) { + // // ignore + // } + + // // 2) Fallback: scale by diagonal pixel count relative to design resolution + // try { + // const w = Math.max(1, currentScreen ? (currentScreen.width || 0) : 0) + // const h = Math.max(1, currentScreen ? (currentScreen.height || 0) : 0) + // if (w > 1 && h > 1) { + // const diag = Math.sqrt(w * w + h * h) + // const baseDiag = Math.sqrt(designScreenWidth * designScreenWidth + designScreenHeight * designScreenHeight) + // const ratio = diag / baseDiag + // // Clamp to a reasonable range for UI legibility + // return Math.max(0.9, Math.min(1.6, ratio)) + // } + // } catch (e) { + // // ignore and fall through + // } + + // // 3) Safe default + // return 1.0 + } + + function applyOpacity(color, opacity) { + return color.replace("#", "#" + opacity) + } + + // FileView to load theme data from JSON file + FileView { + id: themeFile + path: Settings.themeFile + watchChanges: true + onFileChanged: reload() + onAdapterUpdated: writeAdapter() + onLoadFailed: function (error) { + if (error.toString().includes("No such file") || error === 2) { + // File doesn't exist, create it with default values + writeAdapter() + } + } + JsonAdapter { + id: themeData + + // Backgrounds + property string backgroundPrimary: "#191724" + property string backgroundSecondary: "#1f1d2e" + property string backgroundTertiary: "#26233a" + + // Surfaces & Elevation + property string surface: "#1f1d2e" + property string surfaceVariant: "#37354c" + + // Text Colors + property string textPrimary: "#e0def4" + property string textSecondary: "#908caa" + property string textDisabled: "#6e6a86" + + // Accent Colors + property string accentPrimary: "#ebbcba" + property string accentSecondary: "#31748f" + property string accentTertiary: "#9ccfd8" + + // Error/Warning + property string error: "#eb6f92" + property string warning: "#f6c177" + + // Highlights & Focus + property string highlight: "#c4a7e7" + property string rippleEffect: "#9ccfd8" + + // Additional Theme Properties + property string onAccent: "#191724" + property string outline: "#44415a" + + // Shadows & Overlays + property string shadow: "#191724" + property string overlay: "#191724" + } + } +} diff --git a/Widgets/NoctaliaToggle.qml b/Widgets/NoctaliaToggle.qml new file mode 100644 index 0000000..26701cd --- /dev/null +++ b/Widgets/NoctaliaToggle.qml @@ -0,0 +1,76 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Theme + +RowLayout { + id: root + + // Local scale convenience with safe fallback + readonly property real scale: (typeof screen !== 'undefined' + && screen) ? Theme.scale(screen) : 1.0 + + property string label: "" + property string description: "" + property bool value: false + property var onToggled: function () {} + + Layout.fillWidth: true + + ColumnLayout { + spacing: 4 * scale + Layout.fillWidth: true + + Text { + text: label + font.pixelSize: 13 * scale + font.bold: true + color: Theme.textPrimary + } + + Text { + text: description + font.pixelSize: 12 * scale + color: Theme.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + + Rectangle { + id: switcher + + width: 52 * scale + height: 32 * scale + radius: height / 2 + color: value ? Theme.accentPrimary : Theme.surfaceVariant + border.color: value ? Theme.accentPrimary : Theme.outline + border.width: 2 * scale + + Rectangle { + width: 28 * scale + height: 28 * scale + radius: height / 2 + color: Theme.surface + border.color: Theme.outline + border.width: 1 * scale + y: 2 * scale + x: value ? switcher.width - width - 2 * scale : 2 * scale + + Behavior on x { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + root.onToggled() + } + } + } +} diff --git a/shell.qml b/shell.qml index bf70526..3ba8775 100644 --- a/shell.qml +++ b/shell.qml @@ -1,9 +1,10 @@ + + /* Here we go, this is it. Rebuild time... No spaghetti code, preset sizing, proper project structure only "spawn" UI, do not do anything else here. */ - import QtQuick import Quickshell import Quickshell.Io @@ -11,15 +12,13 @@ import Quickshell.Widgets import qs.Modules.Bar ShellRoot { - id: root + id: root - Variants { - model: Quickshell.screens - - delegate: Bar { - modelData: item - } + Variants { + model: Quickshell.screens + delegate: Bar { + modelData: item } - + } } From b5a2bb3fe088bc34e917bc239532d36647cdd0e6 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sat, 9 Aug 2025 12:53:17 -0400 Subject: [PATCH 007/394] qmlfmt --- Bin/run-qmlfmt.sh | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100755 Bin/run-qmlfmt.sh diff --git a/Bin/run-qmlfmt.sh b/Bin/run-qmlfmt.sh new file mode 100755 index 0000000..f9aaa46 --- /dev/null +++ b/Bin/run-qmlfmt.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +# Uses: https://github.com/jesperhh/qmlfmt +# Can be installed from AUR "qmlfmt-git" +# Requires qt6-5compat + +find . -name "*.qml" -exec qmlfmt -t 2 -i 2 -w {} \; \ No newline at end of file From c8f4599ff19e568a7507675593082bc1891b3af5 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sat, 9 Aug 2025 13:12:13 -0400 Subject: [PATCH 008/394] Scaling service --- Modules/Bar/Bar.qml | 11 +++++++-- Services/Scaling.qml | 44 ++++++++++++++++++++++++++++++++++ Theme/Theme.qml | 38 ----------------------------- Widgets/NoctaliaIconButton.qml | 36 ++++++++++++++++++++++++++++ Widgets/NoctaliaToggle.qml | 9 ++++--- 5 files changed, 95 insertions(+), 43 deletions(-) create mode 100644 Services/Scaling.qml create mode 100644 Widgets/NoctaliaIconButton.qml diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 47fb6c1..f479d2d 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -1,5 +1,7 @@ import QtQuick import Quickshell +import QtQuick.Controls +import QtQuick.Layouts import qs.Widgets import qs.Theme @@ -27,7 +29,12 @@ PanelWindow { layer.enabled: true } - // Just testing - NoctaliaToggle {} + RowLayout { + // Just testing + NoctaliaToggle { + label: "Label" + description: "Description" + } + } } } diff --git a/Services/Scaling.qml b/Services/Scaling.qml new file mode 100644 index 0000000..5e646de --- /dev/null +++ b/Services/Scaling.qml @@ -0,0 +1,44 @@ +pragma Singleton + +import Quickshell + +Singleton { + id: root + + // Design reference resolution (for scale = 1.0) + readonly property int designScreenWidth: 2560 + readonly property int designScreenHeight: 1440 + + // Automatic, orientation-agnostic scaling + function scale(currentScreen) { + return 1.0 + // // 1) Per-monitor override wins + // try { + // const overrides = Settings.settings.monitorScaleOverrides || {}; + // if (currentScreen && currentScreen.name && overrides[currentScreen.name] !== undefined) { + // const overrideValue = overrides[currentScreen.name] + // if (isFinite(overrideValue)) return overrideValue + // } + // } catch (e) { + // // ignore + // } + + // // 2) Fallback: scale by diagonal pixel count relative to design resolution + // try { + // const w = Math.max(1, currentScreen ? (currentScreen.width || 0) : 0) + // const h = Math.max(1, currentScreen ? (currentScreen.height || 0) : 0) + // if (w > 1 && h > 1) { + // const diag = Math.sqrt(w * w + h * h) + // const baseDiag = Math.sqrt(designScreenWidth * designScreenWidth + designScreenHeight * designScreenHeight) + // const ratio = diag / baseDiag + // // Clamp to a reasonable range for UI legibility + // return Math.max(0.9, Math.min(1.6, ratio)) + // } + // } catch (e) { + // // ignore and fall through + // } + + // // 3) Safe default + // return 1.0 + } +} diff --git a/Theme/Theme.qml b/Theme/Theme.qml index 93a9f2a..bf1829f 100644 --- a/Theme/Theme.qml +++ b/Theme/Theme.qml @@ -1,4 +1,3 @@ -// Theme.qml pragma Singleton import QtQuick @@ -47,43 +46,6 @@ Singleton { // Font Properties property string fontFamily: "Roboto" // Family for all text - // Design reference resolution (for scale = 1.0) - readonly property int designScreenWidth: 2560 - readonly property int designScreenHeight: 1440 - - // Automatic, orientation-agnostic scaling - function scale(currentScreen) { - return 1.0 - // // 1) Per-monitor override wins - // try { - // const overrides = Settings.settings.monitorScaleOverrides || {}; - // if (currentScreen && currentScreen.name && overrides[currentScreen.name] !== undefined) { - // const overrideValue = overrides[currentScreen.name] - // if (isFinite(overrideValue)) return overrideValue - // } - // } catch (e) { - // // ignore - // } - - // // 2) Fallback: scale by diagonal pixel count relative to design resolution - // try { - // const w = Math.max(1, currentScreen ? (currentScreen.width || 0) : 0) - // const h = Math.max(1, currentScreen ? (currentScreen.height || 0) : 0) - // if (w > 1 && h > 1) { - // const diag = Math.sqrt(w * w + h * h) - // const baseDiag = Math.sqrt(designScreenWidth * designScreenWidth + designScreenHeight * designScreenHeight) - // const ratio = diag / baseDiag - // // Clamp to a reasonable range for UI legibility - // return Math.max(0.9, Math.min(1.6, ratio)) - // } - // } catch (e) { - // // ignore and fall through - // } - - // // 3) Safe default - // return 1.0 - } - function applyOpacity(color, opacity) { return color.replace("#", "#" + opacity) } diff --git a/Widgets/NoctaliaIconButton.qml b/Widgets/NoctaliaIconButton.qml new file mode 100644 index 0000000..bed7259 --- /dev/null +++ b/Widgets/NoctaliaIconButton.qml @@ -0,0 +1,36 @@ +import QtQuick +import Quickshell +import Quickshell.Widgets +import qs.Settings + +MouseArea { + id: root + property string icon + property bool enabled: true + property bool hovering: false + property real size: 32 + cursorShape: Qt.PointingHandCursor + implicitWidth: size + implicitHeight: size + + hoverEnabled: true + onEntered: hovering = true + onExited: hovering = false + + Rectangle { + anchors.fill: parent + radius: 8 + color: root.hovering ? Theme.accentPrimary : "transparent" + } + Text { + id: iconText + anchors.centerIn: parent + text: root.icon + font.family: "Material Symbols Outlined" + font.pixelSize: 24 * Theme.scale(screen) + color: root.hovering ? Theme.onAccent : Theme.textPrimary + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + opacity: root.enabled ? 1.0 : 0.5 + } +} diff --git a/Widgets/NoctaliaToggle.qml b/Widgets/NoctaliaToggle.qml index 26701cd..8b7b76b 100644 --- a/Widgets/NoctaliaToggle.qml +++ b/Widgets/NoctaliaToggle.qml @@ -1,19 +1,21 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import qs.Services import qs.Theme + RowLayout { id: root // Local scale convenience with safe fallback readonly property real scale: (typeof screen !== 'undefined' - && screen) ? Theme.scale(screen) : 1.0 + && screen) ? Scaling.scale(screen) : 1.0 property string label: "" property string description: "" property bool value: false - property var onToggled: function () {} + property var onToggled: function (value: bool) {} Layout.fillWidth: true @@ -69,7 +71,8 @@ RowLayout { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { - root.onToggled() + value = !value; + root.onToggled(value); } } } From da72c85bfedd10ff8fa024f9d7bd235b54b2d995 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sat, 9 Aug 2025 13:16:32 -0400 Subject: [PATCH 009/394] IconButton --- Modules/Bar/Bar.qml | 4 ++++ Widgets/NoctaliaIconButton.qml | 35 +++++++++++++++++++++------------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index f479d2d..cb0f20c 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -35,6 +35,10 @@ PanelWindow { label: "Label" description: "Description" } + + NoctaliaIconButton { + icon: "refresh" + } } } } diff --git a/Widgets/NoctaliaIconButton.qml b/Widgets/NoctaliaIconButton.qml index bed7259..7df74be 100644 --- a/Widgets/NoctaliaIconButton.qml +++ b/Widgets/NoctaliaIconButton.qml @@ -1,14 +1,21 @@ import QtQuick import Quickshell import Quickshell.Widgets -import qs.Settings +import qs.Services +import qs.Theme MouseArea { id: root + + // Local scale convenience with safe fallback + readonly property real scale: (typeof screen !== 'undefined' + && screen) ? Scaling.scale(screen) : 1.0 + property string icon property bool enabled: true property bool hovering: false property real size: 32 + cursorShape: Qt.PointingHandCursor implicitWidth: size implicitHeight: size @@ -18,19 +25,21 @@ MouseArea { onExited: hovering = false Rectangle { + anchors.fill: parent - radius: 8 + radius: width * 0.5 color: root.hovering ? Theme.accentPrimary : "transparent" - } - Text { - id: iconText - anchors.centerIn: parent - text: root.icon - font.family: "Material Symbols Outlined" - font.pixelSize: 24 * Theme.scale(screen) - color: root.hovering ? Theme.onAccent : Theme.textPrimary - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - opacity: root.enabled ? 1.0 : 0.5 + + Text { + id: iconText + anchors.centerIn: parent + text: root.icon + font.family: "Material Symbols Outlined" + font.pixelSize: 24 * scale + color: root.hovering ? Theme.onAccent : Theme.textPrimary + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + opacity: root.enabled ? 1.0 : 0.5 + } } } From d8debd2429f939ff6306cbf0c89a1e2401d22a33 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sat, 9 Aug 2025 13:59:34 -0400 Subject: [PATCH 010/394] NIconButton and NToggle --- Widgets/NIconButton.qml | 44 ++++++++++++++++++++ Widgets/{NoctaliaToggle.qml => NToggle.qml} | 0 Widgets/NoctaliaIconButton.qml | 45 --------------------- 3 files changed, 44 insertions(+), 45 deletions(-) create mode 100644 Widgets/NIconButton.qml rename Widgets/{NoctaliaToggle.qml => NToggle.qml} (100%) delete mode 100644 Widgets/NoctaliaIconButton.qml diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml new file mode 100644 index 0000000..e8b1228 --- /dev/null +++ b/Widgets/NIconButton.qml @@ -0,0 +1,44 @@ +import QtQuick +import Quickshell +import Quickshell.Widgets +import qs.Services +import qs.Theme + +Rectangle { + id: root + + // Local scale convenience with safe fallback + readonly property real scale: (typeof screen !== 'undefined' + && screen) ? Scaling.scale(screen) : 1.0 + + property real size: 32 * scale + property string icon + property bool enabled: true + property bool hovering: false + + implicitWidth: size + implicitHeight: size + radius: width * 0.5 + + color: root.hovering ? Theme.accentPrimary : "transparent" + + Text { + id: iconText + anchors.centerIn: parent + text: root.icon + font.family: "Material Symbols Outlined" + font.pixelSize: 24 * scale + color: root.hovering ? Theme.onAccent : Theme.textPrimary + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + opacity: root.enabled ? 1.0 : 0.5 + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onEntered: hovering = true + onExited: hovering = false + } +} diff --git a/Widgets/NoctaliaToggle.qml b/Widgets/NToggle.qml similarity index 100% rename from Widgets/NoctaliaToggle.qml rename to Widgets/NToggle.qml diff --git a/Widgets/NoctaliaIconButton.qml b/Widgets/NoctaliaIconButton.qml deleted file mode 100644 index 7df74be..0000000 --- a/Widgets/NoctaliaIconButton.qml +++ /dev/null @@ -1,45 +0,0 @@ -import QtQuick -import Quickshell -import Quickshell.Widgets -import qs.Services -import qs.Theme - -MouseArea { - id: root - - // Local scale convenience with safe fallback - readonly property real scale: (typeof screen !== 'undefined' - && screen) ? Scaling.scale(screen) : 1.0 - - property string icon - property bool enabled: true - property bool hovering: false - property real size: 32 - - cursorShape: Qt.PointingHandCursor - implicitWidth: size - implicitHeight: size - - hoverEnabled: true - onEntered: hovering = true - onExited: hovering = false - - Rectangle { - - anchors.fill: parent - radius: width * 0.5 - color: root.hovering ? Theme.accentPrimary : "transparent" - - Text { - id: iconText - anchors.centerIn: parent - text: root.icon - font.family: "Material Symbols Outlined" - font.pixelSize: 24 * scale - color: root.hovering ? Theme.onAccent : Theme.textPrimary - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - opacity: root.enabled ? 1.0 : 0.5 - } - } -} From 33217e33edf2acf09bbbe6ff5825c5643dedfc07 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sat, 9 Aug 2025 14:28:39 -0400 Subject: [PATCH 011/394] Using proper Style everywhere --- Modules/Bar/Bar.qml | 8 +++--- Services/Scaling.qml | 60 +++++++++++++++++++++-------------------- Theme/Style.qml | 11 +++++--- Widgets/NIconButton.qml | 9 +++---- Widgets/NToggle.qml | 39 ++++++++++++++------------- 5 files changed, 66 insertions(+), 61 deletions(-) diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index cb0f20c..6b9ff4e 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -3,15 +3,17 @@ import Quickshell import QtQuick.Controls import QtQuick.Layouts import qs.Widgets +import qs.Services import qs.Theme PanelWindow { id: root + readonly property real scaling: Scaling.scale(screen) property var modelData screen: modelData - implicitHeight: 36 + implicitHeight: Style.barHeight * scaling color: "transparent" anchors { @@ -31,12 +33,12 @@ PanelWindow { RowLayout { // Just testing - NoctaliaToggle { + NToggle { label: "Label" description: "Description" } - NoctaliaIconButton { + NIconButton { icon: "refresh" } } diff --git a/Services/Scaling.qml b/Services/Scaling.qml index 5e646de..d71fef4 100644 --- a/Services/Scaling.qml +++ b/Services/Scaling.qml @@ -10,35 +10,37 @@ Singleton { readonly property int designScreenHeight: 1440 // Automatic, orientation-agnostic scaling - function scale(currentScreen) { + function scale(aScreen) { + if (typeof aScreen !== 'undefined' & aScreen) { + + // // 1) Per-monitor override wins + // try { + // const overrides = Settings.settings.monitorScaleOverrides || {}; + // if (currentScreen && currentScreen.name && overrides[currentScreen.name] !== undefined) { + // const overrideValue = overrides[currentScreen.name] + // if (isFinite(overrideValue)) return overrideValue + // } + // } catch (e) { + // // ignore + // } + + // // 2) Fallback: scale by diagonal pixel count relative to design resolution + // try { + // const w = Math.max(1, currentScreen ? (currentScreen.width || 0) : 0) + // const h = Math.max(1, currentScreen ? (currentScreen.height || 0) : 0) + // if (w > 1 && h > 1) { + // const diag = Math.sqrt(w * w + h * h) + // const baseDiag = Math.sqrt(designScreenWidth * designScreenWidth + designScreenHeight * designScreenHeight) + // const ratio = diag / baseDiag + // // Clamp to a reasonable range for UI legibility + // return Math.max(0.9, Math.min(1.6, ratio)) + // } + // } catch (e) { + // // ignore and fall through + // } + } + + // 3) Safe default return 1.0 - // // 1) Per-monitor override wins - // try { - // const overrides = Settings.settings.monitorScaleOverrides || {}; - // if (currentScreen && currentScreen.name && overrides[currentScreen.name] !== undefined) { - // const overrideValue = overrides[currentScreen.name] - // if (isFinite(overrideValue)) return overrideValue - // } - // } catch (e) { - // // ignore - // } - - // // 2) Fallback: scale by diagonal pixel count relative to design resolution - // try { - // const w = Math.max(1, currentScreen ? (currentScreen.width || 0) : 0) - // const h = Math.max(1, currentScreen ? (currentScreen.height || 0) : 0) - // if (w > 1 && h > 1) { - // const diag = Math.sqrt(w * w + h * h) - // const baseDiag = Math.sqrt(designScreenWidth * designScreenWidth + designScreenHeight * designScreenHeight) - // const ratio = diag / baseDiag - // // Clamp to a reasonable range for UI legibility - // return Math.max(0.9, Math.min(1.6, ratio)) - // } - // } catch (e) { - // // ignore and fall through - // } - - // // 3) Safe default - // return 1.0 } } diff --git a/Theme/Style.qml b/Theme/Style.qml index 1479346..e6a91e0 100644 --- a/Theme/Style.qml +++ b/Theme/Style.qml @@ -13,10 +13,10 @@ Singleton { */ // Font - property real fontExtraLarge: 32 - property real fontLarge: 16 - property real fontMedium: 14 - property real fontSmall: 12 + property real fontExtraLarge: 20 + property real fontLarge: 14 + property real fontMedium: 10 + property real fontSmall: 8 // Font weight property int fontWeightRegular: 400 @@ -43,4 +43,7 @@ Singleton { property int animationFast: 150 property int animationNormal: 300 property int animationSlow: 500 + + property int barHeight: 36 + property int baseWidgetHeight: 32 } diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml index e8b1228..41fafbf 100644 --- a/Widgets/NIconButton.qml +++ b/Widgets/NIconButton.qml @@ -7,11 +7,8 @@ import qs.Theme Rectangle { id: root - // Local scale convenience with safe fallback - readonly property real scale: (typeof screen !== 'undefined' - && screen) ? Scaling.scale(screen) : 1.0 - - property real size: 32 * scale + readonly property real scaling: Scaling.scale(screen) + property real size: Style.baseWidgetHeight * scaling property string icon property bool enabled: true property bool hovering: false @@ -27,7 +24,7 @@ Rectangle { anchors.centerIn: parent text: root.icon font.family: "Material Symbols Outlined" - font.pixelSize: 24 * scale + font.pointSize: Style.fontExtraLarge * scaling color: root.hovering ? Theme.onAccent : Theme.textPrimary horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter diff --git a/Widgets/NToggle.qml b/Widgets/NToggle.qml index 8b7b76b..0a7cead 100644 --- a/Widgets/NToggle.qml +++ b/Widgets/NToggle.qml @@ -8,31 +8,29 @@ import qs.Theme RowLayout { id: root - // Local scale convenience with safe fallback - readonly property real scale: (typeof screen !== 'undefined' - && screen) ? Scaling.scale(screen) : 1.0 - + readonly property real scaling: Scaling.scale(screen) property string label: "" property string description: "" property bool value: false + property bool hovering: false property var onToggled: function (value: bool) {} Layout.fillWidth: true ColumnLayout { - spacing: 4 * scale + spacing: 2 * scaling Layout.fillWidth: true Text { text: label - font.pixelSize: 13 * scale + font.pointSize: Style.fontMedium * scaling font.bold: true color: Theme.textPrimary } Text { text: description - font.pixelSize: 12 * scale + font.pointSize: Style.fontSmall * scaling color: Theme.textSecondary wrapMode: Text.WordWrap Layout.fillWidth: true @@ -42,22 +40,22 @@ RowLayout { Rectangle { id: switcher - width: 52 * scale - height: 32 * scale - radius: height / 2 - color: value ? Theme.accentPrimary : Theme.surfaceVariant + width: Style.baseWidgetHeight * 1.625 * scaling + height: Style.baseWidgetHeight * scaling + radius: height * 0.5 + color: value ? Theme.accentPrimary :Theme.surfaceVariant border.color: value ? Theme.accentPrimary : Theme.outline - border.width: 2 * scale + border.width: Math.max(1, 1.5 * scale) Rectangle { - width: 28 * scale - height: 28 * scale - radius: height / 2 + width: (Style.baseWidgetHeight- 4) * scaling + height: (Style.baseWidgetHeight - 4) * scaling + radius: height * 0.5 color: Theme.surface - border.color: Theme.outline - border.width: 1 * scale - y: 2 * scale - x: value ? switcher.width - width - 2 * scale : 2 * scale + border.color: hovering ? Theme.textDisabled : Theme.outline + border.width: Math.max(1, 1.5 * scale) + y: 2 * scaling + x: value ? switcher.width - width - 2 * scale : 2 * scaling Behavior on x { NumberAnimation { @@ -70,6 +68,9 @@ RowLayout { MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onEntered: hovering = true + onExited: hovering = false onClicked: { value = !value; root.onToggled(value); From 37fdc538831872e8dbe6c5ffe8502af17de47835 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sat, 9 Aug 2025 14:43:52 -0400 Subject: [PATCH 012/394] NSlider --- Modules/Bar/Bar.qml | 2 + Theme/Style.qml | 1 + Widgets/NSlider.qml | 102 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+) create mode 100644 Widgets/NSlider.qml diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 6b9ff4e..47ee604 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -41,6 +41,8 @@ PanelWindow { NIconButton { icon: "refresh" } + + NSlider {} } } } diff --git a/Theme/Style.qml b/Theme/Style.qml index e6a91e0..a03783b 100644 --- a/Theme/Style.qml +++ b/Theme/Style.qml @@ -46,4 +46,5 @@ Singleton { property int barHeight: 36 property int baseWidgetHeight: 32 + property int sliderWidth: 200 } diff --git a/Widgets/NSlider.qml b/Widgets/NSlider.qml new file mode 100644 index 0000000..5f4e8e3 --- /dev/null +++ b/Widgets/NSlider.qml @@ -0,0 +1,102 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import qs.Services +import qs.Theme + +Slider { + id: slider + + readonly property real scaling: Scaling.scale(screen) + readonly property real knobDiameter: Style.baseWidgetHeight * 0.75 * scaling + readonly property real trackHeight: knobDiameter * 0.5 + readonly property real cutoutExtra: Style.baseWidgetHeight * 0.25 * scaling + + // Optional color to cut the track beneath the knob (should match surrounding background) + property var cutoutColor + property var screen + property bool snapAlways: true + + snapMode: snapAlways ? Slider.SnapAlways : Slider.SnapOnRelease + + implicitHeight: Math.max(trackHeight, knobDiameter) + + background: Rectangle { + x: slider.leftPadding + y: slider.topPadding + slider.availableHeight / 2 - height / 2 + implicitWidth: Style.sliderWidth + implicitHeight: trackHeight + width: slider.availableWidth + height: implicitHeight + radius: height / 2 + color: Theme.surfaceVariant + + Rectangle { + id: activeTrack + width: slider.visualPosition * parent.width + height: parent.height + color: Theme.accentPrimary + radius: parent.radius + + Behavior on width { + NumberAnimation { + duration: 120 + easing.type: Easing.OutQuad + } + } + } + + // Circular cutout + Rectangle { + id: knobCutout + width: knobDiameter + cutoutExtra + height: knobDiameter + cutoutExtra + radius: width / 2 + color: slider.cutoutColor !== undefined ? slider.cutoutColor : Theme.backgroundPrimary + x: Math.max( + 0, Math.min( + parent.width - width, + slider.visualPosition * (parent.width - slider.knobDiameter) - cutoutExtra / 2)) + y: (parent.height - height) / 2 + } + } + + handle: Item { + id: handleRoot + width: knob.implicitWidth + height: knob.implicitHeight + x: slider.leftPadding + slider.visualPosition * (slider.availableWidth - width) + y: slider.topPadding + slider.availableHeight / 2 - height / 2 + + // Subtle shadow for a more polished look (keeps theme colors) + MultiEffect { + anchors.fill: knob + source: knob + shadowEnabled: true + shadowColor: Theme.shadow + shadowOpacity: 0.25 + shadowHorizontalOffset: 0 + shadowVerticalOffset: 1 + shadowBlur: 8 + } + + Rectangle { + id: knob + implicitWidth: knobDiameter + implicitHeight: knobDiameter + radius: width * 0.5 + color: slider.pressed ? Theme.surfaceVariant : Theme.surface + border.color: Theme.accentPrimary + border.width: 2 * scaling + // Press feedback halo (using accent color, low opacity) + Rectangle { + anchors.centerIn: parent + width: parent.width + 8 * scaling + height: parent.height + 8 * scaling + radius: width / 2 + color: Theme.accentPrimary + opacity: slider.pressed ? 0.16 : 0.0 + } + } + } +} From 30eeeeca3666e8522ad1104d15bde984277df21f Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sat, 9 Aug 2025 14:45:34 -0400 Subject: [PATCH 013/394] NSlider: disable easing TBC. --- Widgets/NSlider.qml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Widgets/NSlider.qml b/Widgets/NSlider.qml index 5f4e8e3..f7cd96a 100644 --- a/Widgets/NSlider.qml +++ b/Widgets/NSlider.qml @@ -38,12 +38,13 @@ Slider { color: Theme.accentPrimary radius: parent.radius - Behavior on width { - NumberAnimation { - duration: 120 - easing.type: Easing.OutQuad - } - } + // Feels more responsive without animation + // Behavior on width { + // NumberAnimation { + // duration: 50 + // easing.type: Easing.OutQuad + // } + // } } // Circular cutout From 43eacb7e679b5d31615b49929d99ed81b55d4e95 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sat, 9 Aug 2025 14:53:41 -0400 Subject: [PATCH 014/394] NSlider less boldy --- Widgets/NSlider.qml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Widgets/NSlider.qml b/Widgets/NSlider.qml index f7cd96a..9bbcdc6 100644 --- a/Widgets/NSlider.qml +++ b/Widgets/NSlider.qml @@ -10,7 +10,7 @@ Slider { readonly property real scaling: Scaling.scale(screen) readonly property real knobDiameter: Style.baseWidgetHeight * 0.75 * scaling readonly property real trackHeight: knobDiameter * 0.5 - readonly property real cutoutExtra: Style.baseWidgetHeight * 0.25 * scaling + readonly property real cutoutExtra: Style.baseWidgetHeight * 0.1 * scaling // Optional color to cut the track beneath the knob (should match surrounding background) property var cutoutColor @@ -18,7 +18,6 @@ Slider { property bool snapAlways: true snapMode: snapAlways ? Slider.SnapAlways : Slider.SnapOnRelease - implicitHeight: Math.max(trackHeight, knobDiameter) background: Rectangle { @@ -88,7 +87,7 @@ Slider { radius: width * 0.5 color: slider.pressed ? Theme.surfaceVariant : Theme.surface border.color: Theme.accentPrimary - border.width: 2 * scaling + border.width: Math.max(1, 2 * scaling) // Press feedback halo (using accent color, low opacity) Rectangle { anchors.centerIn: parent From 4a4564fe5fea8c606c7d149322b3b4d136d4d661 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sat, 9 Aug 2025 15:44:03 -0400 Subject: [PATCH 015/394] NSlide: improved id handling --- Widgets/NSlider.qml | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/Widgets/NSlider.qml b/Widgets/NSlider.qml index 9bbcdc6..57aed7d 100644 --- a/Widgets/NSlider.qml +++ b/Widgets/NSlider.qml @@ -5,7 +5,7 @@ import qs.Services import qs.Theme Slider { - id: slider + id: root readonly property real scaling: Scaling.scale(screen) readonly property real knobDiameter: Style.baseWidgetHeight * 0.75 * scaling @@ -21,18 +21,18 @@ Slider { implicitHeight: Math.max(trackHeight, knobDiameter) background: Rectangle { - x: slider.leftPadding - y: slider.topPadding + slider.availableHeight / 2 - height / 2 + x: root.leftPadding + y: root.topPadding + root.availableHeight / 2 - height / 2 implicitWidth: Style.sliderWidth implicitHeight: trackHeight - width: slider.availableWidth + width: root.availableWidth height: implicitHeight radius: height / 2 color: Theme.surfaceVariant Rectangle { id: activeTrack - width: slider.visualPosition * parent.width + width: root.visualPosition * parent.width height: parent.height color: Theme.accentPrimary radius: parent.radius @@ -52,21 +52,20 @@ Slider { width: knobDiameter + cutoutExtra height: knobDiameter + cutoutExtra radius: width / 2 - color: slider.cutoutColor !== undefined ? slider.cutoutColor : Theme.backgroundPrimary + color: root.cutoutColor !== undefined ? root.cutoutColor : Theme.backgroundPrimary x: Math.max( 0, Math.min( parent.width - width, - slider.visualPosition * (parent.width - slider.knobDiameter) - cutoutExtra / 2)) + root.visualPosition * (parent.width - root.knobDiameter) - cutoutExtra / 2)) y: (parent.height - height) / 2 } } handle: Item { - id: handleRoot width: knob.implicitWidth height: knob.implicitHeight - x: slider.leftPadding + slider.visualPosition * (slider.availableWidth - width) - y: slider.topPadding + slider.availableHeight / 2 - height / 2 + x: root.leftPadding + root.visualPosition * (root.availableWidth - width) + y: root.topPadding + root.availableHeight / 2 - height / 2 // Subtle shadow for a more polished look (keeps theme colors) MultiEffect { @@ -85,7 +84,7 @@ Slider { implicitWidth: knobDiameter implicitHeight: knobDiameter radius: width * 0.5 - color: slider.pressed ? Theme.surfaceVariant : Theme.surface + color: root.pressed ? Theme.surfaceVariant : Theme.surface border.color: Theme.accentPrimary border.width: Math.max(1, 2 * scaling) // Press feedback halo (using accent color, low opacity) @@ -95,7 +94,7 @@ Slider { height: parent.height + 8 * scaling radius: width / 2 color: Theme.accentPrimary - opacity: slider.pressed ? 0.16 : 0.0 + opacity: root.pressed ? 0.16 : 0.0 } } } From 37905086744bc0058e09441ee1c7af74cb01b089 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sat, 9 Aug 2025 16:08:58 -0400 Subject: [PATCH 016/394] NTooltip: wip --- Modules/Bar/Bar.qml | 21 ++++++- Widgets/NIconButton.qml | 12 +++- Widgets/NTooltip.qml | 129 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 159 insertions(+), 3 deletions(-) create mode 100644 Widgets/NTooltip.qml diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 47ee604..6887f68 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -31,18 +31,37 @@ PanelWindow { layer.enabled: true } + // Testing widgets RowLayout { - // Just testing + NToggle { label: "Label" description: "Description" + onToggled: function(value: bool) { + console.log("NToggle: " + value) + } } NIconButton { + id: myIconButton icon: "refresh" + onEntered: function() { + myTooltip.tooltipVisible = true; + } + onExited: function() { + myTooltip.tooltipVisible = false; + } + } + NTooltip { + id: myTooltip + targetItem: myIconButton + positionAbove: false + text: "Hello world" } NSlider {} + + } } } diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml index 41fafbf..70d475b 100644 --- a/Widgets/NIconButton.qml +++ b/Widgets/NIconButton.qml @@ -12,6 +12,8 @@ Rectangle { property string icon property bool enabled: true property bool hovering: false + property var onEntered: function () {} + property var onExited: function () {} implicitWidth: size implicitHeight: size @@ -35,7 +37,13 @@ Rectangle { anchors.fill: parent cursorShape: Qt.PointingHandCursor hoverEnabled: true - onEntered: hovering = true - onExited: hovering = false + onEntered: { + hovering = true + root.onEntered() + } + onExited: { + hovering = false + root.onExited() + } } } diff --git a/Widgets/NTooltip.qml b/Widgets/NTooltip.qml new file mode 100644 index 0000000..3f74281 --- /dev/null +++ b/Widgets/NTooltip.qml @@ -0,0 +1,129 @@ +import QtQuick +import QtQuick.Window 2.15 +import qs.Services +import qs.Theme + +Window { + id: tooltipWindow + + readonly property real scaling: Scaling.scale(screen) + property string text: "" + property bool tooltipVisible: false + property Item targetItem: null + property int delay: 300 + property bool positionAbove: true + + flags: Qt.ToolTip | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint + color: "transparent" + visible: false + + property var _timerObj: null + + onTooltipVisibleChanged: { + if (tooltipVisible) { + if (delay > 0) { + if (_timerObj) { + _timerObj.destroy() + _timerObj = null + } + _timerObj = Qt.createQmlObject( + 'import QtQuick 2.0; Timer { interval: ' + delay + + '; running: true; repeat: false; onTriggered: tooltipWindow._showNow() }', + tooltipWindow) + } else { + _showNow() + } + } else { + _hideNow() + } + } + + function _showNow() { + + width = Math.max(50 * scaling, tooltipText.implicitWidth + 24 * scaling) + height = Math.max(50 * scaling, tooltipText.implicitHeight + 16 * scaling) + + if (!targetItem) + return + + if (positionAbove) { + // Position tooltip above the target item + var pos = targetItem.mapToGlobal(0, 0) + x = pos.x - width / 2 + targetItem.width / 2 + y = pos.y - height - 12 // 12 px margin above + } else { + // Position tooltip below the target item + var pos = targetItem.mapToGlobal(0, targetItem.height) + x = pos.x - width / 2 + targetItem.width / 2 + y = pos.y + 12 // 12 px margin below + } + visible = true + } + + function _hideNow() { + visible = false + if (_timerObj) { + _timerObj.destroy() + _timerObj = null + } + } + + Connections { + target: tooltipWindow.targetItem + function onXChanged() { + if (tooltipWindow.visible) + tooltipWindow._showNow() + } + function onYChanged() { + if (tooltipWindow.visible) + tooltipWindow._showNow() + } + function onWidthChanged() { + if (tooltipWindow.visible) + tooltipWindow._showNow() + } + function onHeightChanged() { + if (tooltipWindow.visible) + tooltipWindow._showNow() + } + } + + Rectangle { + anchors.fill: parent + radius: 18 + color: Theme.backgroundTertiary || "#222" + border.color: Theme.outline || "#444" + border.width: 1 * scaling + opacity: 0.97 + z: 1 + } + + Text { + id: tooltipText + text: tooltipWindow.text + color: Theme.textPrimary + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeSmall * scaling + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + wrapMode: Text.Wrap + padding: 8 + z: 2 + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.ArrowCursor + hoverEnabled: true + onExited: tooltipWindow.tooltipVisible = false + + } + + onTextChanged: { + width = Math.max(minimumWidth * scaling, + tooltipText.implicitWidth + 24 * scaling) + height = Math.max(minimumHeight * scaling, + tooltipText.implicitHeight + 16 * scaling) + } +} From 446bbe68d82cc80ff047d98928cbbb3bf35e513d Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sat, 9 Aug 2025 16:21:28 -0400 Subject: [PATCH 017/394] NTooltip: wip --- Modules/Bar/Bar.qml | 6 +-- Widgets/NTooltip.qml | 94 +++++++++++++++++++++----------------------- 2 files changed, 48 insertions(+), 52 deletions(-) diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 6887f68..423e901 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -46,15 +46,15 @@ PanelWindow { id: myIconButton icon: "refresh" onEntered: function() { - myTooltip.tooltipVisible = true; + myTooltip.show(); } onExited: function() { - myTooltip.tooltipVisible = false; + myTooltip.hide(); } } NTooltip { id: myTooltip - targetItem: myIconButton + target: myIconButton positionAbove: false text: "Hello world" } diff --git a/Widgets/NTooltip.qml b/Widgets/NTooltip.qml index 3f74281..bdeacd4 100644 --- a/Widgets/NTooltip.qml +++ b/Widgets/NTooltip.qml @@ -4,23 +4,23 @@ import qs.Services import qs.Theme Window { - id: tooltipWindow + id: root readonly property real scaling: Scaling.scale(screen) + property bool isVisible: false property string text: "" - property bool tooltipVisible: false - property Item targetItem: null + property Item target: null property int delay: 300 - property bool positionAbove: true + property bool positionAbove: false + property var _timerObj: null + flags: Qt.ToolTip | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint color: "transparent" visible: false - property var _timerObj: null - - onTooltipVisibleChanged: { - if (tooltipVisible) { + onIsVisibleChanged: { + if (isVisible) { if (delay > 0) { if (_timerObj) { _timerObj.destroy() @@ -28,8 +28,8 @@ Window { } _timerObj = Qt.createQmlObject( 'import QtQuick 2.0; Timer { interval: ' + delay - + '; running: true; repeat: false; onTriggered: tooltipWindow._showNow() }', - tooltipWindow) + + '; running: true; repeat: false; onTriggered: root._showNow() }', + root) } else { _showNow() } @@ -38,23 +38,31 @@ Window { } } + function show() { + isVisible = true; + } + function hide() { + isVisible = false; + } + function _showNow() { - + // Compute new size everytime we show the tooltip width = Math.max(50 * scaling, tooltipText.implicitWidth + 24 * scaling) height = Math.max(50 * scaling, tooltipText.implicitHeight + 16 * scaling) - if (!targetItem) + if (!target) { return + } if (positionAbove) { - // Position tooltip above the target item - var pos = targetItem.mapToGlobal(0, 0) - x = pos.x - width / 2 + targetItem.width / 2 + // Position tooltip above the target + var pos = target.mapToGlobal(0, 0) + x = pos.x - width / 2 + target.width / 2 y = pos.y - height - 12 // 12 px margin above } else { - // Position tooltip below the target item - var pos = targetItem.mapToGlobal(0, targetItem.height) - x = pos.x - width / 2 + targetItem.width / 2 + // Position tooltip below the target + var pos = target.mapToGlobal(0, target.height) + x = pos.x - width / 2 + target.width / 2 y = pos.y + 12 // 12 px margin below } visible = true @@ -69,30 +77,34 @@ Window { } Connections { - target: tooltipWindow.targetItem + target: root.target function onXChanged() { - if (tooltipWindow.visible) - tooltipWindow._showNow() + if (root.visible) { + root._showNow() + } } function onYChanged() { - if (tooltipWindow.visible) - tooltipWindow._showNow() + if (root.visible) { + root._showNow() + } } function onWidthChanged() { - if (tooltipWindow.visible) - tooltipWindow._showNow() + if (root.visible) { + root._showNow() + } } function onHeightChanged() { - if (tooltipWindow.visible) - tooltipWindow._showNow() + if (root.visible) { + root._showNow() + } } } Rectangle { anchors.fill: parent - radius: 18 - color: Theme.backgroundTertiary || "#222" - border.color: Theme.outline || "#444" + radius: Style.radiusMedium * scaling + color: Theme.backgroundTertiary + border.color: Theme.outline border.width: 1 * scaling opacity: 0.97 z: 1 @@ -100,30 +112,14 @@ Window { Text { id: tooltipText - text: tooltipWindow.text + text: root.text color: Theme.textPrimary font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeSmall * scaling + font.pointSize: Theme.fontSizeMedium * scaling * 4 anchors.centerIn: parent horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter wrapMode: Text.Wrap - padding: 8 - z: 2 - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.ArrowCursor - hoverEnabled: true - onExited: tooltipWindow.tooltipVisible = false - - } - - onTextChanged: { - width = Math.max(minimumWidth * scaling, - tooltipText.implicitWidth + 24 * scaling) - height = Math.max(minimumHeight * scaling, - tooltipText.implicitHeight + 16 * scaling) + z: 1 } } From 0e037561f387dd7e9bc9136d7950d778680d87c6 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sat, 9 Aug 2025 16:32:49 -0400 Subject: [PATCH 018/394] NTooltip: cleaned up --- Widgets/NTooltip.qml | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/Widgets/NTooltip.qml b/Widgets/NTooltip.qml index bdeacd4..8a8e101 100644 --- a/Widgets/NTooltip.qml +++ b/Widgets/NTooltip.qml @@ -1,5 +1,4 @@ import QtQuick -import QtQuick.Window 2.15 import qs.Services import qs.Theme @@ -8,12 +7,10 @@ Window { readonly property real scaling: Scaling.scale(screen) property bool isVisible: false - property string text: "" + property string text: "Placeholder" property Item target: null property int delay: 300 property bool positionAbove: false - property var _timerObj: null - flags: Qt.ToolTip | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint color: "transparent" @@ -22,14 +19,7 @@ Window { onIsVisibleChanged: { if (isVisible) { if (delay > 0) { - if (_timerObj) { - _timerObj.destroy() - _timerObj = null - } - _timerObj = Qt.createQmlObject( - 'import QtQuick 2.0; Timer { interval: ' + delay - + '; running: true; repeat: false; onTriggered: root._showNow() }', - root) + timerShow.running = true } else { _showNow() } @@ -39,10 +29,10 @@ Window { } function show() { - isVisible = true; + isVisible = true } function hide() { - isVisible = false; + isVisible = false } function _showNow() { @@ -70,10 +60,6 @@ Window { function _hideNow() { visible = false - if (_timerObj) { - _timerObj.destroy() - _timerObj = null - } } Connections { @@ -100,6 +86,17 @@ Window { } } + Timer { + id: timerShow + interval: delay + running: false + repeat: false + onTriggered: { + _showNow() + running = false + } + } + Rectangle { anchors.fill: parent radius: Style.radiusMedium * scaling @@ -112,11 +109,11 @@ Window { Text { id: tooltipText + anchors.centerIn: parent text: root.text color: Theme.textPrimary font.family: Theme.fontFamily - font.pointSize: Theme.fontSizeMedium * scaling * 4 - anchors.centerIn: parent + font.pointSize: Style.fontSmall * scaling horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter wrapMode: Text.Wrap From bce57c101a26bf1c49f0e7ffa56060e02be4f43c Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sat, 9 Aug 2025 18:00:57 -0400 Subject: [PATCH 019/394] Clock and calendar --- Helpers/Holidays.js | 81 ++++++++++++++++ Modules/Bar/Bar.qml | 2 +- Modules/Bar/Clock.qml | 56 +++++++++++ Services/.gitkeep | 0 Services/Scaling.qml | 2 +- Services/Settings.qml | 86 ++++++++++++++++- Services/Time.qml | 49 ++++++++++ Theme/Style.qml | 2 + Widgets/NCalendar.qml | 204 ++++++++++++++++++++++++++++++++++++++++ Widgets/NIconButton.qml | 4 + Widgets/NPanel.qml | 46 +++++++++ Widgets/NTooltip.qml | 2 +- 12 files changed, 530 insertions(+), 4 deletions(-) create mode 100644 Helpers/Holidays.js create mode 100644 Modules/Bar/Clock.qml delete mode 100644 Services/.gitkeep create mode 100644 Services/Time.qml create mode 100644 Widgets/NCalendar.qml create mode 100644 Widgets/NPanel.qml diff --git a/Helpers/Holidays.js b/Helpers/Holidays.js new file mode 100644 index 0000000..4bb3a60 --- /dev/null +++ b/Helpers/Holidays.js @@ -0,0 +1,81 @@ +var _countryCode = null; +var _regionCode = null; +var _regionName = null; +var _holidaysCache = {}; + +function getCountryCode(callback) { + if (_countryCode) { + callback(_countryCode); + return; + } + var xhr = new XMLHttpRequest(); + xhr.open("GET", "https://nominatim.openstreetmap.org/search?city="+ Settings.settings.weatherCity+"&country=&format=json&addressdetails=1&extratags=1", true); + xhr.onreadystatechange = function() { + if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { + var response = JSON.parse(xhr.responseText); + _countryCode = response?.[0]?.address?.country_code ?? "US"; + _regionCode = response?.[0]?.address?.["ISO3166-2-lvl4"] ?? ""; + _regionName = response?.[0]?.address?.state ?? ""; + callback(_countryCode); + } + } + xhr.send(); +} + +function getHolidays(year, countryCode, callback) { + var cacheKey = year + "-" + countryCode; + if (_holidaysCache[cacheKey]) { + callback(_holidaysCache[cacheKey]); + return; + } + var url = "https://date.nager.at/api/v3/PublicHolidays/" + year + "/" + countryCode; + var xhr = new XMLHttpRequest(); + xhr.open("GET", url, true); + xhr.onreadystatechange = function() { + if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { + var holidays = JSON.parse(xhr.responseText); + var augmentedHolidays = filterHolidaysByRegion(holidays); + _holidaysCache[cacheKey] = augmentedHolidays; + callback(augmentedHolidays); + } + } + xhr.send(); +} + +function filterHolidaysByRegion(holidays) { + if (!_regionCode) { + return holidays; + } + const retHolidays = []; + holidays.forEach(function(holiday) { + if (holiday.counties?.length > 0) { + let found = false; + holiday.counties.forEach(function(county) { + if (county.toLowerCase() === _regionCode.toLowerCase()) { + found = true; + } + }); + if (found) { + var regionText = " (" + _regionName + ")"; + holiday.name = holiday.name + regionText; + holiday.localName = holiday.localName + regionText; + retHolidays.push(holiday); + } + } else { + retHolidays.push(holiday); + } + }); + return retHolidays; +} + +function getHolidaysForMonth(year, month, callback) { + getCountryCode(function(countryCode) { + getHolidays(year, countryCode, function(holidays) { + var filtered = holidays.filter(function(h) { + var date = new Date(h.date); + return date.getFullYear() === year && date.getMonth() === month; + }); + callback(filtered); + }); + }); +} \ No newline at end of file diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 423e901..253cc6a 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -61,7 +61,7 @@ PanelWindow { NSlider {} - + Clock {} } } } diff --git a/Modules/Bar/Clock.qml b/Modules/Bar/Clock.qml new file mode 100644 index 0000000..57d9c20 --- /dev/null +++ b/Modules/Bar/Clock.qml @@ -0,0 +1,56 @@ +import QtQuick +import qs.Services +import qs.Theme +import qs.Widgets + +Rectangle { + id: root + + readonly property real scaling: Scaling.scale(screen) + + width: textItem.paintedWidth + height: textItem.paintedHeight + color: "transparent" + + Text { + id: textItem + text: Time.time + font.family: Theme.fontFamily + font.weight: Font.Bold + font.pointSize: Style.fontSmall * scaling + color: Theme.textPrimary + anchors.centerIn: parent + } + + MouseArea { + id: clockMouseArea + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onEntered: { + if (!calendar.visible) { + tooltip.show() + } + } + onExited: { + tooltip.hide() + } + onClicked: function () { + calendar.visible = !calendar.visible + if (calendar.visible) { + tooltip.hide(); + } + } + } + + NCalendar { + id: calendar + visible: false + } + + NTooltip { + id: tooltip + text: Time.dateString + target: root + } +} diff --git a/Services/.gitkeep b/Services/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/Services/Scaling.qml b/Services/Scaling.qml index d71fef4..02bf7b2 100644 --- a/Services/Scaling.qml +++ b/Services/Scaling.qml @@ -41,6 +41,6 @@ Singleton { } // 3) Safe default - return 1.0 + return 2.0 } } diff --git a/Services/Settings.qml b/Services/Settings.qml index fd6bfb9..6ec70e0 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -6,7 +6,6 @@ import Quickshell.Io import qs.Services Singleton { - id: root property string shellName: "Noctalia" property string settingsDir: Quickshell.env("NOCTALIA_SETTINGS_DIR") @@ -17,6 +16,7 @@ Singleton { || (settingsDir + "Settings.json") property string themeFile: Quickshell.env("NOCTALIA_THEME_FILE") || (settingsDir + "Theme.json") + property var settings: settingAdapter Item { Component.onCompleted: { @@ -24,4 +24,88 @@ Singleton { Quickshell.execDetached(["mkdir", "-p", settingsDir]) } } + + FileView { + id: settingFileView + path: settingsFile + watchChanges: true + onFileChanged: reload() + onAdapterUpdated: writeAdapter() + Component.onCompleted: function () { + reload() + } + onLoaded: function () {// Qt.callLater(function () { + // WallpaperManager.setCurrentWallpaper(settings.currentWallpaper, true); + // }) + } + onLoadFailed: function (error) { + if (error.toString().includes("No such file") || error === 2) { + // File doesn't exist, create it with default values + writeAdapter() + } + } + JsonAdapter { + id: settingAdapter + property string weatherCity: "Dinslaken" + property string profileImage: Quickshell.env("HOME") + "/.face" + property bool useFahrenheit: false + property string wallpaperFolder: "/usr/share/wallpapers" + property string currentWallpaper: "" + property string videoPath: "~/Videos/" + property bool showActiveWindow: true + property bool showActiveWindowIcon: false + property bool showSystemInfoInBar: false + property bool showCorners: false + property bool showTaskbar: true + property bool showMediaInBar: false + property bool useSWWW: false + property bool randomWallpaper: false + property bool useWallpaperTheme: false + property int wallpaperInterval: 300 + property string wallpaperResize: "crop" + property int transitionFps: 60 + property string transitionType: "random" + property real transitionDuration: 1.1 + property string visualizerType: "radial" + property bool reverseDayMonth: false + property bool use12HourClock: false + property bool dimPanels: true + property real fontSizeMultiplier: 1.0 // Font size multiplier (1.0 = normal, 1.2 = 20% larger, 0.8 = 20% smaller) + property int taskbarIconSize: 24 // Taskbar icon button size in pixels (default: 32, smaller: 24, larger: 40) + property var pinnedExecs: [] // Added for AppLauncher pinned apps + + property bool showDock: true + property bool dockExclusive: false + property bool wifiEnabled: false + property bool bluetoothEnabled: false + property int recordingFrameRate: 60 + property string recordingQuality: "very_high" + property string recordingCodec: "h264" + property string audioCodec: "opus" + property bool showCursor: true + property string colorRange: "limited" + + // Monitor/Display Settings + property var barMonitors: [] // Array of monitor names to show the bar on + property var dockMonitors: [] // Array of monitor names to show the dock on + property var notificationMonitors: [] // Array of monitor names to show notifications on, "*" means all monitors + property var monitorScaleOverrides: { + + } // Map of monitor name -> scale override (e.g., 0.8..2.0). When set, Theme.scale() returns this value + } + } + + Connections { + target: settingAdapter + function onRandomWallpaperChanged() { + WallpaperManager.toggleRandomWallpaper() + } + function onWallpaperIntervalChanged() { + WallpaperManager.restartRandomWallpaperTimer() + } + function onWallpaperFolderChanged() { + WallpaperManager.loadWallpapers() + } + function onNotificationMonitorsChanged() {} + } } diff --git a/Services/Time.qml b/Services/Time.qml new file mode 100644 index 0000000..0d43a9e --- /dev/null +++ b/Services/Time.qml @@ -0,0 +1,49 @@ +pragma Singleton + +import Quickshell +import QtQuick +import qs.Services + +Singleton { + id: root + + property var date: new Date() + property string time: Settings.settings.use12HourClock ? Qt.formatDateTime( + date, + "h:mm AP") : Qt.formatDateTime( + date, "HH:mm") + property string dateString: { + let now = date + let dayName = now.toLocaleDateString(Qt.locale(), "ddd") + dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1) + let day = now.getDate() + let suffix + if (day > 3 && day < 21) + suffix = 'th' + else + switch (day % 10) { + case 1: + suffix = "st" + break + case 2: + suffix = "nd" + break + case 3: + suffix = "rd" + break + default: + suffix = "th" + } + let month = now.toLocaleDateString(Qt.locale(), "MMMM") + let year = now.toLocaleDateString(Qt.locale(), "yyyy") + return `${dayName}, ` + (Settings.settings.reverseDayMonth ? `${month} ${day}${suffix} ${year}` : `${day}${suffix} ${month} ${year}`) + } + + Timer { + interval: 1000 + repeat: true + running: true + + onTriggered: root.date = new Date() + } +} diff --git a/Theme/Style.qml b/Theme/Style.qml index a03783b..3a268b3 100644 --- a/Theme/Style.qml +++ b/Theme/Style.qml @@ -47,4 +47,6 @@ Singleton { property int barHeight: 36 property int baseWidgetHeight: 32 property int sliderWidth: 200 + + property int tooltipDelay: 300 } diff --git a/Widgets/NCalendar.qml b/Widgets/NCalendar.qml new file mode 100644 index 0000000..68ba5d1 --- /dev/null +++ b/Widgets/NCalendar.qml @@ -0,0 +1,204 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import qs.Services +import qs.Theme + +import "../Helpers/Holidays.js" as Holidays + +NPanel { + id: calendarOverlay + + readonly property real scaling: Scaling.scale(screen) + + Rectangle { + color: Theme.backgroundPrimary + radius: 12 + border.color: Theme.backgroundTertiary + border.width: 1 + width: 340 * scaling + height: 380 + anchors.top: parent.top + anchors.right: parent.right + anchors.topMargin: 4 * scaling + anchors.rightMargin: 4 * scaling + + // Prevent closing when clicking in the panel bg + MouseArea { + anchors.fill: parent + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 16 + spacing: 12 + + // Month/Year header with navigation + RowLayout { + Layout.fillWidth: true + spacing: 8 + + NIconButton { + icon: "chevron_left" + onClicked: function () { + let newDate = new Date(calendar.year, calendar.month - 1, 1) + calendar.year = newDate.getFullYear() + calendar.month = newDate.getMonth() + } + } + + Text { + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + text: calendar.title + color: Theme.textPrimary + opacity: 0.7 + font.pointSize: Style.fontSmall * scaling + font.family: Theme.fontFamily + font.bold: true + } + + NIconButton { + icon: "chevron_right" + onClicked: function () { + let newDate = new Date(calendar.year, calendar.month + 1, 1) + calendar.year = newDate.getFullYear() + calendar.month = newDate.getMonth() + } + } + } + + DayOfWeekRow { + Layout.fillWidth: true + spacing: 0 + Layout.leftMargin: 8 // Align with grid + Layout.rightMargin: 8 + + delegate: Text { + text: shortName + color: Theme.textPrimary + opacity: 0.8 + font.pointSize: Style.fontSmall * scaling + font.family: Theme.fontFamily + font.bold: true + horizontalAlignment: Text.AlignHCenter + width: 32 + } + } + + MonthGrid { + id: calendar + + property var holidays: [] + + // Fetch holidays when calendar is opened or month/year changes + function updateHolidays() { + Holidays.getHolidaysForMonth(calendar.year, calendar.month, + function (holidays) { + calendar.holidays = holidays + }) + } + + Layout.fillWidth: true + Layout.leftMargin: 8 + Layout.rightMargin: 8 + spacing: 0 + month: Time.date.getMonth() + year: Time.date.getFullYear() + onMonthChanged: updateHolidays() + onYearChanged: updateHolidays() + Component.onCompleted: updateHolidays() + + // Optionally, update when the panel becomes visible + Connections { + function onVisibleChanged() { + if (calendarOverlay.visible) { + calendar.month = Time.date.getMonth() + calendar.year = Time.date.getFullYear() + calendar.updateHolidays() + } + } + + target: calendarOverlay + } + + delegate: Rectangle { + property var holidayInfo: calendar.holidays.filter(function (h) { + var d = new Date(h.date) + return d.getDate() === model.day && d.getMonth() === model.month + && d.getFullYear() === model.year + }) + property bool isHoliday: holidayInfo.length > 0 + + width: 32 + height: 32 + radius: 8 + color: { + if (model.today) + return Theme.accentPrimary + + if (mouseArea2.containsMouse) + return Theme.backgroundTertiary + + return "transparent" + } + + // Holiday dot indicator + Rectangle { + visible: isHoliday + width: 4 + height: 4 + radius: 4 + color: Theme.accentTertiary + anchors.top: parent.top + anchors.right: parent.right + anchors.topMargin: 4 + anchors.rightMargin: 4 + z: 2 + } + + Text { + anchors.centerIn: parent + text: model.day + color: model.today ? Theme.onAccent : Theme.textPrimary + opacity: model.month === calendar.month ? (mouseArea2.containsMouse ? 1 : 0.7) : 0.3 + font.pointSize: Style.fontSmall * scaling + font.family: Theme.fontFamily + font.bold: model.today ? true : false + } + + MouseArea { + id: mouseArea2 + + anchors.fill: parent + hoverEnabled: true + onEntered: { + if (isHoliday) { + holidayTooltip.text = holidayInfo.map(function (h) { + return h.localName + (h.name !== h.localName ? " (" + h.name + ")" : "") + + (h.global ? " [Global]" : "") + }).join(", ") + holidayTooltip.target = parent; + holidayTooltip.show(); + } + } + onExited: holidayTooltip.hide() + } + + NTooltip { + id: holidayTooltip + text: "" + } + + Behavior on color { + ColorAnimation { + duration: 150 + } + } + } + } + } + } +} diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml index 70d475b..bac4618 100644 --- a/Widgets/NIconButton.qml +++ b/Widgets/NIconButton.qml @@ -14,6 +14,7 @@ Rectangle { property bool hovering: false property var onEntered: function () {} property var onExited: function () {} + property var onClicked: function () {} implicitWidth: size implicitHeight: size @@ -45,5 +46,8 @@ Rectangle { hovering = false root.onExited() } + onClicked: { + root.onClicked() + } } } diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml new file mode 100644 index 0000000..b13ddbc --- /dev/null +++ b/Widgets/NPanel.qml @@ -0,0 +1,46 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland +import qs.Services +import qs.Theme + +PanelWindow { + id: outerPanel + + readonly property real scaling: Scaling.scale(screen) + property bool showOverlay: Settings.settings.dimPanels + property int topMargin: Style.barHeight * scaling + property color overlayColor: showOverlay ? Theme.overlay : "transparent" + + function dismiss() { + visible = false + } + + function show() { + visible = true + } + + implicitWidth: screen.width + implicitHeight: screen.height + color: visible ? overlayColor : "transparent" + visible: false + WlrLayershell.exclusionMode: ExclusionMode.Ignore + screen: (typeof modelData !== 'undefined' ? modelData : null) + anchors.top: true + anchors.left: true + anchors.right: true + anchors.bottom: true + margins.top: topMargin + + MouseArea { + anchors.fill: parent + onClicked: outerPanel.dismiss() + } + + Behavior on color { + ColorAnimation { + duration: 350 + easing.type: Easing.InOutCubic + } + } +} diff --git a/Widgets/NTooltip.qml b/Widgets/NTooltip.qml index 8a8e101..231fbb6 100644 --- a/Widgets/NTooltip.qml +++ b/Widgets/NTooltip.qml @@ -9,7 +9,7 @@ Window { property bool isVisible: false property string text: "Placeholder" property Item target: null - property int delay: 300 + property int delay: Style.tooltipDelay property bool positionAbove: false flags: Qt.ToolTip | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint From 03af84e297cbfb57a0ac07528314ef6d3d0ccd7b Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sat, 9 Aug 2025 18:02:18 -0400 Subject: [PATCH 020/394] All singletons in Services/ --- Modules/Bar/Bar.qml | 1 - Modules/Bar/Clock.qml | 1 - {Theme => Services}/Style.qml | 0 {Theme => Services}/Theme.qml | 0 Widgets/NCalendar.qml | 1 - Widgets/NIconButton.qml | 1 - Widgets/NPanel.qml | 1 - Widgets/NSlider.qml | 1 - Widgets/NToggle.qml | 2 -- Widgets/NTooltip.qml | 1 - 10 files changed, 9 deletions(-) rename {Theme => Services}/Style.qml (100%) rename {Theme => Services}/Theme.qml (100%) diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 253cc6a..34d07d7 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -4,7 +4,6 @@ import QtQuick.Controls import QtQuick.Layouts import qs.Widgets import qs.Services -import qs.Theme PanelWindow { id: root diff --git a/Modules/Bar/Clock.qml b/Modules/Bar/Clock.qml index 57d9c20..aa7a361 100644 --- a/Modules/Bar/Clock.qml +++ b/Modules/Bar/Clock.qml @@ -1,6 +1,5 @@ import QtQuick import qs.Services -import qs.Theme import qs.Widgets Rectangle { diff --git a/Theme/Style.qml b/Services/Style.qml similarity index 100% rename from Theme/Style.qml rename to Services/Style.qml diff --git a/Theme/Theme.qml b/Services/Theme.qml similarity index 100% rename from Theme/Theme.qml rename to Services/Theme.qml diff --git a/Widgets/NCalendar.qml b/Widgets/NCalendar.qml index 68ba5d1..5d09a5c 100644 --- a/Widgets/NCalendar.qml +++ b/Widgets/NCalendar.qml @@ -4,7 +4,6 @@ import QtQuick.Layouts import Quickshell import Quickshell.Wayland import qs.Services -import qs.Theme import "../Helpers/Holidays.js" as Holidays diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml index bac4618..041ecaf 100644 --- a/Widgets/NIconButton.qml +++ b/Widgets/NIconButton.qml @@ -2,7 +2,6 @@ import QtQuick import Quickshell import Quickshell.Widgets import qs.Services -import qs.Theme Rectangle { id: root diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index b13ddbc..4b614a7 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -2,7 +2,6 @@ import QtQuick import Quickshell import Quickshell.Wayland import qs.Services -import qs.Theme PanelWindow { id: outerPanel diff --git a/Widgets/NSlider.qml b/Widgets/NSlider.qml index 57aed7d..80932ac 100644 --- a/Widgets/NSlider.qml +++ b/Widgets/NSlider.qml @@ -2,7 +2,6 @@ import QtQuick import QtQuick.Controls import QtQuick.Effects import qs.Services -import qs.Theme Slider { id: root diff --git a/Widgets/NToggle.qml b/Widgets/NToggle.qml index 0a7cead..12c692e 100644 --- a/Widgets/NToggle.qml +++ b/Widgets/NToggle.qml @@ -2,8 +2,6 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import qs.Services -import qs.Theme - RowLayout { id: root diff --git a/Widgets/NTooltip.qml b/Widgets/NTooltip.qml index 231fbb6..089fef8 100644 --- a/Widgets/NTooltip.qml +++ b/Widgets/NTooltip.qml @@ -1,6 +1,5 @@ import QtQuick import qs.Services -import qs.Theme Window { id: root From c701192c08e5d949df386e72cb80e57ebadad67e Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sat, 9 Aug 2025 18:05:11 -0400 Subject: [PATCH 021/394] Reset scaling to 1.0 --- Modules/Bar/Clock.qml | 2 +- Services/Scaling.qml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/Bar/Clock.qml b/Modules/Bar/Clock.qml index aa7a361..fc15665 100644 --- a/Modules/Bar/Clock.qml +++ b/Modules/Bar/Clock.qml @@ -16,7 +16,7 @@ Rectangle { text: Time.time font.family: Theme.fontFamily font.weight: Font.Bold - font.pointSize: Style.fontSmall * scaling + font.pointSize: Style.fontMedium * scaling color: Theme.textPrimary anchors.centerIn: parent } diff --git a/Services/Scaling.qml b/Services/Scaling.qml index 02bf7b2..d71fef4 100644 --- a/Services/Scaling.qml +++ b/Services/Scaling.qml @@ -41,6 +41,6 @@ Singleton { } // 3) Safe default - return 2.0 + return 1.0 } } From 5d6b82a80ef3dd74316b39fa349caf92d66e71a3 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sat, 9 Aug 2025 18:23:33 -0400 Subject: [PATCH 022/394] Moved test widgets to a demo panel --- Modules/Bar/Bar.qml | 27 ++----------- Modules/Bar/Clock.qml | 2 +- Modules/DemoPanel/DemoPanel.qml | 72 +++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 24 deletions(-) create mode 100644 Modules/DemoPanel/DemoPanel.qml diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 34d07d7..5f8f1d9 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -33,32 +33,13 @@ PanelWindow { // Testing widgets RowLayout { - NToggle { - label: "Label" - description: "Description" - onToggled: function(value: bool) { - console.log("NToggle: " + value) - } - } - NIconButton { - id: myIconButton - icon: "refresh" - onEntered: function() { - myTooltip.show(); - } - onExited: function() { - myTooltip.hide(); + id: demoPanelToggler + icon: "experiment" + onClicked: function () { + demoPanel.visible ? demoPanel.hide() : demoPanel.show() } } - NTooltip { - id: myTooltip - target: myIconButton - positionAbove: false - text: "Hello world" - } - - NSlider {} Clock {} } diff --git a/Modules/Bar/Clock.qml b/Modules/Bar/Clock.qml index fc15665..4a94e21 100644 --- a/Modules/Bar/Clock.qml +++ b/Modules/Bar/Clock.qml @@ -37,7 +37,7 @@ Rectangle { onClicked: function () { calendar.visible = !calendar.visible if (calendar.visible) { - tooltip.hide(); + tooltip.hide() } } } diff --git a/Modules/DemoPanel/DemoPanel.qml b/Modules/DemoPanel/DemoPanel.qml new file mode 100644 index 0000000..72b98b2 --- /dev/null +++ b/Modules/DemoPanel/DemoPanel.qml @@ -0,0 +1,72 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import qs.Services +import qs.Widgets + +/* + An experiment/demo panel to tweaks widgets +*/ + + +NPanel { + id: root + + readonly property real scaling: Scaling.scale(screen) + + Rectangle { + color: Theme.backgroundPrimary + radius: Style.radiusMedium * scaling + border.color: Theme.backgroundTertiary + border.width: Math.max(1, 1.5 * scale) + width: 340 * scaling + height: 200 + anchors.top: parent.top + anchors.right: parent.right + anchors.topMargin: 4 * scaling + anchors.rightMargin: 4 * scaling + + // Prevent closing when clicking in the panel bg + MouseArea { + anchors.fill: parent + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 16 * scaling + spacing: 12 * scaling + + NToggle { + label: "Label" + description: "Description" + onToggled: function(value: bool) { + console.log("NToggle: " + value) + } + } + + NIconButton { + id: myIconButton + icon: "refresh" + onEntered: function() { + myTooltip.show(); + } + onExited: function() { + myTooltip.hide(); + } + } + + NTooltip { + id: myTooltip + target: myIconButton + positionAbove: false + text: "Hello world" + } + + NSlider {} + + + } + } +} From 05b76b96b3ac82a19202ba6cb588cc88e608ffa3 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sat, 9 Aug 2025 18:23:45 -0400 Subject: [PATCH 023/394] DemoPanel --- Widgets/NCalendar.qml | 18 +++++++++--------- Widgets/NPanel.qml | 4 ++-- Widgets/NTooltip.qml | 2 +- shell.qml | 5 +++++ 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/Widgets/NCalendar.qml b/Widgets/NCalendar.qml index 5d09a5c..cb097e6 100644 --- a/Widgets/NCalendar.qml +++ b/Widgets/NCalendar.qml @@ -8,15 +8,15 @@ import qs.Services import "../Helpers/Holidays.js" as Holidays NPanel { - id: calendarOverlay + id: root readonly property real scaling: Scaling.scale(screen) Rectangle { color: Theme.backgroundPrimary - radius: 12 + radius: Style.radiusMedium * scaling border.color: Theme.backgroundTertiary - border.width: 1 + border.width: Math.max(1, 1.5 * scale) width: 340 * scaling height: 380 anchors.top: parent.top @@ -31,8 +31,8 @@ NPanel { ColumnLayout { anchors.fill: parent - anchors.margins: 16 - spacing: 12 + anchors.margins: 16 * scaling + spacing: 12 * scaling // Month/Year header with navigation RowLayout { @@ -113,14 +113,14 @@ NPanel { // Optionally, update when the panel becomes visible Connections { function onVisibleChanged() { - if (calendarOverlay.visible) { + if (root.visible) { calendar.month = Time.date.getMonth() calendar.year = Time.date.getFullYear() calendar.updateHolidays() } } - target: calendarOverlay + target: root } delegate: Rectangle { @@ -179,8 +179,8 @@ NPanel { return h.localName + (h.name !== h.localName ? " (" + h.name + ")" : "") + (h.global ? " [Global]" : "") }).join(", ") - holidayTooltip.target = parent; - holidayTooltip.show(); + holidayTooltip.target = parent + holidayTooltip.show() } } onExited: holidayTooltip.hide() diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 4b614a7..7ced550 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -11,7 +11,7 @@ PanelWindow { property int topMargin: Style.barHeight * scaling property color overlayColor: showOverlay ? Theme.overlay : "transparent" - function dismiss() { + function hide() { visible = false } @@ -33,7 +33,7 @@ PanelWindow { MouseArea { anchors.fill: parent - onClicked: outerPanel.dismiss() + onClicked: outerPanel.hide() } Behavior on color { diff --git a/Widgets/NTooltip.qml b/Widgets/NTooltip.qml index 089fef8..914298f 100644 --- a/Widgets/NTooltip.qml +++ b/Widgets/NTooltip.qml @@ -112,7 +112,7 @@ Window { text: root.text color: Theme.textPrimary font.family: Theme.fontFamily - font.pointSize: Style.fontSmall * scaling + font.pointSize: Style.fontMedium * scaling horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter wrapMode: Text.Wrap diff --git a/shell.qml b/shell.qml index 3ba8775..8f3c314 100644 --- a/shell.qml +++ b/shell.qml @@ -10,6 +10,7 @@ import Quickshell import Quickshell.Io import Quickshell.Widgets import qs.Modules.Bar +import qs.Modules.DemoPanel ShellRoot { id: root @@ -21,4 +22,8 @@ ShellRoot { modelData: item } } + + DemoPanel { + id: demoPanel + } } From 285321dcb963dccd1bbc08787dcb37ccc908a382 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sat, 9 Aug 2025 18:36:48 -0400 Subject: [PATCH 024/394] DemoPanel improved layout --- Modules/DemoPanel/DemoPanel.qml | 79 ++++++++++++++++++++++----------- 1 file changed, 53 insertions(+), 26 deletions(-) diff --git a/Modules/DemoPanel/DemoPanel.qml b/Modules/DemoPanel/DemoPanel.qml index 72b98b2..83a480e 100644 --- a/Modules/DemoPanel/DemoPanel.qml +++ b/Modules/DemoPanel/DemoPanel.qml @@ -21,12 +21,10 @@ NPanel { radius: Style.radiusMedium * scaling border.color: Theme.backgroundTertiary border.width: Math.max(1, 1.5 * scale) - width: 340 * scaling - height: 200 - anchors.top: parent.top - anchors.right: parent.right - anchors.topMargin: 4 * scaling - anchors.rightMargin: 4 * scaling + width: 500 * scaling + height: 300 + anchors.centerIn: parent + // Prevent closing when clicking in the panel bg MouseArea { @@ -38,34 +36,63 @@ NPanel { anchors.margins: 16 * scaling spacing: 12 * scaling - NToggle { - label: "Label" - description: "Description" - onToggled: function(value: bool) { - console.log("NToggle: " + value) + + // NIconButton + RowLayout { + spacing: 16 * scaling + Text { + text: "NIconButton" + color: Theme.textPrimary + } + + NIconButton { + id: myIconButton + icon: "refresh" + onEntered: function() { + myTooltip.show(); + } + onExited: function() { + myTooltip.hide(); + } } } - NIconButton { - id: myIconButton - icon: "refresh" - onEntered: function() { - myTooltip.show(); + + // NToggle + RowLayout { + spacing: 16 * scaling + uniformCellSizes: true + Text { + text: "NToggle + NTooltip" + color: Theme.textPrimary } - onExited: function() { - myTooltip.hide(); + + NToggle { + label: "Label" + description: "Description" + onToggled: function(value: bool) { + console.log("NToggle: " + value) + } + } + + NTooltip { + id: myTooltip + target: myIconButton + positionAbove: false + text: "Hello world" } } - NTooltip { - id: myTooltip - target: myIconButton - positionAbove: false - text: "Hello world" - } + // NSlider + RowLayout { + spacing: 16 * scaling + Text { + text: "NSlider" + color: Theme.textPrimary + } - NSlider {} - + NSlider {} + } } } From 55bc7c953414605b63fec575a1ab9e65f6249a84 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sat, 9 Aug 2025 18:40:55 -0400 Subject: [PATCH 025/394] Settings vs Colors Moved the fontFamily in the settings Theme is now called Colors (as in ColorScheme) --- Modules/Bar/Bar.qml | 2 +- Modules/Bar/Clock.qml | 4 ++-- Modules/DemoPanel/DemoPanel.qml | 10 +++++----- Services/{Theme.qml => Colors.qml} | 5 +---- Services/Settings.qml | 8 +++++--- Widgets/NCalendar.qml | 22 +++++++++++----------- Widgets/NIconButton.qml | 4 ++-- Widgets/NPanel.qml | 2 +- Widgets/NSlider.qml | 14 +++++++------- Widgets/NToggle.qml | 12 ++++++------ Widgets/NTooltip.qml | 8 ++++---- 11 files changed, 45 insertions(+), 46 deletions(-) rename Services/{Theme.qml => Colors.qml} (96%) diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 5f8f1d9..41731f8 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -26,7 +26,7 @@ PanelWindow { Rectangle { anchors.fill: parent - color: Theme.backgroundPrimary + color: Colors.backgroundPrimary layer.enabled: true } diff --git a/Modules/Bar/Clock.qml b/Modules/Bar/Clock.qml index 4a94e21..da71018 100644 --- a/Modules/Bar/Clock.qml +++ b/Modules/Bar/Clock.qml @@ -14,10 +14,10 @@ Rectangle { Text { id: textItem text: Time.time - font.family: Theme.fontFamily + font.family: Settings.settings.fontFamily font.weight: Font.Bold font.pointSize: Style.fontMedium * scaling - color: Theme.textPrimary + color: Colors.textPrimary anchors.centerIn: parent } diff --git a/Modules/DemoPanel/DemoPanel.qml b/Modules/DemoPanel/DemoPanel.qml index 83a480e..9ecb833 100644 --- a/Modules/DemoPanel/DemoPanel.qml +++ b/Modules/DemoPanel/DemoPanel.qml @@ -17,9 +17,9 @@ NPanel { readonly property real scaling: Scaling.scale(screen) Rectangle { - color: Theme.backgroundPrimary + color: Colors.backgroundPrimary radius: Style.radiusMedium * scaling - border.color: Theme.backgroundTertiary + border.color: Colors.backgroundTertiary border.width: Math.max(1, 1.5 * scale) width: 500 * scaling height: 300 @@ -42,7 +42,7 @@ NPanel { spacing: 16 * scaling Text { text: "NIconButton" - color: Theme.textPrimary + color: Colors.textPrimary } NIconButton { @@ -64,7 +64,7 @@ NPanel { uniformCellSizes: true Text { text: "NToggle + NTooltip" - color: Theme.textPrimary + color: Colors.textPrimary } NToggle { @@ -88,7 +88,7 @@ NPanel { spacing: 16 * scaling Text { text: "NSlider" - color: Theme.textPrimary + color: Colors.textPrimary } NSlider {} diff --git a/Services/Theme.qml b/Services/Colors.qml similarity index 96% rename from Services/Theme.qml rename to Services/Colors.qml index bf1829f..d8ad5e6 100644 --- a/Services/Theme.qml +++ b/Services/Colors.qml @@ -43,9 +43,6 @@ Singleton { property color shadow: applyOpacity(themeData.shadow, "B3") property color overlay: applyOpacity(themeData.overlay, "66") - // Font Properties - property string fontFamily: "Roboto" // Family for all text - function applyOpacity(color, opacity) { return color.replace("#", "#" + opacity) } @@ -53,7 +50,7 @@ Singleton { // FileView to load theme data from JSON file FileView { id: themeFile - path: Settings.themeFile + path: Settings.colorsFile watchChanges: true onFileChanged: reload() onAdapterUpdated: writeAdapter() diff --git a/Services/Settings.qml b/Services/Settings.qml index 6ec70e0..e4935d1 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -14,8 +14,8 @@ Singleton { "HOME") + "/.config") + "/" + shellName + "/" property string settingsFile: Quickshell.env("NOCTALIA_SETTINGS_FILE") || (settingsDir + "Settings.json") - property string themeFile: Quickshell.env("NOCTALIA_THEME_FILE") - || (settingsDir + "Theme.json") + property string colorsFile: Quickshell.env("NOCTALIA_COLORS_FILE") + || (settingsDir + "Colors.json") property var settings: settingAdapter Item { @@ -91,7 +91,9 @@ Singleton { property var notificationMonitors: [] // Array of monitor names to show notifications on, "*" means all monitors property var monitorScaleOverrides: { - } // Map of monitor name -> scale override (e.g., 0.8..2.0). When set, Theme.scale() returns this value + } // Map of monitor name -> scale override (e.g., 0.8..2.0). When set, Colors.scale() returns this value + + property string fontFamily: "Roboto" // Family for all text } } diff --git a/Widgets/NCalendar.qml b/Widgets/NCalendar.qml index cb097e6..1c790d2 100644 --- a/Widgets/NCalendar.qml +++ b/Widgets/NCalendar.qml @@ -13,9 +13,9 @@ NPanel { readonly property real scaling: Scaling.scale(screen) Rectangle { - color: Theme.backgroundPrimary + color: Colors.backgroundPrimary radius: Style.radiusMedium * scaling - border.color: Theme.backgroundTertiary + border.color: Colors.backgroundTertiary border.width: Math.max(1, 1.5 * scale) width: 340 * scaling height: 380 @@ -52,10 +52,10 @@ NPanel { Layout.fillWidth: true horizontalAlignment: Text.AlignHCenter text: calendar.title - color: Theme.textPrimary + color: Colors.textPrimary opacity: 0.7 font.pointSize: Style.fontSmall * scaling - font.family: Theme.fontFamily + font.family: Settings.settings.fontFamily font.bold: true } @@ -77,10 +77,10 @@ NPanel { delegate: Text { text: shortName - color: Theme.textPrimary + color: Colors.textPrimary opacity: 0.8 font.pointSize: Style.fontSmall * scaling - font.family: Theme.fontFamily + font.family: Settings.settings.fontFamily font.bold: true horizontalAlignment: Text.AlignHCenter width: 32 @@ -136,10 +136,10 @@ NPanel { radius: 8 color: { if (model.today) - return Theme.accentPrimary + return Colors.accentPrimary if (mouseArea2.containsMouse) - return Theme.backgroundTertiary + return Colors.backgroundTertiary return "transparent" } @@ -150,7 +150,7 @@ NPanel { width: 4 height: 4 radius: 4 - color: Theme.accentTertiary + color: Colors.accentTertiary anchors.top: parent.top anchors.right: parent.right anchors.topMargin: 4 @@ -161,10 +161,10 @@ NPanel { Text { anchors.centerIn: parent text: model.day - color: model.today ? Theme.onAccent : Theme.textPrimary + color: model.today ? Colors.onAccent : Colors.textPrimary opacity: model.month === calendar.month ? (mouseArea2.containsMouse ? 1 : 0.7) : 0.3 font.pointSize: Style.fontSmall * scaling - font.family: Theme.fontFamily + font.family: Settings.settings.fontFamily font.bold: model.today ? true : false } diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml index 041ecaf..e6d4d76 100644 --- a/Widgets/NIconButton.qml +++ b/Widgets/NIconButton.qml @@ -19,7 +19,7 @@ Rectangle { implicitHeight: size radius: width * 0.5 - color: root.hovering ? Theme.accentPrimary : "transparent" + color: root.hovering ? Colors.accentPrimary : "transparent" Text { id: iconText @@ -27,7 +27,7 @@ Rectangle { text: root.icon font.family: "Material Symbols Outlined" font.pointSize: Style.fontExtraLarge * scaling - color: root.hovering ? Theme.onAccent : Theme.textPrimary + color: root.hovering ? Colors.onAccent : Colors.textPrimary horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter opacity: root.enabled ? 1.0 : 0.5 diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 7ced550..ac539ee 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -9,7 +9,7 @@ PanelWindow { readonly property real scaling: Scaling.scale(screen) property bool showOverlay: Settings.settings.dimPanels property int topMargin: Style.barHeight * scaling - property color overlayColor: showOverlay ? Theme.overlay : "transparent" + property color overlayColor: showOverlay ? Colors.overlay : "transparent" function hide() { visible = false diff --git a/Widgets/NSlider.qml b/Widgets/NSlider.qml index 80932ac..bde21b4 100644 --- a/Widgets/NSlider.qml +++ b/Widgets/NSlider.qml @@ -27,13 +27,13 @@ Slider { width: root.availableWidth height: implicitHeight radius: height / 2 - color: Theme.surfaceVariant + color: Colors.surfaceVariant Rectangle { id: activeTrack width: root.visualPosition * parent.width height: parent.height - color: Theme.accentPrimary + color: Colors.accentPrimary radius: parent.radius // Feels more responsive without animation @@ -51,7 +51,7 @@ Slider { width: knobDiameter + cutoutExtra height: knobDiameter + cutoutExtra radius: width / 2 - color: root.cutoutColor !== undefined ? root.cutoutColor : Theme.backgroundPrimary + color: root.cutoutColor !== undefined ? root.cutoutColor : Colors.backgroundPrimary x: Math.max( 0, Math.min( parent.width - width, @@ -71,7 +71,7 @@ Slider { anchors.fill: knob source: knob shadowEnabled: true - shadowColor: Theme.shadow + shadowColor: Colors.shadow shadowOpacity: 0.25 shadowHorizontalOffset: 0 shadowVerticalOffset: 1 @@ -83,8 +83,8 @@ Slider { implicitWidth: knobDiameter implicitHeight: knobDiameter radius: width * 0.5 - color: root.pressed ? Theme.surfaceVariant : Theme.surface - border.color: Theme.accentPrimary + color: root.pressed ? Colors.surfaceVariant : Colors.surface + border.color: Colors.accentPrimary border.width: Math.max(1, 2 * scaling) // Press feedback halo (using accent color, low opacity) Rectangle { @@ -92,7 +92,7 @@ Slider { width: parent.width + 8 * scaling height: parent.height + 8 * scaling radius: width / 2 - color: Theme.accentPrimary + color: Colors.accentPrimary opacity: root.pressed ? 0.16 : 0.0 } } diff --git a/Widgets/NToggle.qml b/Widgets/NToggle.qml index 12c692e..90c8a65 100644 --- a/Widgets/NToggle.qml +++ b/Widgets/NToggle.qml @@ -23,13 +23,13 @@ RowLayout { text: label font.pointSize: Style.fontMedium * scaling font.bold: true - color: Theme.textPrimary + color: Colors.textPrimary } Text { text: description font.pointSize: Style.fontSmall * scaling - color: Theme.textSecondary + color: Colors.textSecondary wrapMode: Text.WordWrap Layout.fillWidth: true } @@ -41,16 +41,16 @@ RowLayout { width: Style.baseWidgetHeight * 1.625 * scaling height: Style.baseWidgetHeight * scaling radius: height * 0.5 - color: value ? Theme.accentPrimary :Theme.surfaceVariant - border.color: value ? Theme.accentPrimary : Theme.outline + color: value ? Colors.accentPrimary :Colors.surfaceVariant + border.color: value ? Colors.accentPrimary : Colors.outline border.width: Math.max(1, 1.5 * scale) Rectangle { width: (Style.baseWidgetHeight- 4) * scaling height: (Style.baseWidgetHeight - 4) * scaling radius: height * 0.5 - color: Theme.surface - border.color: hovering ? Theme.textDisabled : Theme.outline + color: Colors.surface + border.color: hovering ? Colors.textDisabled : Colors.outline border.width: Math.max(1, 1.5 * scale) y: 2 * scaling x: value ? switcher.width - width - 2 * scale : 2 * scaling diff --git a/Widgets/NTooltip.qml b/Widgets/NTooltip.qml index 914298f..fbb7b4b 100644 --- a/Widgets/NTooltip.qml +++ b/Widgets/NTooltip.qml @@ -99,8 +99,8 @@ Window { Rectangle { anchors.fill: parent radius: Style.radiusMedium * scaling - color: Theme.backgroundTertiary - border.color: Theme.outline + color: Colors.backgroundTertiary + border.color: Colors.outline border.width: 1 * scaling opacity: 0.97 z: 1 @@ -110,8 +110,8 @@ Window { id: tooltipText anchors.centerIn: parent text: root.text - color: Theme.textPrimary - font.family: Theme.fontFamily + color: Colors.textPrimary + font.family: Settings.settings.fontFamily font.pointSize: Style.fontMedium * scaling horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter From 71cb79b08bc056151a84c0342bf90d5cb0552f80 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sat, 9 Aug 2025 22:45:06 -0400 Subject: [PATCH 026/394] More theming --- Modules/Bar/Bar.qml | 42 ++++++++++++++++++-- Modules/Bar/Clock.qml | 10 +---- Modules/DemoPanel/DemoPanel.qml | 17 ++++---- Services/Settings.qml | 2 +- Services/Style.qml | 29 ++++++++++---- Widgets/NCalendar.qml | 69 +++++++++++++++------------------ Widgets/NIconButton.qml | 6 +-- Widgets/NSlider.qml | 6 +-- Widgets/NText.qml | 14 +++++++ Widgets/NToggle.qml | 18 ++++----- Widgets/NTooltip.qml | 15 ++++--- 11 files changed, 140 insertions(+), 88 deletions(-) create mode 100644 Widgets/NText.qml diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 41731f8..9ce9a32 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -14,6 +14,8 @@ PanelWindow { screen: modelData implicitHeight: Style.barHeight * scaling color: "transparent" + visible: Settings.settings.barMonitors.includes(modelData.name) + || (Settings.settings.barMonitors.length === 0) anchors { top: true @@ -25,13 +27,47 @@ PanelWindow { anchors.fill: parent Rectangle { + id: bar anchors.fill: parent color: Colors.backgroundPrimary layer.enabled: true } - // Testing widgets - RowLayout { + Row { + id: leftSection + anchors.left: bar.left + anchors.leftMargin: Style.marginMedium * scaling + anchors.verticalCenter: bar.verticalCenter + spacing: Style.marginMedium * scaling + + NText { + text: "Left" + } + } + + Row { + id: centerSection + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: bar.verticalCenter + spacing: Style.marginMedium * scaling + NText { + text: "Center" + } + } + + Row { + id: rightSection + + anchors.right: bar.right + anchors.rightMargin: Style.marginMedium * scaling + anchors.verticalCenter: bar.verticalCenter + spacing: Style.marginMedium * scaling + + NText { + text: "Right" + } + + Clock {} NIconButton { id: demoPanelToggler @@ -40,8 +76,6 @@ PanelWindow { demoPanel.visible ? demoPanel.hide() : demoPanel.show() } } - - Clock {} } } } diff --git a/Modules/Bar/Clock.qml b/Modules/Bar/Clock.qml index da71018..d883e27 100644 --- a/Modules/Bar/Clock.qml +++ b/Modules/Bar/Clock.qml @@ -11,13 +11,9 @@ Rectangle { height: textItem.paintedHeight color: "transparent" - Text { + NText { id: textItem text: Time.time - font.family: Settings.settings.fontFamily - font.weight: Font.Bold - font.pointSize: Style.fontMedium * scaling - color: Colors.textPrimary anchors.centerIn: parent } @@ -36,9 +32,7 @@ Rectangle { } onClicked: function () { calendar.visible = !calendar.visible - if (calendar.visible) { - tooltip.hide() - } + tooltip.hide() } } diff --git a/Modules/DemoPanel/DemoPanel.qml b/Modules/DemoPanel/DemoPanel.qml index 9ecb833..e1f336e 100644 --- a/Modules/DemoPanel/DemoPanel.qml +++ b/Modules/DemoPanel/DemoPanel.qml @@ -20,7 +20,7 @@ NPanel { color: Colors.backgroundPrimary radius: Style.radiusMedium * scaling border.color: Colors.backgroundTertiary - border.width: Math.max(1, 1.5 * scale) + border.width: Math.min(1, Style.borderMedium * scaling) width: 500 * scaling height: 300 anchors.centerIn: parent @@ -38,11 +38,10 @@ NPanel { // NIconButton - RowLayout { + ColumnLayout { spacing: 16 * scaling - Text { + NText { text: "NIconButton" - color: Colors.textPrimary } NIconButton { @@ -59,12 +58,11 @@ NPanel { // NToggle - RowLayout { + ColumnLayout { spacing: 16 * scaling uniformCellSizes: true - Text { + NText { text: "NToggle + NTooltip" - color: Colors.textPrimary } NToggle { @@ -84,11 +82,10 @@ NPanel { } // NSlider - RowLayout { + ColumnLayout { spacing: 16 * scaling - Text { + NText { text: "NSlider" - color: Colors.textPrimary } NSlider {} diff --git a/Services/Settings.qml b/Services/Settings.qml index e4935d1..37b7a21 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -15,7 +15,7 @@ Singleton { property string settingsFile: Quickshell.env("NOCTALIA_SETTINGS_FILE") || (settingsDir + "Settings.json") property string colorsFile: Quickshell.env("NOCTALIA_COLORS_FILE") - || (settingsDir + "Colors.json") + || (settingsDir + "Colors.json") property var settings: settingAdapter Item { diff --git a/Services/Style.qml b/Services/Style.qml index 3a268b3..407af8a 100644 --- a/Services/Style.qml +++ b/Services/Style.qml @@ -12,13 +12,13 @@ Singleton { Preset sizes for font, radii, ? */ - // Font - property real fontExtraLarge: 20 - property real fontLarge: 14 - property real fontMedium: 10 - property real fontSmall: 8 + // Font size + property real fontSizeSmall: 9 + property real fontSizeMedium: 11 + property real fontSizeLarge: 13 + property real fontSizeXL: 18 - // Font weight + // Font weight / Unsure if we keep em? property int fontWeightRegular: 400 property int fontWeightMedium: 500 property int fontWeightBold: 700 @@ -45,8 +45,23 @@ Singleton { property int animationSlow: 500 property int barHeight: 36 - property int baseWidgetHeight: 32 + property int baseWidgetSize: 32 property int sliderWidth: 200 + // Delay property int tooltipDelay: 300 + + // Margins and spacing + property int marginTiny: 4 + property int marginSmall: 8 + property int marginMedium: 12 + property int marginLarge: 18 + property int marginXL: 24 + + // Opacity + property real opacityLight: 0.25 + property real opacityMedium: 0.5 + property real opacityHeavy: 0.75 + property real opacityAlmost: 0.95 + property real opacityFull: 1.0 } diff --git a/Widgets/NCalendar.qml b/Widgets/NCalendar.qml index 1c790d2..59c1519 100644 --- a/Widgets/NCalendar.qml +++ b/Widgets/NCalendar.qml @@ -13,16 +13,16 @@ NPanel { readonly property real scaling: Scaling.scale(screen) Rectangle { - color: Colors.backgroundPrimary + color: Colors.backgroundSecondary radius: Style.radiusMedium * scaling border.color: Colors.backgroundTertiary - border.width: Math.max(1, 1.5 * scale) + border.width: Math.min(1, Style.borderMedium * scaling) width: 340 * scaling - height: 380 + height: 300 anchors.top: parent.top anchors.right: parent.right - anchors.topMargin: 4 * scaling - anchors.rightMargin: 4 * scaling + anchors.topMargin: Style.marginTiny * scaling + anchors.rightMargin: Style.marginTiny * scaling // Prevent closing when clicking in the panel bg MouseArea { @@ -31,13 +31,13 @@ NPanel { ColumnLayout { anchors.fill: parent - anchors.margins: 16 * scaling - spacing: 12 * scaling + anchors.margins: Style.marginMedium * scaling + spacing: Style.marginMedium * scaling // Month/Year header with navigation RowLayout { Layout.fillWidth: true - spacing: 8 + spacing: Style.marginSmall * scaling NIconButton { icon: "chevron_left" @@ -48,15 +48,12 @@ NPanel { } } - Text { + NText { + text: calendar.title Layout.fillWidth: true horizontalAlignment: Text.AlignHCenter - text: calendar.title - color: Colors.textPrimary - opacity: 0.7 - font.pointSize: Style.fontSmall * scaling - font.family: Settings.settings.fontFamily - font.bold: true + font.pointSize: Style.fontSizeMedium * scaling + color: Colors.accentPrimary } NIconButton { @@ -72,18 +69,15 @@ NPanel { DayOfWeekRow { Layout.fillWidth: true spacing: 0 - Layout.leftMargin: 8 // Align with grid - Layout.rightMargin: 8 + Layout.leftMargin: Style.marginSmall * scaling // Align with grid + Layout.rightMargin: Style.marginSmall * scaling - delegate: Text { + delegate: NText { text: shortName - color: Colors.textPrimary - opacity: 0.8 - font.pointSize: Style.fontSmall * scaling - font.family: Settings.settings.fontFamily - font.bold: true + color: Colors.accentSecondary + font.pointSize: Style.fontSizeMedium * scaling horizontalAlignment: Text.AlignHCenter - width: 32 + width: Style.baseWidgetSize * scaling } } @@ -101,8 +95,8 @@ NPanel { } Layout.fillWidth: true - Layout.leftMargin: 8 - Layout.rightMargin: 8 + Layout.leftMargin: Style.marginSmall * scaling + Layout.rightMargin: Style.marginSmall * scaling spacing: 0 month: Time.date.getMonth() year: Time.date.getFullYear() @@ -131,9 +125,9 @@ NPanel { }) property bool isHoliday: holidayInfo.length > 0 - width: 32 - height: 32 - radius: 8 + width: Style.baseWidgetSize * scaling + height: Style.baseWidgetSize * scaling + radius: Style.radiusSmall * scaling color: { if (model.today) return Colors.accentPrimary @@ -147,24 +141,23 @@ NPanel { // Holiday dot indicator Rectangle { visible: isHoliday - width: 4 - height: 4 - radius: 4 + width: Style.baseWidgetSize / 8 * scaling + height: Style.baseWidgetSize / 8 * scaling + radius: Style.radiusSmall * scaling color: Colors.accentTertiary anchors.top: parent.top anchors.right: parent.right - anchors.topMargin: 4 - anchors.rightMargin: 4 + anchors.topMargin: Style.marginTiny * scaling + anchors.rightMargin: Style.marginTiny * scaling z: 2 } - Text { + NText { anchors.centerIn: parent text: model.day color: model.today ? Colors.onAccent : Colors.textPrimary - opacity: model.month === calendar.month ? (mouseArea2.containsMouse ? 1 : 0.7) : 0.3 - font.pointSize: Style.fontSmall * scaling - font.family: Settings.settings.fontFamily + opacity: model.month === calendar.month ? (mouseArea2.containsMouse ? Style.opacityFull : Style.opacityHeavy) : Style.opacityLight + font.pointSize: Style.fontSizeMedium * scaling font.bold: model.today ? true : false } diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml index e6d4d76..a5bc375 100644 --- a/Widgets/NIconButton.qml +++ b/Widgets/NIconButton.qml @@ -7,7 +7,7 @@ Rectangle { id: root readonly property real scaling: Scaling.scale(screen) - property real size: Style.baseWidgetHeight * scaling + property real size: Style.baseWidgetSize * scaling property string icon property bool enabled: true property bool hovering: false @@ -26,11 +26,11 @@ Rectangle { anchors.centerIn: parent text: root.icon font.family: "Material Symbols Outlined" - font.pointSize: Style.fontExtraLarge * scaling + font.pointSize: Style.fontSizeXL * scaling color: root.hovering ? Colors.onAccent : Colors.textPrimary horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter - opacity: root.enabled ? 1.0 : 0.5 + opacity: root.enabled ? Style.opacityFull : Style.opacityMedium } MouseArea { diff --git a/Widgets/NSlider.qml b/Widgets/NSlider.qml index bde21b4..e6fe997 100644 --- a/Widgets/NSlider.qml +++ b/Widgets/NSlider.qml @@ -7,9 +7,9 @@ Slider { id: root readonly property real scaling: Scaling.scale(screen) - readonly property real knobDiameter: Style.baseWidgetHeight * 0.75 * scaling + readonly property real knobDiameter: Style.baseWidgetSize * 0.75 * scaling readonly property real trackHeight: knobDiameter * 0.5 - readonly property real cutoutExtra: Style.baseWidgetHeight * 0.1 * scaling + readonly property real cutoutExtra: Style.baseWidgetSize * 0.1 * scaling // Optional color to cut the track beneath the knob (should match surrounding background) property var cutoutColor @@ -85,7 +85,7 @@ Slider { radius: width * 0.5 color: root.pressed ? Colors.surfaceVariant : Colors.surface border.color: Colors.accentPrimary - border.width: Math.max(1, 2 * scaling) + border.width: Math.min(1, Style.borderThick * scaling) // Press feedback halo (using accent color, low opacity) Rectangle { anchors.centerIn: parent diff --git a/Widgets/NText.qml b/Widgets/NText.qml new file mode 100644 index 0000000..a26cff1 --- /dev/null +++ b/Widgets/NText.qml @@ -0,0 +1,14 @@ +import QtQuick +import qs.Services +import qs.Widgets + +Text { + id: root + + readonly property real scaling: Scaling.scale(screen) + + font.family: Settings.settings.fontFamily + font.pointSize: Style.fontSizeMedium * scaling + font.weight: Font.Bold + color: Colors.textPrimary +} diff --git a/Widgets/NToggle.qml b/Widgets/NToggle.qml index 90c8a65..c4cfc95 100644 --- a/Widgets/NToggle.qml +++ b/Widgets/NToggle.qml @@ -21,14 +21,14 @@ RowLayout { Text { text: label - font.pointSize: Style.fontMedium * scaling + font.pointSize: Style.fontSizeMedium * scaling font.bold: true color: Colors.textPrimary } Text { text: description - font.pointSize: Style.fontSmall * scaling + font.pointSize: Style.fontSizeSmall * scaling color: Colors.textSecondary wrapMode: Text.WordWrap Layout.fillWidth: true @@ -38,22 +38,22 @@ RowLayout { Rectangle { id: switcher - width: Style.baseWidgetHeight * 1.625 * scaling - height: Style.baseWidgetHeight * scaling + width: Style.baseWidgetSize * 1.625 * scaling + height: Style.baseWidgetSize * scaling radius: height * 0.5 color: value ? Colors.accentPrimary :Colors.surfaceVariant border.color: value ? Colors.accentPrimary : Colors.outline - border.width: Math.max(1, 1.5 * scale) + border.width: Math.min(1, Style.borderMedium * scaling) Rectangle { - width: (Style.baseWidgetHeight- 4) * scaling - height: (Style.baseWidgetHeight - 4) * scaling + width: (Style.baseWidgetSize - 4) * scaling + height: (Style.baseWidgetSize - 4) * scaling radius: height * 0.5 color: Colors.surface border.color: hovering ? Colors.textDisabled : Colors.outline - border.width: Math.max(1, 1.5 * scale) + border.width: Math.min(1, Style.borderMedium * scaling) y: 2 * scaling - x: value ? switcher.width - width - 2 * scale : 2 * scaling + x: value ? switcher.width - width - 2 * scaling : 2 * scaling Behavior on x { NumberAnimation { diff --git a/Widgets/NTooltip.qml b/Widgets/NTooltip.qml index fbb7b4b..38a1393 100644 --- a/Widgets/NTooltip.qml +++ b/Widgets/NTooltip.qml @@ -32,12 +32,17 @@ Window { } function hide() { isVisible = false + timerShow.running = false } function _showNow() { // Compute new size everytime we show the tooltip - width = Math.max(50 * scaling, tooltipText.implicitWidth + 24 * scaling) - height = Math.max(50 * scaling, tooltipText.implicitHeight + 16 * scaling) + width = Math.max( + 50 * scaling, + tooltipText.implicitWidth + Style.marginLarge * 2 * scaling) + height = Math.max( + 50 * scaling, + tooltipText.implicitHeight + Style.marginSmall * 2 * scaling) if (!target) { return @@ -101,8 +106,8 @@ Window { radius: Style.radiusMedium * scaling color: Colors.backgroundTertiary border.color: Colors.outline - border.width: 1 * scaling - opacity: 0.97 + border.width: Math.min(1, Style.borderThin * scaling) + opacity: Style.opacityFull z: 1 } @@ -112,7 +117,7 @@ Window { text: root.text color: Colors.textPrimary font.family: Settings.settings.fontFamily - font.pointSize: Style.fontMedium * scaling + font.pointSize: Style.fontSizeMedium * scaling horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter wrapMode: Text.Wrap From ec3bff68acd874bb82a3c0a13e1b44c4ae4d71e9 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sat, 9 Aug 2025 23:42:02 -0400 Subject: [PATCH 027/394] Bring backs most services --- Modules/Background/Background.qml | 0 Modules/Background/Overview.qml | 0 Modules/Background/WallpaperPicker.qml | 0 Modules/Bar/Bar.qml | 14 +- Modules/DemoPanel/DemoPanel.qml | 21 +- Services/Location.qml | 2 + Services/MediaPlayer.qml | 169 ++++++++++++ Services/Network.qml | 345 +++++++++++++++++++++++++ Services/Niri.qml | 140 ++++++++++ Services/Style.qml | 4 +- Services/SysInfo.qml | 47 ++++ Services/Wallpapers.qml | 136 ++++++++++ Services/Workspaces.qml | 156 +++++++++++ Widgets/NCalendar.qml | 6 +- Widgets/NDivider.qml | 10 + Widgets/NIconButton.qml | 1 - 16 files changed, 1038 insertions(+), 13 deletions(-) create mode 100644 Modules/Background/Background.qml create mode 100644 Modules/Background/Overview.qml create mode 100644 Modules/Background/WallpaperPicker.qml create mode 100644 Services/Location.qml create mode 100644 Services/MediaPlayer.qml create mode 100644 Services/Network.qml create mode 100644 Services/Niri.qml create mode 100644 Services/SysInfo.qml create mode 100644 Services/Wallpapers.qml create mode 100644 Services/Workspaces.qml create mode 100644 Widgets/NDivider.qml diff --git a/Modules/Background/Background.qml b/Modules/Background/Background.qml new file mode 100644 index 0000000..e69de29 diff --git a/Modules/Background/Overview.qml b/Modules/Background/Overview.qml new file mode 100644 index 0000000..e69de29 diff --git a/Modules/Background/WallpaperPicker.qml b/Modules/Background/WallpaperPicker.qml new file mode 100644 index 0000000..e69de29 diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 9ce9a32..e4c7f3b 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -25,7 +25,9 @@ PanelWindow { Item { anchors.fill: parent + clip: true + // Background fill Rectangle { id: bar anchors.fill: parent @@ -35,9 +37,10 @@ PanelWindow { Row { id: leftSection - anchors.left: bar.left + height: parent.height + anchors.left: parent.left anchors.leftMargin: Style.marginMedium * scaling - anchors.verticalCenter: bar.verticalCenter + anchors.verticalCenter: parent.verticalCenter spacing: Style.marginMedium * scaling NText { @@ -47,9 +50,11 @@ PanelWindow { Row { id: centerSection + height: parent.height anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: bar.verticalCenter + anchors.verticalCenter: parent.verticalCenter spacing: Style.marginMedium * scaling + NText { text: "Center" } @@ -57,7 +62,7 @@ PanelWindow { Row { id: rightSection - + height: parent.height anchors.right: bar.right anchors.rightMargin: Style.marginMedium * scaling anchors.verticalCenter: bar.verticalCenter @@ -65,6 +70,7 @@ PanelWindow { NText { text: "Right" + Layout.alignment: Qt.AlignVCenter } Clock {} diff --git a/Modules/DemoPanel/DemoPanel.qml b/Modules/DemoPanel/DemoPanel.qml index e1f336e..89fec51 100644 --- a/Modules/DemoPanel/DemoPanel.qml +++ b/Modules/DemoPanel/DemoPanel.qml @@ -22,7 +22,7 @@ NPanel { border.color: Colors.backgroundTertiary border.width: Math.min(1, Style.borderMedium * scaling) width: 500 * scaling - height: 300 + height: 400 anchors.centerIn: parent @@ -33,15 +33,15 @@ NPanel { ColumnLayout { anchors.fill: parent - anchors.margins: 16 * scaling - spacing: 12 * scaling - + anchors.margins: Style.marginXL * scaling + spacing: Style.marginSmall * scaling // NIconButton ColumnLayout { spacing: 16 * scaling NText { text: "NIconButton" + color: Colors.accentSecondary } NIconButton { @@ -54,15 +54,18 @@ NPanel { myTooltip.hide(); } } + + NDivider {Layout.fillWidth: true} } // NToggle ColumnLayout { - spacing: 16 * scaling + spacing: Style.marginLarge * scaling uniformCellSizes: true NText { text: "NToggle + NTooltip" + color: Colors.accentSecondary } NToggle { @@ -79,16 +82,24 @@ NPanel { positionAbove: false text: "Hello world" } + NDivider { + Layout.fillWidth: true + } } // NSlider ColumnLayout { spacing: 16 * scaling + NText { text: "NSlider" + color: Colors.accentSecondary } NSlider {} + NDivider { + Layout.fillWidth: true + } } } diff --git a/Services/Location.qml b/Services/Location.qml new file mode 100644 index 0000000..9458435 --- /dev/null +++ b/Services/Location.qml @@ -0,0 +1,2 @@ +// Weather logic and caching +// Calendar Hollidays logic and caching \ No newline at end of file diff --git a/Services/MediaPlayer.qml b/Services/MediaPlayer.qml new file mode 100644 index 0000000..b9d17d0 --- /dev/null +++ b/Services/MediaPlayer.qml @@ -0,0 +1,169 @@ +// pragma Singleton + +// import QtQuick +// import Quickshell +// import Quickshell.Services.Mpris +// import qs.Services + +// Singleton { +// id: manager + +// property var currentPlayer: null +// property real currentPosition: 0 +// property int selectedPlayerIndex: 0 +// property bool isPlaying: currentPlayer ? currentPlayer.isPlaying : false +// property string trackTitle: currentPlayer ? (currentPlayer.trackTitle +// || "Unknown Track") : "" +// property string trackArtist: currentPlayer ? (currentPlayer.trackArtist +// || "Unknown Artist") : "" +// property string trackAlbum: currentPlayer ? (currentPlayer.trackAlbum +// || "Unknown Album") : "" +// property string trackArtUrl: currentPlayer ? (currentPlayer.trackArtUrl +// || "") : "" +// property real trackLength: currentPlayer ? currentPlayer.length : 0 +// property bool canPlay: currentPlayer ? currentPlayer.canPlay : false +// property bool canPause: currentPlayer ? currentPlayer.canPause : false +// property bool canGoNext: currentPlayer ? currentPlayer.canGoNext : false +// property bool canGoPrevious: currentPlayer ? currentPlayer.canGoPrevious : false +// property bool canSeek: currentPlayer ? currentPlayer.canSeek : false +// property bool hasPlayer: getAvailablePlayers().length > 0 + +// Item { +// Component.onCompleted: { +// updateCurrentPlayer() +// } +// } + +// function getAvailablePlayers() { +// if (!Mpris.players || !Mpris.players.values) { +// return [] +// } + +// let allPlayers = Mpris.players.values +// let controllablePlayers = [] + +// for (var i = 0; i < allPlayers.length; i++) { +// let player = allPlayers[i] +// if (player && player.canControl) { +// controllablePlayers.push(player) +// } +// } + +// return controllablePlayers +// } + +// function findActivePlayer() { +// let availablePlayers = getAvailablePlayers() +// if (availablePlayers.length === 0) { +// return null +// } + +// if (selectedPlayerIndex < availablePlayers.length) { +// return availablePlayers[selectedPlayerIndex] +// } else { +// selectedPlayerIndex = 0 +// return availablePlayers[0] +// } +// } + +// // Switch to the most recently active player +// function updateCurrentPlayer() { +// let newPlayer = findActivePlayer() +// if (newPlayer !== currentPlayer) { +// currentPlayer = newPlayer +// currentPosition = currentPlayer ? currentPlayer.position : 0 +// } +// } + +// function playPause() { +// if (currentPlayer) { +// if (currentPlayer.isPlaying) { +// currentPlayer.pause() +// } else { +// currentPlayer.play() +// } +// } +// } + +// function play() { +// if (currentPlayer && currentPlayer.canPlay) { +// currentPlayer.play() +// } +// } + +// function pause() { +// if (currentPlayer && currentPlayer.canPause) { +// currentPlayer.pause() +// } +// } + +// function next() { +// if (currentPlayer && currentPlayer.canGoNext) { +// currentPlayer.next() +// } +// } + +// function previous() { +// if (currentPlayer && currentPlayer.canGoPrevious) { +// currentPlayer.previous() +// } +// } + +// function seek(position) { +// if (currentPlayer && currentPlayer.canSeek) { +// currentPlayer.position = position +// currentPosition = position +// } +// } + +// // Seek to position based on ratio (0.0 to 1.0) +// function seekByRatio(ratio) { +// if (currentPlayer && currentPlayer.canSeek && currentPlayer.length > 0) { +// let seekPosition = ratio * currentPlayer.length +// currentPlayer.position = seekPosition +// currentPosition = seekPosition +// } +// } + +// // Update progress bar every second while playing +// Timer { +// id: positionTimer +// interval: 1000 +// running: currentPlayer && currentPlayer.isPlaying +// && currentPlayer.length > 0 +// && currentPlayer.playbackState === MprisPlaybackState.Playing +// repeat: true +// onTriggered: { +// if (currentPlayer && currentPlayer.isPlaying +// && currentPlayer.playbackState === MprisPlaybackState.Playing) { +// currentPosition = currentPlayer.position +// } else { +// running = false +// } +// } +// } + +// // Reset position when switching to inactive player +// onCurrentPlayerChanged: { +// if (!currentPlayer || !currentPlayer.isPlaying +// || currentPlayer.playbackState !== MprisPlaybackState.Playing) { +// currentPosition = 0 +// } +// } + +// // Update current player when available players change +// Connections { +// target: Mpris.players +// function onValuesChanged() { +// updateCurrentPlayer() +// } +// } + +// Cava { +// id: cava +// count: 44 +// } + +// // Expose cava values +// property alias cavaValues: cava.values +// } diff --git a/Services/Network.qml b/Services/Network.qml new file mode 100644 index 0000000..7427a2a --- /dev/null +++ b/Services/Network.qml @@ -0,0 +1,345 @@ +import QtQuick +import Quickshell.Io + +QtObject { + id: root + + property var networks: ({}) + property string connectingSsid: "" + property string connectStatus: "" + property string connectStatusSsid: "" + property string connectError: "" + property string detectedInterface: "" + + function signalIcon(signal) { + if (signal >= 80) + return "network_wifi" + if (signal >= 60) + return "network_wifi_3_bar" + if (signal >= 40) + return "network_wifi_2_bar" + if (signal >= 20) + return "network_wifi_1_bar" + return "wifi_0_bar" + } + + function isSecured(security) { + return security && security.trim() !== "" && security.trim() !== "--" + } + + function refreshNetworks() { + existingNetwork.running = true + } + + function connectNetwork(ssid, security) { + pendingConnect = { + "ssid": ssid, + "security": security, + "password": "" + } + doConnect() + } + + function submitPassword(ssid, password) { + pendingConnect = { + "ssid": ssid, + "security": networks[ssid].security, + "password": password + } + doConnect() + } + + function disconnectNetwork(ssid) { + disconnectProfileProcess.connectionName = ssid + disconnectProfileProcess.running = true + } + + property var pendingConnect: null + + function doConnect() { + const params = pendingConnect + if (!params) + return + + connectingSsid = params.ssid + connectStatus = "" + connectStatusSsid = params.ssid + + const targetNetwork = networks[params.ssid] + + if (targetNetwork && targetNetwork.existing) { + upConnectionProcess.profileName = params.ssid + upConnectionProcess.running = true + pendingConnect = null + return + } + + if (params.security && params.security !== "--") { + getInterfaceProcess.running = true + return + } + connectProcess.security = params.security + connectProcess.ssid = params.ssid + connectProcess.password = params.password + connectProcess.running = true + pendingConnect = null + } + + property int refreshInterval: 25000 + + // Only refresh when we have an active connection + property bool hasActiveConnection: { + for (const net in networks) { + if (networks[net].connected) { + return true + } + } + return false + } + + property Timer refreshTimer: Timer { + interval: root.refreshInterval + // Only run timer when we're connected to a network + running: root.hasActiveConnection + repeat: true + onTriggered: root.refreshNetworks() + } + + // Force a refresh when menu is opened + function onMenuOpened() { + refreshNetworks() + } + + function onMenuClosed() {// No need to do anything special on close + } + + property Process disconnectProfileProcess: Process { + id: disconnectProfileProcess + property string connectionName: "" + running: false + command: ["nmcli", "connection", "down", connectionName] + onRunningChanged: { + if (!running) { + root.refreshNetworks() + } + } + } + + property Process existingNetwork: Process { + id: existingNetwork + running: false + command: ["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show"] + stdout: StdioCollector { + onStreamFinished: { + const lines = text.split("\n") + const networksMap = {} + + for (var i = 0; i < lines.length; ++i) { + const line = lines[i].trim() + if (!line) + continue + + const parts = line.split(":") + if (parts.length < 2) { + console.warn("Malformed nmcli output line:", line) + continue + } + + const ssid = parts[0] + const type = parts[1] + + if (ssid) { + networksMap[ssid] = { + "ssid": ssid, + "type": type + } + } + } + scanProcess.existingNetwork = networksMap + scanProcess.running = true + } + } + } + + property Process scanProcess: Process { + id: scanProcess + running: false + command: ["nmcli", "-t", "-f", "SSID,SECURITY,SIGNAL,IN-USE", "device", "wifi", "list"] + + property var existingNetwork + + stdout: StdioCollector { + onStreamFinished: { + const lines = text.split("\n") + const networksMap = {} + + for (var i = 0; i < lines.length; ++i) { + const line = lines[i].trim() + if (!line) + continue + + const parts = line.split(":") + if (parts.length < 4) { + console.warn("Malformed nmcli output line:", line) + continue + } + const ssid = parts[0] + const security = parts[1] + const signal = parseInt(parts[2]) + const inUse = parts[3] === "*" + + if (ssid) { + if (!networksMap[ssid]) { + networksMap[ssid] = { + "ssid": ssid, + "security": security, + "signal": signal, + "connected": inUse, + "existing": ssid in scanProcess.existingNetwork + } + } else { + const existingNet = networksMap[ssid] + if (inUse) { + existingNet.connected = true + } + if (signal > existingNet.signal) { + existingNet.signal = signal + existingNet.security = security + } + } + } + } + + root.networks = networksMap + scanProcess.existingNetwork = {} + } + } + } + + property Process connectProcess: Process { + id: connectProcess + property string ssid: "" + property string password: "" + property string security: "" + running: false + command: { + if (password) { + return ["nmcli", "device", "wifi", "connect", `'${ssid}'`, "password", password] + } else { + return ["nmcli", "device", "wifi", "connect", `'${ssid}'`] + } + } + stdout: StdioCollector { + onStreamFinished: { + root.connectingSsid = "" + root.connectStatus = "success" + root.connectStatusSsid = connectProcess.ssid + root.connectError = "" + root.refreshNetworks() + } + } + stderr: StdioCollector { + onStreamFinished: { + root.connectingSsid = "" + root.connectStatus = "error" + root.connectStatusSsid = connectProcess.ssid + root.connectError = text + } + } + } + + property Process getInterfaceProcess: Process { + id: getInterfaceProcess + running: false + command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"] + stdout: StdioCollector { + onStreamFinished: { + var lines = text.split("\n") + for (var i = 0; i < lines.length; ++i) { + var parts = lines[i].split(":") + if (parts[1] === "wifi" && parts[2] !== "unavailable") { + root.detectedInterface = parts[0] + break + } + } + if (root.detectedInterface) { + var params = root.pendingConnect + addConnectionProcess.ifname = root.detectedInterface + addConnectionProcess.ssid = params.ssid + addConnectionProcess.password = params.password + addConnectionProcess.profileName = params.ssid + addConnectionProcess.security = params.security + addConnectionProcess.running = true + } else { + root.connectStatus = "error" + root.connectStatusSsid = root.pendingConnect.ssid + root.connectError = "No Wi-Fi interface found." + root.connectingSsid = "" + root.pendingConnect = null + } + } + } + } + + property Process addConnectionProcess: Process { + id: addConnectionProcess + property string ifname: "" + property string ssid: "" + property string password: "" + property string profileName: "" + property string security: "" + running: false + command: { + var cmd = ["nmcli", "connection", "add", "type", "wifi", "ifname", ifname, "con-name", profileName, "ssid", ssid] + if (security && security !== "--") { + cmd.push("wifi-sec.key-mgmt") + cmd.push("wpa-psk") + cmd.push("wifi-sec.psk") + cmd.push(password) + } + return cmd + } + stdout: StdioCollector { + onStreamFinished: { + upConnectionProcess.profileName = addConnectionProcess.profileName + upConnectionProcess.running = true + } + } + stderr: StdioCollector { + onStreamFinished: { + upConnectionProcess.profileName = addConnectionProcess.profileName + upConnectionProcess.running = true + } + } + } + + property Process upConnectionProcess: Process { + id: upConnectionProcess + property string profileName: "" + running: false + command: ["nmcli", "connection", "up", "id", profileName] + stdout: StdioCollector { + onStreamFinished: { + root.connectingSsid = "" + root.connectStatus = "success" + root.connectStatusSsid = root.pendingConnect ? root.pendingConnect.ssid : "" + root.connectError = "" + root.pendingConnect = null + root.refreshNetworks() + } + } + stderr: StdioCollector { + onStreamFinished: { + root.connectingSsid = "" + root.connectStatus = "error" + root.connectStatusSsid = root.pendingConnect ? root.pendingConnect.ssid : "" + root.connectError = text + root.pendingConnect = null + } + } + } + + Component.onCompleted: { + refreshNetworks() + } +} diff --git a/Services/Niri.qml b/Services/Niri.qml new file mode 100644 index 0000000..bb7f1c4 --- /dev/null +++ b/Services/Niri.qml @@ -0,0 +1,140 @@ +pragma Singleton + +pragma ComponentBehavior + +import QtQuick +import Quickshell +import Quickshell.Io + +Singleton { + id: root + + property var workspaces: [] + property var windows: [] + property int focusedWindowIndex: -1 + property bool inOverview: false + property string focusedWindowTitle: "(No active window)" + + function updateFocusedWindowTitle() { + if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) { + focusedWindowTitle = windows[focusedWindowIndex].title + || "(Unnamed window)" + } else { + focusedWindowTitle = "(No active window)" + } + } + + onWindowsChanged: updateFocusedWindowTitle() + onFocusedWindowIndexChanged: updateFocusedWindowTitle() + + Component.onCompleted: { + eventStream.running = true + } + + Process { + id: workspaceProcess + running: false + command: ["niri", "msg", "--json", "workspaces"] + + stdout: SplitParser { + onRead: function (line) { + try { + const workspacesData = JSON.parse(line) + const workspacesList = [] + + for (const ws of workspacesData) { + workspacesList.push({ + "id": ws.id, + "idx": ws.idx, + "name": ws.name || "", + "output": ws.output || "", + "isFocused": ws.is_focused === true, + "isActive": ws.is_active === true, + "isUrgent": ws.is_urgent === true, + "isOccupied": ws.active_window_id ? true : false + }) + } + + workspacesList.sort((a, b) => { + if (a.output !== b.output) { + return a.output.localeCompare(b.output) + } + return a.id - b.id + }) + + root.workspaces = workspacesList + } catch (e) { + console.error("Failed to parse workspaces:", e, line) + } + } + } + } + + Process { + id: eventStream + running: false + command: ["niri", "msg", "--json", "event-stream"] + + stdout: SplitParser { + onRead: data => { + try { + const event = JSON.parse(data.trim()) + + if (event.WorkspacesChanged) { + workspaceProcess.running = true + } else if (event.WindowsChanged) { + try { + const windowsData = event.WindowsChanged.windows + const windowsList = [] + for (const win of windowsData) { + windowsList.push({ + "id": win.id, + "title": win.title || "", + "appId": win.app_id || "", + "workspaceId": win.workspace_id || null, + "isFocused": win.is_focused === true + }) + } + + windowsList.sort((a, b) => a.id - b.id) + root.windows = windowsList + for (var i = 0; i < windowsList.length; i++) { + if (windowsList[i].isFocused) { + root.focusedWindowIndex = i + break + } + } + } catch (e) { + console.error("Error parsing windows event:", e) + } + } else if (event.WorkspaceActivated) { + workspaceProcess.running = true + } else if (event.WindowFocusChanged) { + try { + const focusedId = event.WindowFocusChanged.id + if (focusedId) { + root.focusedWindowIndex = root.windows.findIndex( + w => w.id === focusedId) + if (root.focusedWindowIndex < 0) { + root.focusedWindowIndex = 0 + } + } else { + root.focusedWindowIndex = -1 + } + } catch (e) { + console.error("Error parsing window focus event:", e) + } + } else if (event.OverviewOpenedOrClosed) { + try { + root.inOverview = event.OverviewOpenedOrClosed.is_open === true + } catch (e) { + console.error("Error parsing overview state:", e) + } + } + } catch (e) { + console.error("Error parsing event stream:", e, data) + } + } + } + } +} diff --git a/Services/Style.qml b/Services/Style.qml index 407af8a..12b0b6e 100644 --- a/Services/Style.qml +++ b/Services/Style.qml @@ -55,8 +55,8 @@ Singleton { property int marginTiny: 4 property int marginSmall: 8 property int marginMedium: 12 - property int marginLarge: 18 - property int marginXL: 24 + property int marginLarge: 16 + property int marginXL: 20 // Opacity property real opacityLight: 0.25 diff --git a/Services/SysInfo.qml b/Services/SysInfo.qml new file mode 100644 index 0000000..39bef22 --- /dev/null +++ b/Services/SysInfo.qml @@ -0,0 +1,47 @@ +pragma Singleton + +import QtQuick +import Qt.labs.folderlistmodel +import Quickshell +import Quickshell.Io + +Singleton { + id: manager //TBC + + property string updateInterval: "2s" + property string cpuUsageStr: "" + property string cpuTempStr: "" + property string memoryUsageStr: "" + property string memoryUsagePerStr: "" + property real cpuUsage: 0 + property real memoryUsage: 0 + property real cpuTemp: 0 + property real diskUsage: 0 + property real memoryUsagePer: 0 + property string diskUsageStr: "" + + Process { + id: zigstatProcess + running: true + command: [Quickshell.shellDir + "/Programs/zigstat", updateInterval] + stdout: SplitParser { + onRead: function (line) { + try { + const data = JSON.parse(line) + cpuUsage = +data.cpu + cpuTemp = +data.cputemp + memoryUsage = +data.mem + memoryUsagePer = +data.memper + diskUsage = +data.diskper + cpuUsageStr = data.cpu + "%" + cpuTempStr = data.cputemp + "°C" + memoryUsageStr = data.mem + "G" + memoryUsagePerStr = data.memper + "%" + diskUsageStr = data.diskper + "%" + } catch (e) { + console.error("Failed to parse zigstat output:", e) + } + } + } + } +} diff --git a/Services/Wallpapers.qml b/Services/Wallpapers.qml new file mode 100644 index 0000000..54c1cfb --- /dev/null +++ b/Services/Wallpapers.qml @@ -0,0 +1,136 @@ +pragma Singleton + +import QtQuick +import Qt.labs.folderlistmodel +import Quickshell +import Quickshell.Io + +Singleton { + id: manager //TBC + + Item { + Component.onCompleted: { + loadWallpapers() + setCurrentWallpaper(currentWallpaper, true) + toggleRandomWallpaper() + } + } + + property var wallpaperList: [] + property string currentWallpaper: Settings.settings.currentWallpaper + property bool scanning: false + property string transitionType: Settings.settings.transitionType + property var randomChoices: ["fade", "left", "right", "top", "bottom", "wipe", "wave", "grow", "center", "any", "outer"] + + function loadWallpapers() { + scanning = true + wallpaperList = [] + folderModel.folder = "" + folderModel.folder = "file://" + (Settings.settings.wallpaperFolder + !== undefined ? Settings.settings.wallpaperFolder : "") + } + + function changeWallpaper(path) { + setCurrentWallpaper(path) + } + + function setCurrentWallpaper(path, isInitial) { + currentWallpaper = path + if (!isInitial) { + Settings.settings.currentWallpaper = path + } + if (Settings.settings.useSWWW) { + if (Settings.settings.transitionType === "random") { + transitionType = randomChoices[Math.floor(Math.random( + ) * randomChoices.length)] + } else { + transitionType = Settings.settings.transitionType + } + changeWallpaperProcess.running = true + } + + if (randomWallpaperTimer.running) { + randomWallpaperTimer.restart() + } + + generateTheme() + } + + function setRandomWallpaper() { + var randomIndex = Math.floor(Math.random() * wallpaperList.length) + var randomPath = wallpaperList[randomIndex] + if (!randomPath) { + return + } + setCurrentWallpaper(randomPath) + } + + function toggleRandomWallpaper() { + if (Settings.settings.randomWallpaper && !randomWallpaperTimer.running) { + randomWallpaperTimer.start() + setRandomWallpaper() + } else if (!Settings.settings.randomWallpaper + && randomWallpaperTimer.running) { + randomWallpaperTimer.stop() + } + } + + function restartRandomWallpaperTimer() { + if (Settings.settings.randomWallpaper) { + randomWallpaperTimer.stop() + randomWallpaperTimer.start() + } + } + + function generateTheme() { + if (Settings.settings.useWallpaperTheme) { + generateThemeProcess.running = true + } + } + + Timer { + id: randomWallpaperTimer + interval: Settings.settings.wallpaperInterval * 1000 + running: false + repeat: true + onTriggered: setRandomWallpaper() + triggeredOnStart: false + } + + FolderListModel { + id: folderModel + // Swww supports many images format but Quickshell only support a subset of those. + nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.pnm", "*.bmp"] + showDirs: false + sortField: FolderListModel.Name + onStatusChanged: { + if (status === FolderListModel.Ready) { + var files = [] + var filesSwww = [] + for (var i = 0; i < count; i++) { + var filepath = (Settings.settings.wallpaperFolder + !== undefined ? Settings.settings.wallpaperFolder : "") + "/" + get( + i, "fileName") + files.push(filepath) + } + wallpaperList = files + scanning = false + } + } + } + + Process { + id: changeWallpaperProcess + command: ["swww", "img", "--resize", Settings.settings.wallpaperResize, "--transition-fps", Settings.settings.transitionFps.toString( + ), "--transition-type", transitionType, "--transition-duration", Settings.settings.transitionDuration.toString( + ), currentWallpaper] + running: false + } + + Process { + id: generateThemeProcess + command: ["wallust", "run", currentWallpaper, "-u", "-k", "-d", "Templates"] + workingDirectory: Quickshell.shellDir + running: false + } +} diff --git a/Services/Workspaces.qml b/Services/Workspaces.qml new file mode 100644 index 0000000..fc1ab9b --- /dev/null +++ b/Services/Workspaces.qml @@ -0,0 +1,156 @@ +pragma Singleton + +pragma ComponentBehavior + +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Hyprland +import qs.Services + +Singleton { + id: root + + property ListModel workspaces: ListModel {} + property bool isHyprland: false + property bool isNiri: false + property var hlWorkspaces: Hyprland.workspaces.values + // Detect which compositor we're using + Component.onCompleted: { + console.log("WorkspaceManager initializing...") + detectCompositor() + } + + function detectCompositor() { + try { + try { + if (Hyprland.eventSocketPath) { + console.log("Detected Hyprland compositor") + isHyprland = true + isNiri = false + initHyprland() + return + } + } catch (e) { + console.log("Hyprland not available:", e) + } + + if (typeof Niri !== "undefined") { + console.log("Detected Niri service") + isHyprland = false + isNiri = true + initNiri() + return + } + + console.log("No supported compositor detected") + } catch (e) { + console.error("Error detecting compositor:", e) + } + } + + // Initialize Hyprland integration + function initHyprland() { + try { + // Fixes the odd workspace issue. + Hyprland.refreshWorkspaces() + // hlWorkspaces = Hyprland.workspaces.values; + // updateHyprlandWorkspaces(); + return true + } catch (e) { + console.error("Error initializing Hyprland:", e) + isHyprland = false + return false + } + } + + onHlWorkspacesChanged: { + updateHyprlandWorkspaces() + } + + Connections { + target: Hyprland.workspaces + function onValuesChanged() { + updateHyprlandWorkspaces() + } + } + + Connections { + target: Hyprland + function onRawEvent(event) { + updateHyprlandWorkspaces() + } + } + + function updateHyprlandWorkspaces() { + workspaces.clear() + try { + for (var i = 0; i < hlWorkspaces.length; i++) { + const ws = hlWorkspaces[i] + workspaces.append({ + "id": i, + "idx": ws.id, + "name": ws.name || "", + "output": ws.monitor?.name || "", + "isActive": ws.active === true, + "isFocused": ws.focused === true, + "isUrgent": ws.urgent === true + }) + } + workspacesChanged() + } catch (e) { + console.error("Error updating Hyprland workspaces:", e) + } + } + + function initNiri() { + updateNiriWorkspaces() + } + + Connections { + target: Niri + function onWorkspacesChanged() { + updateNiriWorkspaces() + } + } + + function updateNiriWorkspaces() { + const niriWorkspaces = Niri.workspaces || [] + workspaces.clear() + for (var i = 0; i < niriWorkspaces.length; i++) { + const ws = niriWorkspaces[i] + workspaces.append({ + "id": ws.id, + "idx": ws.idx || 1, + "name": ws.name || "", + "output": ws.output || "", + "isFocused": ws.isFocused === true, + "isActive": ws.isActive === true, + "isUrgent": ws.isUrgent === true, + "isOccupied": ws.isOccupied === true + }) + } + + workspacesChanged() + } + + function switchToWorkspace(workspaceId) { + if (isHyprland) { + try { + Hyprland.dispatch(`workspace ${workspaceId}`) + } catch (e) { + console.error("Error switching Hyprland workspace:", e) + } + } else if (isNiri) { + try { + Quickshell.execDetached( + ["niri", "msg", "action", "focus-workspace", workspaceId.toString( + )]) + } catch (e) { + console.error("Error switching Niri workspace:", e) + } + } else { + console.warn("No supported compositor detected for workspace switching") + } + } +} diff --git a/Widgets/NCalendar.qml b/Widgets/NCalendar.qml index 59c1519..d1518e8 100644 --- a/Widgets/NCalendar.qml +++ b/Widgets/NCalendar.qml @@ -18,7 +18,7 @@ NPanel { border.color: Colors.backgroundTertiary border.width: Math.min(1, Style.borderMedium * scaling) width: 340 * scaling - height: 300 + height: 320 // TBC anchors.top: parent.top anchors.right: parent.right anchors.topMargin: Style.marginTiny * scaling @@ -66,6 +66,10 @@ NPanel { } } + NDivider { + Layout.fillWidth: true + } + DayOfWeekRow { Layout.fillWidth: true spacing: 0 diff --git a/Widgets/NDivider.qml b/Widgets/NDivider.qml new file mode 100644 index 0000000..bd47353 --- /dev/null +++ b/Widgets/NDivider.qml @@ -0,0 +1,10 @@ +import QtQuick +import Quickshell +import Quickshell.Widgets +import qs.Services + +Rectangle { + width: parent.width + height: Math.max(1, Style.borderThin * scaling) + color: Colors.outline +} diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml index a5bc375..6155345 100644 --- a/Widgets/NIconButton.qml +++ b/Widgets/NIconButton.qml @@ -22,7 +22,6 @@ Rectangle { color: root.hovering ? Colors.accentPrimary : "transparent" Text { - id: iconText anchors.centerIn: parent text: root.icon font.family: "Material Symbols Outlined" From 1b69bab95f4f9dc926684834ac69b2a262a8dadb Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sat, 9 Aug 2025 23:58:53 -0400 Subject: [PATCH 028/394] NClock --- Modules/Bar/Bar.qml | 2 +- Modules/Bar/Clock.qml => Widgets/NClock.qml | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename Modules/Bar/Clock.qml => Widgets/NClock.qml (100%) diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index e4c7f3b..7041f41 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -73,7 +73,7 @@ PanelWindow { Layout.alignment: Qt.AlignVCenter } - Clock {} + NClock {} NIconButton { id: demoPanelToggler diff --git a/Modules/Bar/Clock.qml b/Widgets/NClock.qml similarity index 100% rename from Modules/Bar/Clock.qml rename to Widgets/NClock.qml From 58c8cbbf1df1ec30df550b908d51def6096914f5 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 00:09:45 -0400 Subject: [PATCH 029/394] Reusable Clock --- Modules/Bar/Bar.qml | 2 +- Modules/Bar/Clock.qml | 32 ++++++++++++++++++++++++++++++++ Widgets/NClock.qml | 29 ++++++----------------------- 3 files changed, 39 insertions(+), 24 deletions(-) create mode 100644 Modules/Bar/Clock.qml diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 7041f41..e4c7f3b 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -73,7 +73,7 @@ PanelWindow { Layout.alignment: Qt.AlignVCenter } - NClock {} + Clock {} NIconButton { id: demoPanelToggler diff --git a/Modules/Bar/Clock.qml b/Modules/Bar/Clock.qml new file mode 100644 index 0000000..bc1215a --- /dev/null +++ b/Modules/Bar/Clock.qml @@ -0,0 +1,32 @@ +import QtQuick +import qs.Services +import qs.Widgets + +// Clock Icon with attached calendar +NClock { + id: root + + NTooltip { + id: tooltip + text: Time.dateString + target: root + } + + NCalendar { + id: calendar + visible: false + } + + onEntered: function (){ + if (!calendar.visible) { + tooltip.show() + } + } + onExited: function (){ + tooltip.hide() + } + onClicked: function () { + calendar.visible = !calendar.visible + tooltip.hide() + } +} diff --git a/Widgets/NClock.qml b/Widgets/NClock.qml index d883e27..52ec789 100644 --- a/Widgets/NClock.qml +++ b/Widgets/NClock.qml @@ -6,6 +6,9 @@ Rectangle { id: root readonly property real scaling: Scaling.scale(screen) + property var onEntered: function () {} + property var onExited: function () {} + property var onClicked: function () {} width: textItem.paintedWidth height: textItem.paintedHeight @@ -22,28 +25,8 @@ Rectangle { anchors.fill: parent cursorShape: Qt.PointingHandCursor hoverEnabled: true - onEntered: { - if (!calendar.visible) { - tooltip.show() - } - } - onExited: { - tooltip.hide() - } - onClicked: function () { - calendar.visible = !calendar.visible - tooltip.hide() - } - } - - NCalendar { - id: calendar - visible: false - } - - NTooltip { - id: tooltip - text: Time.dateString - target: root + onEntered: root.onEntered() + onExited: root.onExited() + onClicked: root.onClicked() } } From ff6f9c5690aab348dd1a8fed578679c90b16b4ee Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 10 Aug 2025 12:36:40 +0200 Subject: [PATCH 030/394] Add Workspace, Overview and Background --- Modules/Background/Background.qml | 45 +++++ Modules/Background/Overview.qml | 56 +++++++ Modules/Bar/Bar.qml | 12 +- Modules/Bar/Workspace.qml | 267 ++++++++++++++++++++++++++++++ Services/Workspaces.qml | 2 +- shell.qml | 21 ++- 6 files changed, 389 insertions(+), 14 deletions(-) create mode 100644 Modules/Bar/Workspace.qml diff --git a/Modules/Background/Background.qml b/Modules/Background/Background.qml index e69de29..114bc26 100644 --- a/Modules/Background/Background.qml +++ b/Modules/Background/Background.qml @@ -0,0 +1,45 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland +import qs.Services + +ShellRoot { + + property var modelData + property string wallpaperSource: "/home/lysec/Pictures/wallpapers/wallhaven-6lqvql.jpg" + + Variants { + model: Quickshell.screens + + PanelWindow { + required property ShellScreen modelData + + visible: wallpaperSource !== "" + anchors { + bottom: true + top: true + right: true + left: true + } + margins { + top: 0 + } + color: "transparent" + screen: modelData + WlrLayershell.layer: WlrLayer.Background + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.namespace: "quickshell-wallpaper" + Image { + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + source: wallpaperSource + visible: wallpaperSource !== "" + cache: true + smooth: true + mipmap: false + } + } + } + + +} \ No newline at end of file diff --git a/Modules/Background/Overview.qml b/Modules/Background/Overview.qml index e69de29..fd65eca 100644 --- a/Modules/Background/Overview.qml +++ b/Modules/Background/Overview.qml @@ -0,0 +1,56 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import Quickshell.Wayland +import qs.Services + +ShellRoot { + property string wallpaperSource: "/home/lysec/Pictures/wallpapers/wallhaven-6lqvql.jpg" + property var modelData + + Variants { + model: Quickshell.screens + + PanelWindow { + required property ShellScreen modelData + + visible: wallpaperSource !== "" + anchors { + top: true + bottom: true + right: true + left: true + } + color: "transparent" + screen: modelData + WlrLayershell.layer: WlrLayer.Background + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.namespace: "quickshell-overview" + Image { + id: bgImage + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + source: wallpaperSource + cache: true + smooth: true + mipmap: false + visible: wallpaperSource !== "" + } + MultiEffect { + id: overviewBgBlur + anchors.fill: parent + source: bgImage + blurEnabled: true + blur: 0.48 + blurMax: 128 + } + Rectangle { + anchors.fill: parent + color: Qt.rgba( + Colors.backgroundPrimary.r, + Colors.backgroundPrimary.g, + Colors.backgroundPrimary.b, 0.5) + } + } + } +} diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index e4c7f3b..604a38e 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -45,6 +45,7 @@ PanelWindow { NText { text: "Left" + anchors.verticalCenter: parent.verticalCenter } } @@ -55,9 +56,8 @@ PanelWindow { anchors.verticalCenter: parent.verticalCenter spacing: Style.marginMedium * scaling - NText { - text: "Center" - } + Workspace {} + } Row { @@ -70,10 +70,12 @@ PanelWindow { NText { text: "Right" - Layout.alignment: Qt.AlignVCenter + anchors.verticalCenter: parent.verticalCenter } - Clock {} + Clock { + anchors.verticalCenter: parent.verticalCenter + } NIconButton { id: demoPanelToggler diff --git a/Modules/Bar/Workspace.qml b/Modules/Bar/Workspace.qml new file mode 100644 index 0000000..fa7a2de --- /dev/null +++ b/Modules/Bar/Workspace.qml @@ -0,0 +1,267 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Window +import QtQuick.Effects +import Quickshell +import Quickshell.Io +import qs.Services + +Item { + id: root + property bool isDestroying: false + property bool hovered: false + + readonly property real scaling: Scaling.scale(screen) + property var modelData + + signal workspaceChanged(int workspaceId, color accentColor) + + property ListModel localWorkspaces: ListModel {} + property real masterProgress: 0.0 + property bool effectsActive: false + property color effectColor: Colors.accentPrimary + + // Unified scale + property real s: scale + property int horizontalPadding: Math.round(16 * s) + property int spacingBetweenPills: Math.round(8 * s) + + width: { + let total = 0; + for (let i = 0; i < localWorkspaces.count; i++) { + const ws = localWorkspaces.get(i); + if (ws.isFocused) + total += Math.round(44 * s); + else if (ws.isActive) + total += Math.round(28 * s); + else + total += Math.round(16 * s); + } + total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills; + total += horizontalPadding * 2; + return total; + } + + height: Math.round(36 * s) + + Component.onCompleted: { + localWorkspaces.clear(); + for (let i = 0; i < Workspaces.workspaces.count; i++) { + const ws = Services.Workspaces.workspaces.get(i); + if (ws.output.toLowerCase() === screen.name.toLowerCase()) { + localWorkspaces.append(ws); + } + } + workspaceRepeater.model = localWorkspaces; + updateWorkspaceFocus(); + } + + Connections { + target: Workspaces + function onWorkspacesChanged() { + localWorkspaces.clear(); + for (let i = 0; i < Workspaces.workspaces.count; i++) { + const ws = Workspaces.workspaces.get(i); + if (ws.output.toLowerCase() === screen.name.toLowerCase()) { + localWorkspaces.append(ws); + } + } + + workspaceRepeater.model = localWorkspaces; + updateWorkspaceFocus(); + } + } + + function triggerUnifiedWave() { + effectColor = Colors.accentPrimary; + masterAnimation.restart(); + } + + SequentialAnimation { + id: masterAnimation + PropertyAction { + target: root + property: "effectsActive" + value: true + } + NumberAnimation { + target: root + property: "masterProgress" + from: 0.0 + to: 1.0 + duration: 1000 + easing.type: Easing.OutQuint + } + PropertyAction { + target: root + property: "effectsActive" + value: false + } + PropertyAction { + target: root + property: "masterProgress" + value: 0.0 + } + } + + function updateWorkspaceFocus() { + for (let i = 0; i < localWorkspaces.count; i++) { + const ws = localWorkspaces.get(i); + if (ws.isFocused === true) { + root.triggerUnifiedWave(); + root.workspaceChanged(ws.id, Colors.accentPrimary); + break; + } + } + } + + Rectangle { + id: workspaceBackground + width: parent.width - Math.round(15 * s) + height: Math.round(26 * s) + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + radius: Math.round(12 * s) + color: Colors.surfaceVariant + border.color: Qt.rgba(Colors.textPrimary.r, Colors.textPrimary.g, Colors.textPrimary.b, 0.1) + border.width: Math.max(1, Math.round(1 * s)) + layer.enabled: true + layer.effect: MultiEffect { + shadowColor: "black" + // radius: 12 + + shadowVerticalOffset: 0 + shadowHorizontalOffset: 0 + shadowOpacity: 0.10 + } + } + + Row { + id: pillRow + spacing: spacingBetweenPills + anchors.verticalCenter: workspaceBackground.verticalCenter + width: root.width - horizontalPadding * 2 + x: horizontalPadding + Repeater { + id: workspaceRepeater + model: localWorkspaces + Item { + id: workspacePillContainer + height: Math.round(12 * s) + width: { + if (model.isFocused) + return Math.round(44 * s); + else if (model.isActive) + return Math.round(28 * s); + else + return Math.round(16 * s); + } + + Rectangle { + id: workspacePill + anchors.fill: parent + radius: { + if (model.isFocused) + return Math.round(12 * s); + else + // half of focused height (if you want to animate this too) + return Math.round(6 * s); + } + color: { + if (model.isFocused) + return Colors.accentPrimary; + if (model.isUrgent) + return Colors.error; + if (model.isActive || model.isOccupied) + return Colors.accentTertiary; + if (model.isUrgent) + return Colors.error; + + return Colors.outline; + } + scale: model.isFocused ? 1.0 : 0.9 + z: 0 + + MouseArea { + id: pillMouseArea + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + Workspaces.switchToWorkspace(model.idx); + } + hoverEnabled: true + } + // Material 3-inspired smooth animation for width, height, scale, color, opacity, and radius + Behavior on width { + NumberAnimation { + duration: 350 + easing.type: Easing.OutBack + } + } + Behavior on height { + NumberAnimation { + duration: 350 + easing.type: Easing.OutBack + } + } + Behavior on scale { + NumberAnimation { + duration: 300 + easing.type: Easing.OutBack + } + } + Behavior on color { + ColorAnimation { + duration: 200 + easing.type: Easing.InOutCubic + } + } + Behavior on opacity { + NumberAnimation { + duration: 200 + easing.type: Easing.InOutCubic + } + } + Behavior on radius { + NumberAnimation { + duration: 350 + easing.type: Easing.OutBack + } + } + } + + Behavior on width { + NumberAnimation { + duration: 350 + easing.type: Easing.OutBack + } + } + Behavior on height { + NumberAnimation { + duration: 350 + easing.type: Easing.OutBack + } + } + // Burst effect overlay for focused pill (smaller outline) + Rectangle { + id: pillBurst + anchors.centerIn: workspacePillContainer + width: workspacePillContainer.width + 18 * root.masterProgress * scale + height: workspacePillContainer.height + 18 * root.masterProgress * scale + radius: width / 2 + color: "transparent" + border.color: root.effectColor + border.width: Math.max(1, Math.round((2 + 6 * (1.0 - root.masterProgress)) * s)) + opacity: root.effectsActive && model.isFocused ? (1.0 - root.masterProgress) * 0.7 : 0 + visible: root.effectsActive && model.isFocused + z: 1 + } + } + } + } + + Component.onDestruction: { + root.isDestroying = true; + } +} diff --git a/Services/Workspaces.qml b/Services/Workspaces.qml index fc1ab9b..8f3fb5f 100644 --- a/Services/Workspaces.qml +++ b/Services/Workspaces.qml @@ -17,7 +17,7 @@ Singleton { property var hlWorkspaces: Hyprland.workspaces.values // Detect which compositor we're using Component.onCompleted: { - console.log("WorkspaceManager initializing...") + console.log("Workspace initializing...") detectCompositor() } diff --git a/shell.qml b/shell.qml index 8f3c314..0b8c344 100644 --- a/shell.qml +++ b/shell.qml @@ -1,16 +1,13 @@ +// Disable reload popup +//@ pragma Env QS_NO_RELOAD_POPUP=1 - -/* - Here we go, this is it. Rebuild time... - No spaghetti code, preset sizing, proper project structure - only "spawn" UI, do not do anything else here. -*/ import QtQuick import Quickshell import Quickshell.Io import Quickshell.Widgets import qs.Modules.Bar import qs.Modules.DemoPanel +import qs.Modules.Background ShellRoot { id: root @@ -18,8 +15,16 @@ ShellRoot { Variants { model: Quickshell.screens - delegate: Bar { - modelData: item + delegate: Item { + required property ShellScreen modelData + + Bar { + modelData: parent.modelData + } + + // Background {} + + // Overview {} } } From 30485876b598ec21dc362d9ff80aa73b3eabb8ee Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 06:45:00 -0400 Subject: [PATCH 031/394] Assets/Tests/wallpaper.png (2560x1440) --- Assets/Tests/wallpaper.png | Bin 0 -> 382179 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 Assets/Tests/wallpaper.png diff --git a/Assets/Tests/wallpaper.png b/Assets/Tests/wallpaper.png new file mode 100644 index 0000000000000000000000000000000000000000..6974b0ddc0a2dd89d8bfdbaaeafeeed745207b3e GIT binary patch literal 382179 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
7aQf{kd&fv01 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
2m!qAJgXyhBToIEM33#9PPv*`zH_h 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 Date: Sun, 10 Aug 2025 06:57:11 -0400 Subject: [PATCH 032/394] revert shell Variants stuff --- Modules/Bar/Bar.qml | 3 +-- shell.qml | 19 +++++++++---------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 604a38e..dc66a01 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -14,8 +14,7 @@ PanelWindow { screen: modelData implicitHeight: Style.barHeight * scaling color: "transparent" - visible: Settings.settings.barMonitors.includes(modelData.name) - || (Settings.settings.barMonitors.length === 0) + visible: true //Settings.settings.barMonitors.includes(modelData.name) || (Settings.settings.barMonitors.length === 0) anchors { top: true diff --git a/shell.qml b/shell.qml index 0b8c344..5b518e4 100644 --- a/shell.qml +++ b/shell.qml @@ -12,22 +12,21 @@ import qs.Modules.Background ShellRoot { id: root + Variants { model: Quickshell.screens - delegate: Item { - required property ShellScreen modelData - - Bar { - modelData: parent.modelData - } - - // Background {} - - // Overview {} + delegate: Bar { + modelData: item } + + + Background {} + + Overview {} } + DemoPanel { id: demoPanel } From ca86e93540ece6619ca72c7105e4e747875fd689 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 07:02:14 -0400 Subject: [PATCH 033/394] Shell vs Bar modelData --- Modules/Bar/Bar.qml | 4 ++-- shell.qml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index dc66a01..fa14d46 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -14,7 +14,7 @@ PanelWindow { screen: modelData implicitHeight: Style.barHeight * scaling color: "transparent" - visible: true //Settings.settings.barMonitors.includes(modelData.name) || (Settings.settings.barMonitors.length === 0) + visible: Settings.settings.barMonitors.includes(modelData.name) || (Settings.settings.barMonitors.length === 0) anchors { top: true @@ -43,7 +43,7 @@ PanelWindow { spacing: Style.marginMedium * scaling NText { - text: "Left" + text: screen.name anchors.verticalCenter: parent.verticalCenter } } diff --git a/shell.qml b/shell.qml index 5b518e4..375f95e 100644 --- a/shell.qml +++ b/shell.qml @@ -21,9 +21,9 @@ ShellRoot { } - Background {} + // Background {} - Overview {} + // Overview {} } From afd69ffbedcd5f433eb40ac36b13fb2b4a47ede6 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 07:08:11 -0400 Subject: [PATCH 034/394] Variants need to be splitted --- Modules/Background/Background.qml | 2 +- shell.qml | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Modules/Background/Background.qml b/Modules/Background/Background.qml index 114bc26..c1f3d12 100644 --- a/Modules/Background/Background.qml +++ b/Modules/Background/Background.qml @@ -6,7 +6,7 @@ import qs.Services ShellRoot { property var modelData - property string wallpaperSource: "/home/lysec/Pictures/wallpapers/wallhaven-6lqvql.jpg" + property string wallpaperSource: "/home/seb/Development/misc/Noctalia/Assets/Tests/wallpaper.png" Variants { model: Quickshell.screens diff --git a/shell.qml b/shell.qml index 375f95e..49f416c 100644 --- a/shell.qml +++ b/shell.qml @@ -19,13 +19,15 @@ ShellRoot { delegate: Bar { modelData: item } - - - // Background {} - - // Overview {} } + + Variants { + model: Quickshell.screens + delegate: Background { + modelData: item + } + } DemoPanel { id: demoPanel From f4e992bf83e159e2a5d4bb933cb2933796c87cd8 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 10 Aug 2025 13:11:29 +0200 Subject: [PATCH 035/394] Avoid hardcoding test wallpaper path --- Modules/Background/Background.qml | 2 +- Modules/Background/Overview.qml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/Background/Background.qml b/Modules/Background/Background.qml index c1f3d12..c587bbe 100644 --- a/Modules/Background/Background.qml +++ b/Modules/Background/Background.qml @@ -6,7 +6,7 @@ import qs.Services ShellRoot { property var modelData - property string wallpaperSource: "/home/seb/Development/misc/Noctalia/Assets/Tests/wallpaper.png" + property string wallpaperSource: Qt.resolvedUrl("../../Assets/Tests/wallpaper.png") Variants { model: Quickshell.screens diff --git a/Modules/Background/Overview.qml b/Modules/Background/Overview.qml index fd65eca..2bbc85b 100644 --- a/Modules/Background/Overview.qml +++ b/Modules/Background/Overview.qml @@ -5,7 +5,7 @@ import Quickshell.Wayland import qs.Services ShellRoot { - property string wallpaperSource: "/home/lysec/Pictures/wallpapers/wallhaven-6lqvql.jpg" + property string wallpaperSource: Qt.resolvedUrl("../../Assets/Tests/wallpaper.png") property var modelData Variants { From b9103d4976dcc6369120c7c1ed89db7d9e0633fb Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 10 Aug 2025 13:16:25 +0200 Subject: [PATCH 036/394] Add Overview --- shell.qml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/shell.qml b/shell.qml index 49f416c..55872f2 100644 --- a/shell.qml +++ b/shell.qml @@ -20,7 +20,7 @@ ShellRoot { modelData: item } } - + Variants { model: Quickshell.screens @@ -29,6 +29,14 @@ ShellRoot { } } + Variants { + model: Quickshell.screens + + delegate: Overview { + modelData: item + } + } + DemoPanel { id: demoPanel } From c287ff35000f8cb18d79f897b9b01abb2a432052 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 07:33:27 -0400 Subject: [PATCH 037/394] DemoPanel cleanup --- Modules/DemoPanel/DemoPanel.qml | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/Modules/DemoPanel/DemoPanel.qml b/Modules/DemoPanel/DemoPanel.qml index 89fec51..c758c16 100644 --- a/Modules/DemoPanel/DemoPanel.qml +++ b/Modules/DemoPanel/DemoPanel.qml @@ -40,7 +40,7 @@ NPanel { ColumnLayout { spacing: 16 * scaling NText { - text: "NIconButton" + text: "NIconButton + NTooltip" color: Colors.accentSecondary } @@ -55,6 +55,13 @@ NPanel { } } + NTooltip { + id: myTooltip + target: myIconButton + positionAbove: false + text: "Hello world" + } + NDivider {Layout.fillWidth: true} } @@ -64,7 +71,7 @@ NPanel { spacing: Style.marginLarge * scaling uniformCellSizes: true NText { - text: "NToggle + NTooltip" + text: "NToggle" color: Colors.accentSecondary } @@ -76,12 +83,7 @@ NPanel { } } - NTooltip { - id: myTooltip - target: myIconButton - positionAbove: false - text: "Hello world" - } + NDivider { Layout.fillWidth: true } From c612347f0cd6de56b4bdcb0e91dea2f81f242549 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 07:34:30 -0400 Subject: [PATCH 038/394] Background and Overview without Variants (test) --- shell.qml | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/shell.qml b/shell.qml index 55872f2..1cdc7ad 100644 --- a/shell.qml +++ b/shell.qml @@ -21,21 +21,24 @@ ShellRoot { } } - Variants { - model: Quickshell.screens + Background {} + Overview {} - delegate: Background { - modelData: item - } - } + // Variants { + // model: Quickshell.screens - Variants { - model: Quickshell.screens + // delegate: Background { + // modelData: item + // } + // } - delegate: Overview { - modelData: item - } - } + // Variants { + // model: Quickshell.screens + + // delegate: Overview { + // modelData: item + // } + // } DemoPanel { id: demoPanel From a7e17c25ef6aff6b123a125c85ea0344021cd659 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 10 Aug 2025 13:52:58 +0200 Subject: [PATCH 039/394] Add NLoader --- Modules/Bar/Bar.qml | 4 +- Modules/DemoPanel/DemoPanel.qml | 139 +++++++++++++------------------- Widgets/NLoader.qml | 31 +++++++ Widgets/NPanel.qml | 2 + shell.qml | 16 ---- 5 files changed, 91 insertions(+), 101 deletions(-) create mode 100644 Widgets/NLoader.qml diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index fa14d46..dc3e593 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -79,9 +79,7 @@ PanelWindow { NIconButton { id: demoPanelToggler icon: "experiment" - onClicked: function () { - demoPanel.visible ? demoPanel.hide() : demoPanel.show() - } + onClicked: function () { demoPanel.isLoaded = !demoPanel.isLoaded } } } } diff --git a/Modules/DemoPanel/DemoPanel.qml b/Modules/DemoPanel/DemoPanel.qml index c758c16..62c3959 100644 --- a/Modules/DemoPanel/DemoPanel.qml +++ b/Modules/DemoPanel/DemoPanel.qml @@ -10,100 +10,75 @@ import qs.Widgets An experiment/demo panel to tweaks widgets */ - -NPanel { +NLoader { id: root - readonly property real scaling: Scaling.scale(screen) + panel: Component { + NPanel { + id: demoPanel - Rectangle { - color: Colors.backgroundPrimary - radius: Style.radiusMedium * scaling - border.color: Colors.backgroundTertiary - border.width: Math.min(1, Style.borderMedium * scaling) - width: 500 * scaling - height: 400 - anchors.centerIn: parent + readonly property real scaling: Scaling.scale(screen) + // Ensure panel shows itself once created + Component.onCompleted: show() - // Prevent closing when clicking in the panel bg - MouseArea { - anchors.fill: parent - } + Rectangle { + color: Colors.backgroundPrimary + radius: Style.radiusMedium * scaling + border.color: Colors.backgroundTertiary + border.width: Math.min(1, Style.borderMedium * scaling) + width: 500 * scaling + height: 400 + anchors.centerIn: parent - ColumnLayout { - anchors.fill: parent - anchors.margins: Style.marginXL * scaling - spacing: Style.marginSmall * scaling + // Prevent closing when clicking in the panel bg + MouseArea { anchors.fill: parent } - // NIconButton - ColumnLayout { - spacing: 16 * scaling - NText { - text: "NIconButton + NTooltip" - color: Colors.accentSecondary - } + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginXL * scaling + spacing: Style.marginSmall * scaling - NIconButton { - id: myIconButton - icon: "refresh" - onEntered: function() { - myTooltip.show(); + // NIconButton + ColumnLayout { + spacing: 16 * scaling + NText { text: "NIconButton"; color: Colors.accentSecondary } + + NIconButton { + id: myIconButton + icon: "refresh" + onEntered: function() { myTooltip.show() } + onExited: function() { myTooltip.hide() } + } + + NDivider { Layout.fillWidth: true } } - onExited: function() { - myTooltip.hide(); + + // NToggle + ColumnLayout { + spacing: Style.marginLarge * scaling + uniformCellSizes: true + NText { text: "NToggle + NTooltip"; color: Colors.accentSecondary } + + NToggle { + label: "Label" + description: "Description" + onToggled: function(value: bool) { console.log("NToggle: " + value) } + } + + NTooltip { id: myTooltip; target: myIconButton; positionAbove: false; text: "Hello world" } + NDivider { Layout.fillWidth: true } + } + + // NSlider + ColumnLayout { + spacing: 16 * scaling + NText { text: "NSlider"; color: Colors.accentSecondary } + NSlider {} + NDivider { Layout.fillWidth: true } } } - - NTooltip { - id: myTooltip - target: myIconButton - positionAbove: false - text: "Hello world" - } - - NDivider {Layout.fillWidth: true} } - - - // NToggle - ColumnLayout { - spacing: Style.marginLarge * scaling - uniformCellSizes: true - NText { - text: "NToggle" - color: Colors.accentSecondary - } - - NToggle { - label: "Label" - description: "Description" - onToggled: function(value: bool) { - console.log("NToggle: " + value) - } - } - - - NDivider { - Layout.fillWidth: true - } - } - - // NSlider - ColumnLayout { - spacing: 16 * scaling - - NText { - text: "NSlider" - color: Colors.accentSecondary - } - - NSlider {} - NDivider { - Layout.fillWidth: true - } - } - } } } diff --git a/Widgets/NLoader.qml b/Widgets/NLoader.qml new file mode 100644 index 0000000..807d02f --- /dev/null +++ b/Widgets/NLoader.qml @@ -0,0 +1,31 @@ +import QtQuick + +Loader { + id: loader + + // Boolean control to load/unload the item + property bool isLoaded: false + + // Provide the component to load. + // Example usage: + // content: Component { NPanel { /* ... */ } } + property Component panel + + active: isLoaded + asynchronous: true + sourceComponent: panel + + onActiveChanged: { + if (active && item && item.show) item.show() + } + + onItemChanged: { + if (active && item && item.show) item.show() + } + + Connections { + target: loader.item + function onDismissed() { loader.isLoaded = false } + } +} + diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index ac539ee..d912508 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -10,9 +10,11 @@ PanelWindow { property bool showOverlay: Settings.settings.dimPanels property int topMargin: Style.barHeight * scaling property color overlayColor: showOverlay ? Colors.overlay : "transparent" + signal dismissed() function hide() { visible = false + dismissed() } function show() { diff --git a/shell.qml b/shell.qml index 1cdc7ad..09568fc 100644 --- a/shell.qml +++ b/shell.qml @@ -24,22 +24,6 @@ ShellRoot { Background {} Overview {} - // Variants { - // model: Quickshell.screens - - // delegate: Background { - // modelData: item - // } - // } - - // Variants { - // model: Quickshell.screens - - // delegate: Overview { - // modelData: item - // } - // } - DemoPanel { id: demoPanel } From b4702ef0703745afd5fd523acd214b3a285085f2 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 08:00:34 -0400 Subject: [PATCH 040/394] Workspaces: Error fix. --- Modules/Bar/Workspace.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Bar/Workspace.qml b/Modules/Bar/Workspace.qml index fa7a2de..2bcda02 100644 --- a/Modules/Bar/Workspace.qml +++ b/Modules/Bar/Workspace.qml @@ -48,7 +48,7 @@ Item { Component.onCompleted: { localWorkspaces.clear(); for (let i = 0; i < Workspaces.workspaces.count; i++) { - const ws = Services.Workspaces.workspaces.get(i); + const ws = Workspaces.workspaces.get(i); if (ws.output.toLowerCase() === screen.name.toLowerCase()) { localWorkspaces.append(ws); } From ff7dff8a6d097e9f50b0a3d9ad66b977aac78735 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 08:11:45 -0400 Subject: [PATCH 041/394] Wallust files structure renamed quickshell.json to noctalia.json --- Assets/Wallust/Templates/kitty.conf | 29 +++ Assets/Wallust/Templates/niri.kdl | 292 +++++++++++++++++++++++++ Assets/Wallust/Templates/noctalia.json | 28 +++ Assets/Wallust/wallust.toml | 47 ++++ 4 files changed, 396 insertions(+) create mode 100644 Assets/Wallust/Templates/kitty.conf create mode 100644 Assets/Wallust/Templates/niri.kdl create mode 100644 Assets/Wallust/Templates/noctalia.json create mode 100644 Assets/Wallust/wallust.toml diff --git a/Assets/Wallust/Templates/kitty.conf b/Assets/Wallust/Templates/kitty.conf new file mode 100644 index 0000000..72fb6f4 --- /dev/null +++ b/Assets/Wallust/Templates/kitty.conf @@ -0,0 +1,29 @@ +# The kitty terminal template for wallust +# Add to wallust config: kitty = { src='kitty.conf', dst='~/.config/kitty/colors.conf'} +# And add to kitty config: include colors.conf + +cursor {{ cursor }} + +background {{ background }} +foreground {{ foreground }} + +color0 {{ color0 }} +color1 {{ color1 }} +color2 {{ color2 }} +color3 {{ color3 }} +color4 {{ color4 }} +color5 {{ color5 }} +color6 {{ color6 }} +color7 {{ color7 }} +color8 {{ color8 }} +color9 {{ color9 }} +color10 {{ color10 }} +color11 {{ color11 }} +color12 {{ color12 }} +color13 {{ color13 }} +color14 {{ color14 }} +color15 {{ color15 }} + +mark1_foreground {{ color6 | saturate(0.2) }} +mark2_foreground {{ color7 | saturate(0.2) }} +mark3_foreground {{ color6 | saturate(0.2) }} \ No newline at end of file diff --git a/Assets/Wallust/Templates/niri.kdl b/Assets/Wallust/Templates/niri.kdl new file mode 100644 index 0000000..cd99b52 --- /dev/null +++ b/Assets/Wallust/Templates/niri.kdl @@ -0,0 +1,292 @@ +// Niri configuration for CachyOS +// For documentation and full reference, see: https://github.com/YaLTeR/niri/wiki + +// ────────────── Input Configuration ────────────── +// https://github.com/YaLTeR/niri/wiki/Configuration:-Input + +input { + keyboard { + xkb { + layout "de" // Use the German keyboard layout + } + numlock // Enable numlock on startup + } + + touchpad { + tap // Enable tap-to-click + natural-scroll // Enable natural (macOS-style) scrolling + } + + focus-follows-mouse // Automatically focus windows under the mouse pointer + workspace-auto-back-and-forth // Enable workspace back & forth switching +} + +// ────────────── Output Configuration ────────────── +// You can run `niri msg outputs` to get the correct name for your displays. +// You will have to remove "/-" and edit it before it takes effect. +// https://github.com/YaLTeR/niri/wiki/Configuration:-Outputs + + output "DP-1" { + mode "2560x1440@359.979" // Set resolution and refresh rate + scale 1 // No scaling (use 2 for HiDPI) +} + +// ────────────── Keybindings ────────────── +// https://github.com/YaLTeR/niri/wiki/Configuration:-Key-Bindings + +binds { + MOD+SHIFT+ESCAPE { show-hotkey-overlay; } + + // ─── Applications ─── + MOD+RETURN hotkey-overlay-title="Open Terminal: Kitty" { spawn "kitty"; } + MOD+CTRL+RETURN hotkey-overlay-title="Open App Launcher: QS" { spawn "qs" "ipc" "call" "globalIPC" "toggleLauncher"; } + MOD+B hotkey-overlay-title="Open Browser: firefox" { spawn "firefox"; } + MOD+ALT+L hotkey-overlay-title="Lock Screen: swaylock" { spawn "swaylock"; } + + // Please choose your own file manager + MOD+E hotkey-overlay-title="File Manager: Nautilus" { spawn "nautilus"; } + + // ─── Audio Controls ─── + XF86AUDIORAISEVOLUME allow-when-locked=true { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1+"; } + XF86AUDIOLOWERVOLUME allow-when-locked=true { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1-"; } + XF86AUDIOMUTE allow-when-locked=true { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SINK@" "toggle"; } + XF86AUDIOMICMUTE allow-when-locked=true { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SOURCE@" "toggle"; } + + // ─── Window Movement and Focus ─── + MOD+Q { close-window; } + + MOD+LEFT { focus-column-left; } + MOD+H { focus-column-left; } + MOD+RIGHT { focus-column-right; } + MOD+L { focus-column-right; } + MOD+UP { focus-window-up; } + MOD+K { focus-window-up; } + MOD+DOWN { focus-window-down; } + MOD+J { focus-window-down; } + + MOD+CTRL+LEFT { move-column-left; } + MOD+CTRL+H { move-column-left; } + MOD+CTRL+RIGHT { move-column-right; } + MOD+CTRL+L { move-column-right; } + MOD+CTRL+UP { move-window-up; } + MOD+CTRL+K { move-window-up; } + MOD+CTRL+DOWN { move-window-down; } + MOD+CTRL+J { move-window-down; } + + MOD+HOME { focus-column-first; } + MOD+END { focus-column-last; } + MOD+CTRL+HOME { move-column-to-first; } + MOD+CTRL+END { move-column-to-last; } + + MOD+SHIFT+LEFT { focus-monitor-left; } + MOD+SHIFT+RIGHT { focus-monitor-right; } + MOD+SHIFT+UP { focus-monitor-up; } + MOD+SHIFT+DOWN { focus-monitor-down; } + + MOD+SHIFT+CTRL+LEFT { move-column-to-monitor-left; } + MOD+SHIFT+CTRL+RIGHT { move-column-to-monitor-right; } + MOD+SHIFT+CTRL+UP { move-column-to-monitor-up; } + MOD+SHIFT+CTRL+DOWN { move-column-to-monitor-down; } + + // ─── Workspace Switching ─── + MOD+WHEELSCROLLDOWN cooldown-ms=150 { focus-workspace-down; } + MOD+WHEELSCROLLUP cooldown-ms=150 { focus-workspace-up; } + MOD+CTRL+WHEELSCROLLDOWN cooldown-ms=150 { move-column-to-workspace-down; } + MOD+CTRL+WHEELSCROLLUP cooldown-ms=150 { move-column-to-workspace-up; } + + MOD+WHEELSCROLLRIGHT { focus-column-right; } + MOD+WHEELSCROLLLEFT { focus-column-left; } + MOD+CTRL+WHEELSCROLLRIGHT { move-column-right; } + MOD+CTRL+WHEELSCROLLLEFT { move-column-left; } + + MOD+SHIFT+WHEELSCROLLDOWN { focus-column-right; } + MOD+SHIFT+WHEELSCROLLUP { focus-column-left; } + MOD+CTRL+SHIFT+WHEELSCROLLDOWN { move-column-right; } + MOD+CTRL+SHIFT+WHEELSCROLLUP { move-column-left; } + + MOD+1 { focus-workspace 1; } + MOD+2 { focus-workspace 2; } + MOD+3 { focus-workspace 3; } + MOD+4 { focus-workspace 4; } + MOD+5 { focus-workspace 5; } + MOD+6 { focus-workspace 6; } + MOD+7 { focus-workspace 7; } + MOD+8 { focus-workspace 8; } + MOD+9 { focus-workspace 9; } + + MOD+CTRL+1 { move-column-to-workspace 1; } + MOD+CTRL+2 { move-column-to-workspace 2; } + MOD+CTRL+3 { move-column-to-workspace 3; } + MOD+CTRL+4 { move-column-to-workspace 4; } + MOD+CTRL+5 { move-column-to-workspace 5; } + MOD+CTRL+6 { move-column-to-workspace 6; } + MOD+CTRL+7 { move-column-to-workspace 7; } + MOD+CTRL+8 { move-column-to-workspace 8; } + MOD+CTRL+9 { move-column-to-workspace 9; } + + MOD+TAB { focus-workspace-previous; } + + // ─── Layout Controls ─── + MOD+CTRL+F { expand-column-to-available-width; } + MOD+C { center-column; } + MOD+CTRL+C { center-visible-columns; } + MOD+MINUS { set-column-width "-10%"; } + MOD+EQUAL { set-column-width "+10%"; } + MOD+SHIFT+MINUS { set-window-height "-10%"; } + MOD+SHIFT+EQUAL { set-window-height "+10%"; } + + // ─── Modes ─── + MOD+T { toggle-window-floating; } + MOD+F { fullscreen-window; } + MOD+W { toggle-column-tabbed-display; } + + // ─── Screenshots ─── + CTRL+SHIFT+1 { screenshot; } + CTRL+SHIFT+2 { screenshot-screen; } + CTRL+SHIFT+3 { screenshot-window; } + + // ─── Emergency Escape Key ─── + // Use this when a fullscreen app blocks your keybinds. + // It disables any active keyboard shortcut inhibitor, restoring control. + MOD+ESCAPE allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; } + + // ─── Exit / Power ─── + CTRL+ALT+DELETE { quit; } // Also quits Niri + MOD+SHIFT+P { power-off-monitors; } // Turn off screens (useful for OLED or privacy) + MOD+O repeat=false { toggle-overview; } +} + +// ────────────── Startup Applications ────────────── +// https://github.com/YaLTeR/niri/wiki/Configuration:-Miscellaneous#spawn-at-startup + + spawn-at-startup "/usr/lib/polkit-kde-authentication-agent-1" "&" // Polkit + spawn-at-startup "xwayland-satellite" // XWayland support + spawn-at-startup "swww-daemon" // Wallpaper daemon + spawn-at-startup "swww img" "/usr/share/wallpapers/cachyos-wallpapers/Skyscraper.png" // Set wallpaper + spawn-at-startup "qs" // Launch Quickshell + spawn-at-startup "vesktop" // Launch Vesktop + + prefer-no-csd // Disable program decorations + screenshot-path null // Disable screenshot saving + +// ────────────── Layout Settings ────────────── +// https://github.com/YaLTeR/niri/wiki/Configuration:-Layout + + layout { + gaps 16 // Gap between windows + center-focused-column "never" // Don’t auto-center focused column + + preset-column-widths { + proportion 0.33333 + proportion 0.5 + proportion 0.66667 + } + + focus-ring { + width 3 + active-color "{{ color4 }}" + inactive-color "{{ color0 }}" + } + + shadow { + softness 30 + spread 5 + offset x=0 y=5 + color "#0007" + } + + background-color "transparent" + + struts {} + } + +// ────────────── Animation Settings ────────────── +// https://github.com/YaLTeR/niri/wiki/Configuration:-Animations + animations { + workspace-switch { + spring damping-ratio=1.0 stiffness=1000 epsilon=0.0001 + } + window-open { + duration-ms 200 + curve "ease-out-quad" + } + window-close { + duration-ms 200 + curve "ease-out-cubic" + } + horizontal-view-movement { + spring damping-ratio=1.0 stiffness=900 epsilon=0.0001 + } + window-movement { + spring damping-ratio=1.0 stiffness=800 epsilon=0.0001 + } + window-resize { + spring damping-ratio=1.0 stiffness=1000 epsilon=0.0001 + } + config-notification-open-close { + spring damping-ratio=0.6 stiffness=1200 epsilon=0.001 + } + screenshot-ui-open { + duration-ms 300 + curve "ease-out-quad" + } + overview-open-close { + spring damping-ratio=1.0 stiffness=900 epsilon=0.0001 + } + } + +// ────────────── Named Workspaces ────────────── +// https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules + + workspace "browser" + workspace "chat" + +// ────────────── Window Rules ────────────── +// https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules + + window-rule { + match at-startup=true app-id="vesktop" + open-on-workspace "chat" + open-maximized true + } + + window-rule { + match app-id="firefox" + open-on-workspace "browser" + open-maximized true + } + + window-rule { + match app-id=r#"firefox$"# title="^Picture-in-Picture$" + open-floating true // Always float Firefox PiP windows + } + + window-rule { + geometry-corner-radius 20 // Set every window radius to 20 + clip-to-geometry true + } + +// ────────────── Layer Rules ────────────── +// https://github.com/YaLTeR/niri/wiki/Configuration:-Layer-Rules + + layer-rule { + match namespace="^swww-daemon$" + place-within-backdrop true + } + +// ────────────── Environment Variables ────────────── +// https://github.com/YaLTeR/niri/wiki/Configuration:-Miscellaneous#environment + + environment { + DISPLAY ":1" + ELECTRON_OZONE_PLATFORM_HINT "auto" + QT_QPA_PLATFORM "wayland" + QT_WAYLAND_DISABLE_WINDOWDECORATION "1" + XDG_SESSION_TYPE "wayland" + XDG_CURRENT_DESKTOP "niri" + } + +// ────────────── Misc ────────────── +hotkey-overlay { + skip-at-startup +} \ No newline at end of file diff --git a/Assets/Wallust/Templates/noctalia.json b/Assets/Wallust/Templates/noctalia.json new file mode 100644 index 0000000..22da0f5 --- /dev/null +++ b/Assets/Wallust/Templates/noctalia.json @@ -0,0 +1,28 @@ +{ + "backgroundPrimary": "{{ background }}", + "backgroundSecondary": "{{ background | lighten(0.05) }}", + "backgroundTertiary": "{{ background | lighten(0.1) }}", + + "surface": "{{ background | lighten(0.08) }}", + "surfaceVariant": "{{ background | lighten(0.15) }}", + + "textPrimary": "{{ foreground }}", + "textSecondary": "{{ foreground | darken(0.1) }}", + "textDisabled": "{{ foreground | darken(0.4) }}", + + "accentPrimary": "{{ color4 }}", + "accentSecondary": "{{ color4 | lighten(0.2) }}", + "accentTertiary": "{{ color4 | darken(0.2) }}", + + "error": "{{ color5 | lighten(0.1) }}", + "warning": "{{ color5 | lighten(0.3) }}", + + "highlight": "{{ color4 | lighten(0.4) }}", + "rippleEffect": "{{ color4 | lighten(0.1) }}", + + "onAccent": "{{ background }}", + "outline": "{{ background | lighten(0.3) }}", + + "shadow": "{{ background }}", + "overlay": "{{ background }}" +} \ No newline at end of file diff --git a/Assets/Wallust/wallust.toml b/Assets/Wallust/wallust.toml new file mode 100644 index 0000000..e50c5bb --- /dev/null +++ b/Assets/Wallust/wallust.toml @@ -0,0 +1,47 @@ +# wallust v3.3 +# +# You can copy this file to ~/.config/wallust/wallust.toml (keep in mind is a sample config) + +# SIMPLE TUTORIAL, or `man wallust.5`: +# https://explosion-mental.codeberg.page/wallust/ +# +# If comming from v2: https://explosion-mental.codeberg.page/wallust/v3.html#wallusttoml + +# Global section - values below can be overwritten by command line flags + +# How the image is parse, in order to get the colors: +# full - resized - wal - thumb - fastresize - kmeans +backend = "resized" + +# What color space to use to produce and select the most prominent colors: +# lab - labmixed - lch - lchmixed +color_space = "labmixed" + +# Use the most prominent colors in a way that makes sense, a scheme color palette: +# dark - dark16 - darkcomp - darkcomp16 +# light - light16 - lightcomp - lightcomp16 +# harddark - harddark16 - harddarkcomp - harddarkcomp16 +# softdark - softdark16 - softdarkcomp - softdarkcomp16 +# softlight - softlight16 - softlightcomp - softlightcomp16 +palette = "dark" + +# Ensures a "readable contrast" (OPTIONAL, disabled by default) +# Should only be enabled when you notice an unreadable contrast frequently happening +# with your images. The reference color for the contrast is the background color. +check_contrast = true + +# Color saturation, between [1% and 100%] (OPTIONAL, disabled by default) +# usually something higher than 50 increases the saturation and below +# decreases it (on a scheme with strong and vivid colors) +#saturation = 50 + +# Alpha value for templating, by default 100 (no other use whatsoever) +#alpha = 100 + +[templates] +# NOTE: prefer '' over "" for paths, avoids escaping. +# template: A RELATIVE path that points to `~/.config/wallust/template` (depends on platform) +# target: ABSOLUTE path in which to place a file with generated templated values. +# ¡ If either one is a directory, then both SHOULD be one. ! +# zathura = { template = 'zathura', target = '~/.config/zathura/zathurarc' } +Quickshell = { template = 'quickshell.json', target = '~/.config/Noctalia/Theme.json' } \ No newline at end of file From 0c044c7b81b19f78c8cf4dc99a0b7728cb052ffa Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 08:13:58 -0400 Subject: [PATCH 042/394] qmlformat --- Modules/Background/Background.qml | 69 +++-- Modules/Background/Overview.qml | 87 +++--- Modules/Bar/Bar.qml | 8 +- Modules/Bar/Clock.qml | 4 +- Modules/Bar/Workspace.qml | 485 +++++++++++++++--------------- Widgets/NClock.qml | 2 +- Widgets/NLoader.qml | 11 +- Widgets/NPanel.qml | 2 +- shell.qml | 3 +- 9 files changed, 338 insertions(+), 333 deletions(-) diff --git a/Modules/Background/Background.qml b/Modules/Background/Background.qml index c587bbe..2756aed 100644 --- a/Modules/Background/Background.qml +++ b/Modules/Background/Background.qml @@ -4,42 +4,41 @@ import Quickshell.Wayland import qs.Services ShellRoot { - - property var modelData - property string wallpaperSource: Qt.resolvedUrl("../../Assets/Tests/wallpaper.png") - Variants { - model: Quickshell.screens + property var modelData + property string wallpaperSource: Qt.resolvedUrl( + "../../Assets/Tests/wallpaper.png") - PanelWindow { - required property ShellScreen modelData + Variants { + model: Quickshell.screens - visible: wallpaperSource !== "" - anchors { - bottom: true - top: true - right: true - left: true - } - margins { - top: 0 - } - color: "transparent" - screen: modelData - WlrLayershell.layer: WlrLayer.Background - WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.namespace: "quickshell-wallpaper" - Image { - anchors.fill: parent - fillMode: Image.PreserveAspectCrop - source: wallpaperSource - visible: wallpaperSource !== "" - cache: true - smooth: true - mipmap: false - } - } + PanelWindow { + required property ShellScreen modelData + + visible: wallpaperSource !== "" + anchors { + bottom: true + top: true + right: true + left: true + } + margins { + top: 0 + } + color: "transparent" + screen: modelData + WlrLayershell.layer: WlrLayer.Background + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.namespace: "quickshell-wallpaper" + Image { + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + source: wallpaperSource + visible: wallpaperSource !== "" + cache: true + smooth: true + mipmap: false + } } - - -} \ No newline at end of file + } +} diff --git a/Modules/Background/Overview.qml b/Modules/Background/Overview.qml index 2bbc85b..0179aad 100644 --- a/Modules/Background/Overview.qml +++ b/Modules/Background/Overview.qml @@ -5,52 +5,51 @@ import Quickshell.Wayland import qs.Services ShellRoot { - property string wallpaperSource: Qt.resolvedUrl("../../Assets/Tests/wallpaper.png") - property var modelData + property string wallpaperSource: Qt.resolvedUrl( + "../../Assets/Tests/wallpaper.png") + property var modelData - Variants { - model: Quickshell.screens + Variants { + model: Quickshell.screens - PanelWindow { - required property ShellScreen modelData + PanelWindow { + required property ShellScreen modelData - visible: wallpaperSource !== "" - anchors { - top: true - bottom: true - right: true - left: true - } - color: "transparent" - screen: modelData - WlrLayershell.layer: WlrLayer.Background - WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.namespace: "quickshell-overview" - Image { - id: bgImage - anchors.fill: parent - fillMode: Image.PreserveAspectCrop - source: wallpaperSource - cache: true - smooth: true - mipmap: false - visible: wallpaperSource !== "" - } - MultiEffect { - id: overviewBgBlur - anchors.fill: parent - source: bgImage - blurEnabled: true - blur: 0.48 - blurMax: 128 - } - Rectangle { - anchors.fill: parent - color: Qt.rgba( - Colors.backgroundPrimary.r, - Colors.backgroundPrimary.g, - Colors.backgroundPrimary.b, 0.5) - } - } + visible: wallpaperSource !== "" + anchors { + top: true + bottom: true + right: true + left: true + } + color: "transparent" + screen: modelData + WlrLayershell.layer: WlrLayer.Background + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.namespace: "quickshell-overview" + Image { + id: bgImage + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + source: wallpaperSource + cache: true + smooth: true + mipmap: false + visible: wallpaperSource !== "" + } + MultiEffect { + id: overviewBgBlur + anchors.fill: parent + source: bgImage + blurEnabled: true + blur: 0.48 + blurMax: 128 + } + Rectangle { + anchors.fill: parent + color: Qt.rgba(Colors.backgroundPrimary.r, Colors.backgroundPrimary.g, + Colors.backgroundPrimary.b, 0.5) + } } + } } diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index dc3e593..88b3ced 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -14,7 +14,8 @@ PanelWindow { screen: modelData implicitHeight: Style.barHeight * scaling color: "transparent" - visible: Settings.settings.barMonitors.includes(modelData.name) || (Settings.settings.barMonitors.length === 0) + visible: Settings.settings.barMonitors.includes(modelData.name) + || (Settings.settings.barMonitors.length === 0) anchors { top: true @@ -56,7 +57,6 @@ PanelWindow { spacing: Style.marginMedium * scaling Workspace {} - } Row { @@ -79,7 +79,9 @@ PanelWindow { NIconButton { id: demoPanelToggler icon: "experiment" - onClicked: function () { demoPanel.isLoaded = !demoPanel.isLoaded } + onClicked: function () { + demoPanel.isLoaded = !demoPanel.isLoaded + } } } } diff --git a/Modules/Bar/Clock.qml b/Modules/Bar/Clock.qml index bc1215a..79fda94 100644 --- a/Modules/Bar/Clock.qml +++ b/Modules/Bar/Clock.qml @@ -17,12 +17,12 @@ NClock { visible: false } - onEntered: function (){ + onEntered: function () { if (!calendar.visible) { tooltip.show() } } - onExited: function (){ + onExited: function () { tooltip.hide() } onClicked: function () { diff --git a/Modules/Bar/Workspace.qml b/Modules/Bar/Workspace.qml index 2bcda02..43f9e32 100644 --- a/Modules/Bar/Workspace.qml +++ b/Modules/Bar/Workspace.qml @@ -8,260 +8,263 @@ import Quickshell.Io import qs.Services Item { - id: root - property bool isDestroying: false - property bool hovered: false + id: root + property bool isDestroying: false + property bool hovered: false - readonly property real scaling: Scaling.scale(screen) - property var modelData + readonly property real scaling: Scaling.scale(screen) + property var modelData - signal workspaceChanged(int workspaceId, color accentColor) + signal workspaceChanged(int workspaceId, color accentColor) - property ListModel localWorkspaces: ListModel {} - property real masterProgress: 0.0 - property bool effectsActive: false - property color effectColor: Colors.accentPrimary + property ListModel localWorkspaces: ListModel {} + property real masterProgress: 0.0 + property bool effectsActive: false + property color effectColor: Colors.accentPrimary - // Unified scale - property real s: scale - property int horizontalPadding: Math.round(16 * s) - property int spacingBetweenPills: Math.round(8 * s) + // Unified scale + property real s: scale + property int horizontalPadding: Math.round(16 * s) + property int spacingBetweenPills: Math.round(8 * s) - width: { - let total = 0; - for (let i = 0; i < localWorkspaces.count; i++) { - const ws = localWorkspaces.get(i); - if (ws.isFocused) - total += Math.round(44 * s); - else if (ws.isActive) - total += Math.round(28 * s); + width: { + let total = 0 + for (var i = 0; i < localWorkspaces.count; i++) { + const ws = localWorkspaces.get(i) + if (ws.isFocused) + total += Math.round(44 * s) + else if (ws.isActive) + total += Math.round(28 * s) + else + total += Math.round(16 * s) + } + total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills + total += horizontalPadding * 2 + return total + } + + height: Math.round(36 * s) + + Component.onCompleted: { + localWorkspaces.clear() + for (var i = 0; i < Workspaces.workspaces.count; i++) { + const ws = Workspaces.workspaces.get(i) + if (ws.output.toLowerCase() === screen.name.toLowerCase()) { + localWorkspaces.append(ws) + } + } + workspaceRepeater.model = localWorkspaces + updateWorkspaceFocus() + } + + Connections { + target: Workspaces + function onWorkspacesChanged() { + localWorkspaces.clear() + for (var i = 0; i < Workspaces.workspaces.count; i++) { + const ws = Workspaces.workspaces.get(i) + if (ws.output.toLowerCase() === screen.name.toLowerCase()) { + localWorkspaces.append(ws) + } + } + + workspaceRepeater.model = localWorkspaces + updateWorkspaceFocus() + } + } + + function triggerUnifiedWave() { + effectColor = Colors.accentPrimary + masterAnimation.restart() + } + + SequentialAnimation { + id: masterAnimation + PropertyAction { + target: root + property: "effectsActive" + value: true + } + NumberAnimation { + target: root + property: "masterProgress" + from: 0.0 + to: 1.0 + duration: 1000 + easing.type: Easing.OutQuint + } + PropertyAction { + target: root + property: "effectsActive" + value: false + } + PropertyAction { + target: root + property: "masterProgress" + value: 0.0 + } + } + + function updateWorkspaceFocus() { + for (var i = 0; i < localWorkspaces.count; i++) { + const ws = localWorkspaces.get(i) + if (ws.isFocused === true) { + root.triggerUnifiedWave() + root.workspaceChanged(ws.id, Colors.accentPrimary) + break + } + } + } + + Rectangle { + id: workspaceBackground + width: parent.width - Math.round(15 * s) + height: Math.round(26 * s) + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + radius: Math.round(12 * s) + color: Colors.surfaceVariant + border.color: Qt.rgba(Colors.textPrimary.r, Colors.textPrimary.g, + Colors.textPrimary.b, 0.1) + border.width: Math.max(1, Math.round(1 * s)) + layer.enabled: true + layer.effect: MultiEffect { + shadowColor: "black" + + // radius: 12 + shadowVerticalOffset: 0 + shadowHorizontalOffset: 0 + shadowOpacity: 0.10 + } + } + + Row { + id: pillRow + spacing: spacingBetweenPills + anchors.verticalCenter: workspaceBackground.verticalCenter + width: root.width - horizontalPadding * 2 + x: horizontalPadding + Repeater { + id: workspaceRepeater + model: localWorkspaces + Item { + id: workspacePillContainer + height: Math.round(12 * s) + width: { + if (model.isFocused) + return Math.round(44 * s) + else if (model.isActive) + return Math.round(28 * s) + else + return Math.round(16 * s) + } + + Rectangle { + id: workspacePill + anchors.fill: parent + radius: { + if (model.isFocused) + return Math.round(12 * s) else - total += Math.round(16 * s); - } - total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills; - total += horizontalPadding * 2; - return total; - } + // half of focused height (if you want to animate this too) + return Math.round(6 * s) + } + color: { + if (model.isFocused) + return Colors.accentPrimary + if (model.isUrgent) + return Colors.error + if (model.isActive || model.isOccupied) + return Colors.accentTertiary + if (model.isUrgent) + return Colors.error - height: Math.round(36 * s) + return Colors.outline + } + scale: model.isFocused ? 1.0 : 0.9 + z: 0 - Component.onCompleted: { - localWorkspaces.clear(); - for (let i = 0; i < Workspaces.workspaces.count; i++) { - const ws = Workspaces.workspaces.get(i); - if (ws.output.toLowerCase() === screen.name.toLowerCase()) { - localWorkspaces.append(ws); + MouseArea { + id: pillMouseArea + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + Workspaces.switchToWorkspace(model.idx) } - } - workspaceRepeater.model = localWorkspaces; - updateWorkspaceFocus(); - } - - Connections { - target: Workspaces - function onWorkspacesChanged() { - localWorkspaces.clear(); - for (let i = 0; i < Workspaces.workspaces.count; i++) { - const ws = Workspaces.workspaces.get(i); - if (ws.output.toLowerCase() === screen.name.toLowerCase()) { - localWorkspaces.append(ws); - } + hoverEnabled: true + } + // Material 3-inspired smooth animation for width, height, scale, color, opacity, and radius + Behavior on width { + NumberAnimation { + duration: 350 + easing.type: Easing.OutBack } - - workspaceRepeater.model = localWorkspaces; - updateWorkspaceFocus(); - } - } - - function triggerUnifiedWave() { - effectColor = Colors.accentPrimary; - masterAnimation.restart(); - } - - SequentialAnimation { - id: masterAnimation - PropertyAction { - target: root - property: "effectsActive" - value: true - } - NumberAnimation { - target: root - property: "masterProgress" - from: 0.0 - to: 1.0 - duration: 1000 - easing.type: Easing.OutQuint - } - PropertyAction { - target: root - property: "effectsActive" - value: false - } - PropertyAction { - target: root - property: "masterProgress" - value: 0.0 - } - } - - function updateWorkspaceFocus() { - for (let i = 0; i < localWorkspaces.count; i++) { - const ws = localWorkspaces.get(i); - if (ws.isFocused === true) { - root.triggerUnifiedWave(); - root.workspaceChanged(ws.id, Colors.accentPrimary); - break; + } + Behavior on height { + NumberAnimation { + duration: 350 + easing.type: Easing.OutBack } - } - } - - Rectangle { - id: workspaceBackground - width: parent.width - Math.round(15 * s) - height: Math.round(26 * s) - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - radius: Math.round(12 * s) - color: Colors.surfaceVariant - border.color: Qt.rgba(Colors.textPrimary.r, Colors.textPrimary.g, Colors.textPrimary.b, 0.1) - border.width: Math.max(1, Math.round(1 * s)) - layer.enabled: true - layer.effect: MultiEffect { - shadowColor: "black" - // radius: 12 - - shadowVerticalOffset: 0 - shadowHorizontalOffset: 0 - shadowOpacity: 0.10 - } - } - - Row { - id: pillRow - spacing: spacingBetweenPills - anchors.verticalCenter: workspaceBackground.verticalCenter - width: root.width - horizontalPadding * 2 - x: horizontalPadding - Repeater { - id: workspaceRepeater - model: localWorkspaces - Item { - id: workspacePillContainer - height: Math.round(12 * s) - width: { - if (model.isFocused) - return Math.round(44 * s); - else if (model.isActive) - return Math.round(28 * s); - else - return Math.round(16 * s); - } - - Rectangle { - id: workspacePill - anchors.fill: parent - radius: { - if (model.isFocused) - return Math.round(12 * s); - else - // half of focused height (if you want to animate this too) - return Math.round(6 * s); - } - color: { - if (model.isFocused) - return Colors.accentPrimary; - if (model.isUrgent) - return Colors.error; - if (model.isActive || model.isOccupied) - return Colors.accentTertiary; - if (model.isUrgent) - return Colors.error; - - return Colors.outline; - } - scale: model.isFocused ? 1.0 : 0.9 - z: 0 - - MouseArea { - id: pillMouseArea - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: { - Workspaces.switchToWorkspace(model.idx); - } - hoverEnabled: true - } - // Material 3-inspired smooth animation for width, height, scale, color, opacity, and radius - Behavior on width { - NumberAnimation { - duration: 350 - easing.type: Easing.OutBack - } - } - Behavior on height { - NumberAnimation { - duration: 350 - easing.type: Easing.OutBack - } - } - Behavior on scale { - NumberAnimation { - duration: 300 - easing.type: Easing.OutBack - } - } - Behavior on color { - ColorAnimation { - duration: 200 - easing.type: Easing.InOutCubic - } - } - Behavior on opacity { - NumberAnimation { - duration: 200 - easing.type: Easing.InOutCubic - } - } - Behavior on radius { - NumberAnimation { - duration: 350 - easing.type: Easing.OutBack - } - } - } - - Behavior on width { - NumberAnimation { - duration: 350 - easing.type: Easing.OutBack - } - } - Behavior on height { - NumberAnimation { - duration: 350 - easing.type: Easing.OutBack - } - } - // Burst effect overlay for focused pill (smaller outline) - Rectangle { - id: pillBurst - anchors.centerIn: workspacePillContainer - width: workspacePillContainer.width + 18 * root.masterProgress * scale - height: workspacePillContainer.height + 18 * root.masterProgress * scale - radius: width / 2 - color: "transparent" - border.color: root.effectColor - border.width: Math.max(1, Math.round((2 + 6 * (1.0 - root.masterProgress)) * s)) - opacity: root.effectsActive && model.isFocused ? (1.0 - root.masterProgress) * 0.7 : 0 - visible: root.effectsActive && model.isFocused - z: 1 - } + } + Behavior on scale { + NumberAnimation { + duration: 300 + easing.type: Easing.OutBack } + } + Behavior on color { + ColorAnimation { + duration: 200 + easing.type: Easing.InOutCubic + } + } + Behavior on opacity { + NumberAnimation { + duration: 200 + easing.type: Easing.InOutCubic + } + } + Behavior on radius { + NumberAnimation { + duration: 350 + easing.type: Easing.OutBack + } + } } - } - Component.onDestruction: { - root.isDestroying = true; + Behavior on width { + NumberAnimation { + duration: 350 + easing.type: Easing.OutBack + } + } + Behavior on height { + NumberAnimation { + duration: 350 + easing.type: Easing.OutBack + } + } + // Burst effect overlay for focused pill (smaller outline) + Rectangle { + id: pillBurst + anchors.centerIn: workspacePillContainer + width: workspacePillContainer.width + 18 * root.masterProgress * scale + height: workspacePillContainer.height + 18 * root.masterProgress * scale + radius: width / 2 + color: "transparent" + border.color: root.effectColor + border.width: Math.max(1, Math.round( + (2 + 6 * (1.0 - root.masterProgress)) * s)) + opacity: root.effectsActive + && model.isFocused ? (1.0 - root.masterProgress) * 0.7 : 0 + visible: root.effectsActive && model.isFocused + z: 1 + } + } } + } + + Component.onDestruction: { + root.isDestroying = true + } } diff --git a/Widgets/NClock.qml b/Widgets/NClock.qml index 52ec789..f5ca8f5 100644 --- a/Widgets/NClock.qml +++ b/Widgets/NClock.qml @@ -25,7 +25,7 @@ Rectangle { anchors.fill: parent cursorShape: Qt.PointingHandCursor hoverEnabled: true - onEntered: root.onEntered() + onEntered: root.onEntered() onExited: root.onExited() onClicked: root.onClicked() } diff --git a/Widgets/NLoader.qml b/Widgets/NLoader.qml index 807d02f..f0166c3 100644 --- a/Widgets/NLoader.qml +++ b/Widgets/NLoader.qml @@ -16,16 +16,19 @@ Loader { sourceComponent: panel onActiveChanged: { - if (active && item && item.show) item.show() + if (active && item && item.show) + item.show() } onItemChanged: { - if (active && item && item.show) item.show() + if (active && item && item.show) + item.show() } Connections { target: loader.item - function onDismissed() { loader.isLoaded = false } + function onDismissed() { + loader.isLoaded = false + } } } - diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index d912508..2c9362c 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -10,7 +10,7 @@ PanelWindow { property bool showOverlay: Settings.settings.dimPanels property int topMargin: Style.barHeight * scaling property color overlayColor: showOverlay ? Colors.overlay : "transparent" - signal dismissed() + signal dismissed function hide() { visible = false diff --git a/shell.qml b/shell.qml index 09568fc..03df77d 100644 --- a/shell.qml +++ b/shell.qml @@ -1,6 +1,6 @@ + // Disable reload popup //@ pragma Env QS_NO_RELOAD_POPUP=1 - import QtQuick import Quickshell import Quickshell.Io @@ -12,7 +12,6 @@ import qs.Modules.Background ShellRoot { id: root - Variants { model: Quickshell.screens From ba76e562018e4bf0b226e57c35bec4b5481779e0 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 10 Aug 2025 15:16:50 +0200 Subject: [PATCH 043/394] Add SidePanel base --- Modules/Bar/Bar.qml | 24 +++++++++++++++----- Modules/SidePanel/SidePanel.qml | 40 +++++++++++++++++++++++++++++++++ Widgets/NIconButton.qml | 9 ++++++-- shell.qml | 9 +++++++- 4 files changed, 73 insertions(+), 9 deletions(-) create mode 100644 Modules/SidePanel/SidePanel.qml diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 88b3ced..bb60854 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -39,9 +39,9 @@ PanelWindow { id: leftSection height: parent.height anchors.left: parent.left - anchors.leftMargin: Style.marginMedium * scaling + anchors.leftMargin: Style.marginSmall * scaling anchors.verticalCenter: parent.verticalCenter - spacing: Style.marginMedium * scaling + spacing: Style.marginSmall * scaling NText { text: screen.name @@ -54,7 +54,7 @@ PanelWindow { height: parent.height anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter - spacing: Style.marginMedium * scaling + spacing: Style.marginSmall * scaling Workspace {} } @@ -63,9 +63,9 @@ PanelWindow { id: rightSection height: parent.height anchors.right: bar.right - anchors.rightMargin: Style.marginMedium * scaling + anchors.rightMargin: Style.marginSmall * scaling anchors.verticalCenter: bar.verticalCenter - spacing: Style.marginMedium * scaling + spacing: Style.marginSmall * scaling NText { text: "Right" @@ -77,12 +77,24 @@ PanelWindow { } NIconButton { - id: demoPanelToggler + id: demoPanelToggle icon: "experiment" + fontPointSize: Style.fontSizeMedium + anchors.verticalCenter: parent.verticalCenter onClicked: function () { demoPanel.isLoaded = !demoPanel.isLoaded } } + + NIconButton { + id: sidePanelToggle + icon: "widgets" + fontPointSize: Style.fontSizeMedium + anchors.verticalCenter: parent.verticalCenter + onClicked: function () { + sidePanel.isLoaded = !demoPanel.isLoaded + } + } } } } diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml new file mode 100644 index 0000000..675ebd6 --- /dev/null +++ b/Modules/SidePanel/SidePanel.qml @@ -0,0 +1,40 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import qs.Services +import qs.Widgets + +/* + An experiment/demo panel to tweaks widgets +*/ + +NLoader { + id: root + + panel: Component { + NPanel { + id: sidePanel + + readonly property real scaling: Scaling.scale(screen) + + // Ensure panel shows itself once created + Component.onCompleted: show() + + Rectangle { + color: Colors.backgroundPrimary + radius: Style.radiusMedium * scaling + border.color: Colors.backgroundTertiary + border.width: Math.min(1, Style.borderMedium * scaling) + width: 500 * scaling + height: 400 + anchors.centerIn: parent + + // Prevent closing when clicking in the panel bg + MouseArea { anchors.fill: parent } + + } + } + } +} diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml index 6155345..4603b27 100644 --- a/Widgets/NIconButton.qml +++ b/Widgets/NIconButton.qml @@ -7,13 +7,16 @@ Rectangle { id: root readonly property real scaling: Scaling.scale(screen) - property real size: Style.baseWidgetSize * scaling + // Multiplier to control how large the button container is relative to Style.baseWidgetSize + property real sizeMultiplier: 0.8 + property real size: Style.baseWidgetSize * sizeMultiplier * scaling property string icon property bool enabled: true property bool hovering: false property var onEntered: function () {} property var onExited: function () {} property var onClicked: function () {} + property real fontPointSize: Style.fontSizeXL implicitWidth: size implicitHeight: size @@ -23,9 +26,11 @@ Rectangle { Text { anchors.centerIn: parent + anchors.horizontalCenterOffset: 0 + anchors.verticalCenterOffset: 0 text: root.icon font.family: "Material Symbols Outlined" - font.pointSize: Style.fontSizeXL * scaling + font.pointSize: root.fontPointSize * scaling color: root.hovering ? Colors.onAccent : Colors.textPrimary horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter diff --git a/shell.qml b/shell.qml index 03df77d..5a4e30e 100644 --- a/shell.qml +++ b/shell.qml @@ -1,6 +1,6 @@ - // Disable reload popup //@ pragma Env QS_NO_RELOAD_POPUP=1 + import QtQuick import Quickshell import Quickshell.Io @@ -8,10 +8,13 @@ import Quickshell.Widgets import qs.Modules.Bar import qs.Modules.DemoPanel import qs.Modules.Background +import qs.Modules.SidePanel +import qs.Services ShellRoot { id: root + Variants { model: Quickshell.screens @@ -26,4 +29,8 @@ ShellRoot { DemoPanel { id: demoPanel } + + SidePanel { + id: sidePanel + } } From 2d47c2ed1b547ff68a0af427170e5d9284cfa21a Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 09:18:23 -0400 Subject: [PATCH 044/394] Settings: new json model structure --- Helpers/Holidays.js | 2 +- Modules/Bar/Bar.qml | 4 +- Services/Scaling.qml | 2 +- Services/Settings.qml | 194 +++++++++++++++++++++++++--------------- Services/Time.qml | 4 +- Services/Wallpapers.qml | 34 +++---- Widgets/NPanel.qml | 2 +- Widgets/NText.qml | 2 +- Widgets/NTooltip.qml | 4 +- shell.qml | 1 - 10 files changed, 149 insertions(+), 100 deletions(-) diff --git a/Helpers/Holidays.js b/Helpers/Holidays.js index 4bb3a60..1b67774 100644 --- a/Helpers/Holidays.js +++ b/Helpers/Holidays.js @@ -9,7 +9,7 @@ function getCountryCode(callback) { return; } var xhr = new XMLHttpRequest(); - xhr.open("GET", "https://nominatim.openstreetmap.org/search?city="+ Settings.settings.weatherCity+"&country=&format=json&addressdetails=1&extratags=1", true); + xhr.open("GET", "https://nominatim.openstreetmap.org/search?city="+ Settings.data.location.name+"&country=&format=json&addressdetails=1&extratags=1", true); xhr.onreadystatechange = function() { if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { var response = JSON.parse(xhr.responseText); diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 88b3ced..0983e57 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -14,8 +14,8 @@ PanelWindow { screen: modelData implicitHeight: Style.barHeight * scaling color: "transparent" - visible: Settings.settings.barMonitors.includes(modelData.name) - || (Settings.settings.barMonitors.length === 0) + visible: Settings.data.bar.monitors.includes(modelData.name) + || (Settings.data.bar.monitors.length === 0) anchors { top: true diff --git a/Services/Scaling.qml b/Services/Scaling.qml index d71fef4..7c15215 100644 --- a/Services/Scaling.qml +++ b/Services/Scaling.qml @@ -15,7 +15,7 @@ Singleton { // // 1) Per-monitor override wins // try { - // const overrides = Settings.settings.monitorScaleOverrides || {}; + // const overrides = Settings.data.ui.monitorsScale || {}; // if (currentScreen && currentScreen.name && overrides[currentScreen.name] !== undefined) { // const overrideValue = overrides[currentScreen.name] // if (isFinite(overrideValue)) return overrideValue diff --git a/Services/Settings.qml b/Services/Settings.qml index 37b7a21..3873d99 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -1,22 +1,20 @@ -pragma Singleton - import QtQuick import Quickshell import Quickshell.Io import qs.Services +pragma Singleton Singleton { - - property string shellName: "Noctalia" + property string shellName: "noctalia" property string settingsDir: Quickshell.env("NOCTALIA_SETTINGS_DIR") || (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env( "HOME") + "/.config") + "/" + shellName + "/" property string settingsFile: Quickshell.env("NOCTALIA_SETTINGS_FILE") - || (settingsDir + "Settings.json") + || (settingsDir + "settings.json") property string colorsFile: Quickshell.env("NOCTALIA_COLORS_FILE") - || (settingsDir + "Colors.json") - property var settings: settingAdapter + || (settingsDir + "colors.json") + property var data: settingAdapter Item { Component.onCompleted: { @@ -26,7 +24,14 @@ Singleton { } FileView { + + // TBC ? needed for SWWW only ? + // Qt.callLater(function () { + // WallpaperManager.setCurrentWallpaper(settings.currentWallpaper, true); + // }) + id: settingFileView + path: settingsFile watchChanges: true onFileChanged: reload() @@ -34,80 +39,127 @@ Singleton { Component.onCompleted: function () { reload() } - onLoaded: function () {// Qt.callLater(function () { - // WallpaperManager.setCurrentWallpaper(settings.currentWallpaper, true); - // }) - } + onLoaded: function () {} onLoadFailed: function (error) { - if (error.toString().includes("No such file") || error === 2) { + if (error.toString().includes("No such file") || error === 2) // File doesn't exist, create it with default values writeAdapter() - } } + JsonAdapter { id: settingAdapter - property string weatherCity: "Dinslaken" - property string profileImage: Quickshell.env("HOME") + "/.face" - property bool useFahrenheit: false - property string wallpaperFolder: "/usr/share/wallpapers" - property string currentWallpaper: "" - property string videoPath: "~/Videos/" - property bool showActiveWindow: true - property bool showActiveWindowIcon: false - property bool showSystemInfoInBar: false - property bool showCorners: false - property bool showTaskbar: true - property bool showMediaInBar: false - property bool useSWWW: false - property bool randomWallpaper: false - property bool useWallpaperTheme: false - property int wallpaperInterval: 300 - property string wallpaperResize: "crop" - property int transitionFps: 60 - property string transitionType: "random" - property real transitionDuration: 1.1 - property string visualizerType: "radial" - property bool reverseDayMonth: false - property bool use12HourClock: false - property bool dimPanels: true - property real fontSizeMultiplier: 1.0 // Font size multiplier (1.0 = normal, 1.2 = 20% larger, 0.8 = 20% smaller) - property int taskbarIconSize: 24 // Taskbar icon button size in pixels (default: 32, smaller: 24, larger: 40) - property var pinnedExecs: [] // Added for AppLauncher pinned apps - property bool showDock: true - property bool dockExclusive: false - property bool wifiEnabled: false - property bool bluetoothEnabled: false - property int recordingFrameRate: 60 - property string recordingQuality: "very_high" - property string recordingCodec: "h264" - property string audioCodec: "opus" - property bool showCursor: true - property string colorRange: "limited" + // bar + property JsonObject bar - // Monitor/Display Settings - property var barMonitors: [] // Array of monitor names to show the bar on - property var dockMonitors: [] // Array of monitor names to show the dock on - property var notificationMonitors: [] // Array of monitor names to show notifications on, "*" means all monitors - property var monitorScaleOverrides: { + bar: JsonObject { + property bool showActiveWindow: true + property bool showActiveWindowIcon: false + property bool showSystemInfo: false + property bool showMedia: false + property list monitors: [] + } - } // Map of monitor name -> scale override (e.g., 0.8..2.0). When set, Colors.scale() returns this value + // general + property JsonObject general - property string fontFamily: "Roboto" // Family for all text + general: JsonObject { + property string avatarImage: Quickshell.env("HOME") + "/.face" + property bool dimDesktop: true + property bool showScreenCorners: false + } + + // location + property JsonObject location + + location: JsonObject { + property bool name: true + property bool useFahrenheit: false + property bool reverseDayMonth: false + property bool use12HourClock: false + } + + // screen recorder + property JsonObject screenRecorder + + screenRecorder: JsonObject { + property string directory: "~/Videos" + property int frameRate: 60 + property string audioCodec: "opus" + property string videoCodec: "h264" + property string quality: "very_high" + property string colorRange: "limited" + property bool showCursor: true + } + + // wallpaper + property JsonObject wallpaper + + wallpaper: JsonObject { + property string directory: "/usr/share/wallpapers" + property string current: "" + property bool isRandom: false + property int randomInterval: 300 + property bool generateTheme: false + property JsonObject swww + + onDirectoryChanged: WallpaperManager.loadWallpapers() + onIsRandomChanged: WallpaperManager.toggleRandomWallpaper() + onRandomIntervalChanged: WallpaperManager.restartRandomWallpaperTimer() + + swww: JsonObject { + property bool enabled: false + property string resizeMethod: "crop" + property int transitionFps: 60 + property string transitionType: "random" + property real transitionDuration: 1.1 + } + } + + // applauncher + property JsonObject appLauncher + + appLauncher: JsonObject { + property list pinnedExecs: [] + } + + // dock + property JsonObject dock + + dock: JsonObject { + property bool exclusive: false + property list monitors: [] + } + + // network + property JsonObject network + + network: JsonObject { + property bool wifiEnabled: true + property bool bluetoothEnabled: true + } + + // notifications + property JsonObject notifications + + notifications: JsonObject { + property list monitors: [] + } + + // audioVisualizer + property JsonObject audioVisualizer + + audioVisualizer: JsonObject { + property string type: "radial" + } + + // ui + property JsonObject ui + + ui: JsonObject { + property string fontFamily: "Roboto" // Family for all text + property list monitorsScale: [] + } } } - - Connections { - target: settingAdapter - function onRandomWallpaperChanged() { - WallpaperManager.toggleRandomWallpaper() - } - function onWallpaperIntervalChanged() { - WallpaperManager.restartRandomWallpaperTimer() - } - function onWallpaperFolderChanged() { - WallpaperManager.loadWallpapers() - } - function onNotificationMonitorsChanged() {} - } } diff --git a/Services/Time.qml b/Services/Time.qml index 0d43a9e..c9838b8 100644 --- a/Services/Time.qml +++ b/Services/Time.qml @@ -8,7 +8,7 @@ Singleton { id: root property var date: new Date() - property string time: Settings.settings.use12HourClock ? Qt.formatDateTime( + property string time: Settings.data.location.use12HourClock ? Qt.formatDateTime( date, "h:mm AP") : Qt.formatDateTime( date, "HH:mm") @@ -36,7 +36,7 @@ Singleton { } let month = now.toLocaleDateString(Qt.locale(), "MMMM") let year = now.toLocaleDateString(Qt.locale(), "yyyy") - return `${dayName}, ` + (Settings.settings.reverseDayMonth ? `${month} ${day}${suffix} ${year}` : `${day}${suffix} ${month} ${year}`) + return `${dayName}, ` + (Settings.data.location.reverseDayMonth ? `${month} ${day}${suffix} ${year}` : `${day}${suffix} ${month} ${year}`) } Timer { diff --git a/Services/Wallpapers.qml b/Services/Wallpapers.qml index 54c1cfb..2540a7a 100644 --- a/Services/Wallpapers.qml +++ b/Services/Wallpapers.qml @@ -17,17 +17,17 @@ Singleton { } property var wallpaperList: [] - property string currentWallpaper: Settings.settings.currentWallpaper + property string currentWallpaper: Settings.data.wallpaper.current property bool scanning: false - property string transitionType: Settings.settings.transitionType + property string transitionType: Settings.data.wallpaper.swww.transitionType property var randomChoices: ["fade", "left", "right", "top", "bottom", "wipe", "wave", "grow", "center", "any", "outer"] function loadWallpapers() { scanning = true wallpaperList = [] folderModel.folder = "" - folderModel.folder = "file://" + (Settings.settings.wallpaperFolder - !== undefined ? Settings.settings.wallpaperFolder : "") + folderModel.folder = "file://" + (Settings.data.wallpaper.directory + !== undefined ? Settings.data.wallpaper.directory : "") } function changeWallpaper(path) { @@ -37,14 +37,14 @@ Singleton { function setCurrentWallpaper(path, isInitial) { currentWallpaper = path if (!isInitial) { - Settings.settings.currentWallpaper = path + Settings.data.wallpaper.current = path } - if (Settings.settings.useSWWW) { - if (Settings.settings.transitionType === "random") { + if (Settings.data.swww.enabled) { + if (Settings.data.swww.transitionType === "random") { transitionType = randomChoices[Math.floor(Math.random( ) * randomChoices.length)] } else { - transitionType = Settings.settings.transitionType + transitionType = Settings.data.swww.transitionType } changeWallpaperProcess.running = true } @@ -66,31 +66,31 @@ Singleton { } function toggleRandomWallpaper() { - if (Settings.settings.randomWallpaper && !randomWallpaperTimer.running) { + if (Settings.data.wallpaper.isRandom && !randomWallpaperTimer.running) { randomWallpaperTimer.start() setRandomWallpaper() - } else if (!Settings.settings.randomWallpaper + } else if (!Settings.data.randomWallpaper && randomWallpaperTimer.running) { randomWallpaperTimer.stop() } } function restartRandomWallpaperTimer() { - if (Settings.settings.randomWallpaper) { + if (Settings.data.wallpaper.isRandom) { randomWallpaperTimer.stop() randomWallpaperTimer.start() } } function generateTheme() { - if (Settings.settings.useWallpaperTheme) { + if (Settings.data.wallpaper.generateTheme) { generateThemeProcess.running = true } } Timer { id: randomWallpaperTimer - interval: Settings.settings.wallpaperInterval * 1000 + interval: Settings.data.wallpaper.randomInterval * 1000 running: false repeat: true onTriggered: setRandomWallpaper() @@ -108,8 +108,8 @@ Singleton { var files = [] var filesSwww = [] for (var i = 0; i < count; i++) { - var filepath = (Settings.settings.wallpaperFolder - !== undefined ? Settings.settings.wallpaperFolder : "") + "/" + get( + var filepath = (Settings.data.wallpaper.folder + !== undefined ? Settings.data.wallpaper.folder : "") + "/" + get( i, "fileName") files.push(filepath) } @@ -121,8 +121,8 @@ Singleton { Process { id: changeWallpaperProcess - command: ["swww", "img", "--resize", Settings.settings.wallpaperResize, "--transition-fps", Settings.settings.transitionFps.toString( - ), "--transition-type", transitionType, "--transition-duration", Settings.settings.transitionDuration.toString( + command: ["swww", "img", "--resize", Settings.data.wallpaper.swww.resizeMethod, "--transition-fps", Settings.data.wallpaper.swww.transitionFps.toString( + ), "--transition-type", transitionType, "--transition-duration", Settings.data.wallpaper.transitionDuration.toString( ), currentWallpaper] running: false } diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 2c9362c..7733f4d 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -7,7 +7,7 @@ PanelWindow { id: outerPanel readonly property real scaling: Scaling.scale(screen) - property bool showOverlay: Settings.settings.dimPanels + property bool showOverlay: Settings.data.general.dimDesktop property int topMargin: Style.barHeight * scaling property color overlayColor: showOverlay ? Colors.overlay : "transparent" signal dismissed diff --git a/Widgets/NText.qml b/Widgets/NText.qml index a26cff1..0b9b13a 100644 --- a/Widgets/NText.qml +++ b/Widgets/NText.qml @@ -7,7 +7,7 @@ Text { readonly property real scaling: Scaling.scale(screen) - font.family: Settings.settings.fontFamily + font.family: Settings.data.ui.fontFamily font.pointSize: Style.fontSizeMedium * scaling font.weight: Font.Bold color: Colors.textPrimary diff --git a/Widgets/NTooltip.qml b/Widgets/NTooltip.qml index 38a1393..482f9e2 100644 --- a/Widgets/NTooltip.qml +++ b/Widgets/NTooltip.qml @@ -111,12 +111,10 @@ Window { z: 1 } - Text { + NText { id: tooltipText anchors.centerIn: parent text: root.text - color: Colors.textPrimary - font.family: Settings.settings.fontFamily font.pointSize: Style.fontSizeMedium * scaling horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter diff --git a/shell.qml b/shell.qml index 03df77d..c905a8b 100644 --- a/shell.qml +++ b/shell.qml @@ -1,4 +1,3 @@ - // Disable reload popup //@ pragma Env QS_NO_RELOAD_POPUP=1 import QtQuick From cd6e6729788dc8ae5581e41be63b062c62087895 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 09:30:01 -0400 Subject: [PATCH 045/394] Removed "right" text on the bar --- Modules/Bar/Bar.qml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 436f729..b91c94f 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -67,11 +67,6 @@ PanelWindow { anchors.verticalCenter: bar.verticalCenter spacing: Style.marginSmall * scaling - NText { - text: "Right" - anchors.verticalCenter: parent.verticalCenter - } - Clock { anchors.verticalCenter: parent.verticalCenter } From 15a57c67194af98c352e0f18cf86845ed55c9f36 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 09:35:23 -0400 Subject: [PATCH 046/394] NIconButton: fontSizeMedium by default --- Modules/Bar/Bar.qml | 2 -- Widgets/NIconButton.qml | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index b91c94f..f4e7223 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -74,7 +74,6 @@ PanelWindow { NIconButton { id: demoPanelToggle icon: "experiment" - fontPointSize: Style.fontSizeMedium anchors.verticalCenter: parent.verticalCenter onClicked: function () { demoPanel.isLoaded = !demoPanel.isLoaded @@ -84,7 +83,6 @@ PanelWindow { NIconButton { id: sidePanelToggle icon: "widgets" - fontPointSize: Style.fontSizeMedium anchors.verticalCenter: parent.verticalCenter onClicked: function () { sidePanel.isLoaded = !demoPanel.isLoaded diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml index 4603b27..1c2e370 100644 --- a/Widgets/NIconButton.qml +++ b/Widgets/NIconButton.qml @@ -16,7 +16,7 @@ Rectangle { property var onEntered: function () {} property var onExited: function () {} property var onClicked: function () {} - property real fontPointSize: Style.fontSizeXL + property real fontPointSize: Style.fontSizeMedium implicitWidth: size implicitHeight: size From cdee8023de047ce1141d9028543cb9f3a60e6814 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 10 Aug 2025 15:59:21 +0200 Subject: [PATCH 047/394] Add Corner.qml (~35mb for all 4 corners) --- Modules/Background/Corner.qml | 129 ++++++++++++++++++++++++++++++++++ Modules/Bar/Bar.qml | 2 +- Modules/SidePanel/.gitkeep | 0 shell.qml | 2 + 4 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 Modules/Background/Corner.qml delete mode 100644 Modules/SidePanel/.gitkeep diff --git a/Modules/Background/Corner.qml b/Modules/Background/Corner.qml new file mode 100644 index 0000000..dfd5b38 --- /dev/null +++ b/Modules/Background/Corner.qml @@ -0,0 +1,129 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import Quickshell.Wayland +import qs.Services + +ShellRoot { + id: root + + // Visible ring color + property color ringColor: Colors.backgroundPrimary + // The amount subtracted from full size for the inner cutout + // Inner size = full size - borderWidth (per axis) + property int borderWidth: Style.borderMedium + // Rounded radius for the inner cutout + property int innerRadius: 20 + + Variants { + model: Quickshell.screens + + PanelWindow { + required property ShellScreen modelData + + anchors { + top: true + bottom: true + left: true + right: true + } + margins { + top: Math.round(Style.barHeight * Scaling.scale(screen)) + } + color: "transparent" + screen: modelData + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.namespace: "quickshell-corner" + // Do not take keyboard focus and make the surface click-through + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + mask: Region {} + + // Source we want to show only as a ring + Rectangle { + id: overlaySource + anchors.fill: parent + color: root.ringColor + } + + // Texture for overlaySource + ShaderEffectSource { + id: overlayTexture + anchors.fill: parent + sourceItem: overlaySource + hideSource: true + live: true + visible: false + } + + // Mask via Canvas: paint opaque white, then punch rounded inner hole + Canvas { + id: maskSource + anchors.fill: parent + antialiasing: true + renderTarget: Canvas.FramebufferObject + onPaint: function() { + const ctx = getContext("2d"); + ctx.reset(); + ctx.clearRect(0, 0, width, height); + // Solid white base (alpha=1) + ctx.globalCompositeOperation = "source-over"; + ctx.fillStyle = "#ffffffff"; + ctx.fillRect(0, 0, width, height); + + // Punch hole using destination-out with rounded rect path + const x = Math.round(root.borderWidth / 2); + const y = Math.round(root.borderWidth / 2); + const w = Math.max(0, width - root.borderWidth); + const h = Math.max(0, height - root.borderWidth); + const r = Math.max(0, Math.min(root.innerRadius, Math.min(w, h) / 2)); + + ctx.globalCompositeOperation = "destination-out"; + ctx.fillStyle = "#ffffffff"; + ctx.beginPath(); + // rounded rectangle path using arcTo + ctx.moveTo(x + r, y); + ctx.lineTo(x + w - r, y); + ctx.arcTo(x + w, y, x + w, y + r, r); + ctx.lineTo(x + w, y + h - r); + ctx.arcTo(x + w, y + h, x + w - r, y + h, r); + ctx.lineTo(x + r, y + h); + ctx.arcTo(x, y + h, x, y + h - r, r); + ctx.lineTo(x, y + r); + ctx.arcTo(x, y, x + r, y, r); + ctx.closePath(); + ctx.fill(); + } + onWidthChanged: requestPaint() + onHeightChanged: requestPaint() + } + + // Repaint mask when properties change + Connections { + target: root + function onBorderWidthChanged() { maskSource.requestPaint() } + function onRingColorChanged() { /* no-op for mask */ } + function onInnerRadiusChanged() { maskSource.requestPaint() } + } + + // Texture for maskSource; hides the original + ShaderEffectSource { + id: maskTexture + anchors.fill: parent + sourceItem: maskSource + hideSource: true + live: true + visible: false + } + + // Apply mask to show only the ring area + MultiEffect { + anchors.fill: parent + source: overlayTexture + maskEnabled: true + maskSource: maskTexture + maskInverted: false + } + } + } +} + diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index f4e7223..2161a54 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -85,7 +85,7 @@ PanelWindow { icon: "widgets" anchors.verticalCenter: parent.verticalCenter onClicked: function () { - sidePanel.isLoaded = !demoPanel.isLoaded + sidePanel.isLoaded = !sidePanel.isLoaded } } } diff --git a/Modules/SidePanel/.gitkeep b/Modules/SidePanel/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/shell.qml b/shell.qml index 5a4e30e..adac8ca 100644 --- a/shell.qml +++ b/shell.qml @@ -26,6 +26,8 @@ ShellRoot { Background {} Overview {} + Corner{} + DemoPanel { id: demoPanel } From 7b653ec5a3b5ec3d4d8acb19fdf805c4906d9159 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 10 Aug 2025 17:25:49 +0200 Subject: [PATCH 048/394] Add loader to ScreenCorner, proper position for SidePanel, update Settings & NPanel to only display one NPanel at a time (latest one) --- Modules/Background/Corner.qml | 129 -------------------------- Modules/Background/ScreenCorner.qml | 136 ++++++++++++++++++++++++++++ Modules/Bar/Bar.qml | 13 ++- Modules/SidePanel/SidePanel.qml | 38 ++++++-- Services/Settings.qml | 3 + Widgets/NLoader.qml | 1 + Widgets/NPanel.qml | 23 ++++- shell.qml | 3 +- 8 files changed, 208 insertions(+), 138 deletions(-) delete mode 100644 Modules/Background/Corner.qml create mode 100644 Modules/Background/ScreenCorner.qml diff --git a/Modules/Background/Corner.qml b/Modules/Background/Corner.qml deleted file mode 100644 index dfd5b38..0000000 --- a/Modules/Background/Corner.qml +++ /dev/null @@ -1,129 +0,0 @@ -import QtQuick -import QtQuick.Effects -import Quickshell -import Quickshell.Wayland -import qs.Services - -ShellRoot { - id: root - - // Visible ring color - property color ringColor: Colors.backgroundPrimary - // The amount subtracted from full size for the inner cutout - // Inner size = full size - borderWidth (per axis) - property int borderWidth: Style.borderMedium - // Rounded radius for the inner cutout - property int innerRadius: 20 - - Variants { - model: Quickshell.screens - - PanelWindow { - required property ShellScreen modelData - - anchors { - top: true - bottom: true - left: true - right: true - } - margins { - top: Math.round(Style.barHeight * Scaling.scale(screen)) - } - color: "transparent" - screen: modelData - WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.namespace: "quickshell-corner" - // Do not take keyboard focus and make the surface click-through - WlrLayershell.keyboardFocus: WlrKeyboardFocus.None - mask: Region {} - - // Source we want to show only as a ring - Rectangle { - id: overlaySource - anchors.fill: parent - color: root.ringColor - } - - // Texture for overlaySource - ShaderEffectSource { - id: overlayTexture - anchors.fill: parent - sourceItem: overlaySource - hideSource: true - live: true - visible: false - } - - // Mask via Canvas: paint opaque white, then punch rounded inner hole - Canvas { - id: maskSource - anchors.fill: parent - antialiasing: true - renderTarget: Canvas.FramebufferObject - onPaint: function() { - const ctx = getContext("2d"); - ctx.reset(); - ctx.clearRect(0, 0, width, height); - // Solid white base (alpha=1) - ctx.globalCompositeOperation = "source-over"; - ctx.fillStyle = "#ffffffff"; - ctx.fillRect(0, 0, width, height); - - // Punch hole using destination-out with rounded rect path - const x = Math.round(root.borderWidth / 2); - const y = Math.round(root.borderWidth / 2); - const w = Math.max(0, width - root.borderWidth); - const h = Math.max(0, height - root.borderWidth); - const r = Math.max(0, Math.min(root.innerRadius, Math.min(w, h) / 2)); - - ctx.globalCompositeOperation = "destination-out"; - ctx.fillStyle = "#ffffffff"; - ctx.beginPath(); - // rounded rectangle path using arcTo - ctx.moveTo(x + r, y); - ctx.lineTo(x + w - r, y); - ctx.arcTo(x + w, y, x + w, y + r, r); - ctx.lineTo(x + w, y + h - r); - ctx.arcTo(x + w, y + h, x + w - r, y + h, r); - ctx.lineTo(x + r, y + h); - ctx.arcTo(x, y + h, x, y + h - r, r); - ctx.lineTo(x, y + r); - ctx.arcTo(x, y, x + r, y, r); - ctx.closePath(); - ctx.fill(); - } - onWidthChanged: requestPaint() - onHeightChanged: requestPaint() - } - - // Repaint mask when properties change - Connections { - target: root - function onBorderWidthChanged() { maskSource.requestPaint() } - function onRingColorChanged() { /* no-op for mask */ } - function onInnerRadiusChanged() { maskSource.requestPaint() } - } - - // Texture for maskSource; hides the original - ShaderEffectSource { - id: maskTexture - anchors.fill: parent - sourceItem: maskSource - hideSource: true - live: true - visible: false - } - - // Apply mask to show only the ring area - MultiEffect { - anchors.fill: parent - source: overlayTexture - maskEnabled: true - maskSource: maskTexture - maskInverted: false - } - } - } -} - diff --git a/Modules/Background/ScreenCorner.qml b/Modules/Background/ScreenCorner.qml new file mode 100644 index 0000000..46b9595 --- /dev/null +++ b/Modules/Background/ScreenCorner.qml @@ -0,0 +1,136 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import Quickshell.Wayland +import qs.Services +import qs.Widgets + +NLoader { + id: cornerLoader + isLoaded: Settings.data.general.showScreenCorners + panel: Component { + ShellRoot { + id: root + + // Visible ring color + property color ringColor: Colors.backgroundPrimary + // The amount subtracted from full size for the inner cutout + // Inner size = full size - borderWidth (per axis) + property int borderWidth: Style.borderMedium + // Rounded radius for the inner cutout + property int innerRadius: 20 + + Variants { + model: Quickshell.screens + + PanelWindow { + required property ShellScreen modelData + + anchors { + top: true + bottom: true + left: true + right: true + } + margins { + top: Math.round(Style.barHeight * Scaling.scale(screen)) + } + color: "transparent" + screen: modelData + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.namespace: "quickshell-corner" + // Do not take keyboard focus and make the surface click-through + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + mask: Region {} + + // Source we want to show only as a ring + Rectangle { + id: overlaySource + anchors.fill: parent + color: root.ringColor + } + + // Texture for overlaySource + ShaderEffectSource { + id: overlayTexture + anchors.fill: parent + sourceItem: overlaySource + hideSource: true + live: true + visible: false + } + + // Mask via Canvas: paint opaque white, then punch rounded inner hole + Canvas { + id: maskSource + anchors.fill: parent + antialiasing: true + renderTarget: Canvas.FramebufferObject + onPaint: function() { + const ctx = getContext("2d"); + ctx.reset(); + ctx.clearRect(0, 0, width, height); + // Solid white base (alpha=1) + ctx.globalCompositeOperation = "source-over"; + ctx.fillStyle = "#ffffffff"; + ctx.fillRect(0, 0, width, height); + + // Punch hole using destination-out with rounded rect path + const x = Math.round(root.borderWidth / 2); + const y = Math.round(root.borderWidth / 2); + const w = Math.max(0, width - root.borderWidth); + const h = Math.max(0, height - root.borderWidth); + const r = Math.max(0, Math.min(root.innerRadius, Math.min(w, h) / 2)); + + ctx.globalCompositeOperation = "destination-out"; + ctx.fillStyle = "#ffffffff"; + ctx.beginPath(); + // rounded rectangle path using arcTo + ctx.moveTo(x + r, y); + ctx.lineTo(x + w - r, y); + ctx.arcTo(x + w, y, x + w, y + r, r); + ctx.lineTo(x + w, y + h - r); + ctx.arcTo(x + w, y + h, x + w - r, y + h, r); + ctx.lineTo(x + r, y + h); + ctx.arcTo(x, y + h, x, y + h - r, r); + ctx.lineTo(x, y + r); + ctx.arcTo(x, y, x + r, y, r); + ctx.closePath(); + ctx.fill(); + } + onWidthChanged: requestPaint() + onHeightChanged: requestPaint() + } + + // Repaint mask when properties change + Connections { + target: root + function onBorderWidthChanged() { maskSource.requestPaint() } + function onRingColorChanged() { /* no-op for mask */ } + function onInnerRadiusChanged() { maskSource.requestPaint() } + } + + // Texture for maskSource; hides the original + ShaderEffectSource { + id: maskTexture + anchors.fill: parent + sourceItem: maskSource + hideSource: true + live: true + visible: false + } + + // Apply mask to show only the ring area + MultiEffect { + anchors.fill: parent + source: overlayTexture + maskEnabled: true + maskSource: maskTexture + maskInverted: false + } + } + } + } + } +} + diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 2161a54..5b91d8d 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -85,7 +85,18 @@ PanelWindow { icon: "widgets" anchors.verticalCenter: parent.verticalCenter onClicked: function () { - sidePanel.isLoaded = !sidePanel.isLoaded + // Map this button's center to the screen and open the side panel below it + const localCenterX = width / 2 + const localCenterY = height / 2 + const globalPoint = mapToItem(null, localCenterX, localCenterY) + if (sidePanel.isLoaded) { + sidePanel.isLoaded = false + } else if (sidePanel.openAt) { + sidePanel.openAt(globalPoint.x, screen) + } else { + // Fallback: toggle if API unavailable + sidePanel.isLoaded = true + } } } } diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml index 675ebd6..8068a5c 100644 --- a/Modules/SidePanel/SidePanel.qml +++ b/Modules/SidePanel/SidePanel.qml @@ -6,30 +6,56 @@ import Quickshell.Wayland import qs.Services import qs.Widgets -/* - An experiment/demo panel to tweaks widgets -*/ - NLoader { id: root + // X coordinate on screen (in pixels) where the panel should align its center. + // Set via openAt(x) from the bar button. + property real anchorX: 0 + // Target screen to open on + property var targetScreen: null + + // Public API to open the panel aligned under a given x coordinate. + function openAt(x, screen) { + anchorX = x + targetScreen = screen + isLoaded = true + // If the panel is already instantiated, update immediately + if (item) { + if (item.anchorX !== undefined) + item.anchorX = anchorX + if (item.screen !== undefined) + item.screen = targetScreen + } + } + panel: Component { NPanel { id: sidePanel readonly property real scaling: Scaling.scale(screen) + // X coordinate from the bar to align this panel under + property real anchorX: root.anchorX + // Ensure this panel attaches to the intended screen + screen: root.targetScreen // Ensure panel shows itself once created Component.onCompleted: show() Rectangle { color: Colors.backgroundPrimary - radius: Style.radiusMedium * scaling + radius: Style.radiusLarge * scaling border.color: Colors.backgroundTertiary border.width: Math.min(1, Style.borderMedium * scaling) width: 500 * scaling height: 400 - anchors.centerIn: parent + // Place the panel just below the bar (overlay content starts below bar due to topMargin) + y: Style.marginSmall * scaling + // Center horizontally under the anchorX, clamped to the screen bounds + x: Math.max( + Style.marginSmall * scaling, + Math.min(parent.width - width - Style.marginSmall * scaling, + Math.round(anchorX - width / 2))) // Prevent closing when clicking in the panel bg MouseArea { anchors.fill: parent } diff --git a/Services/Settings.qml b/Services/Settings.qml index 3873d99..5b4869a 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -15,6 +15,9 @@ Singleton { property string colorsFile: Quickshell.env("NOCTALIA_COLORS_FILE") || (settingsDir + "colors.json") property var data: settingAdapter + + // Needed to only have one NPanel loaded at a time. + property var openPanel: null Item { Component.onCompleted: { diff --git a/Widgets/NLoader.qml b/Widgets/NLoader.qml index f0166c3..b0e5323 100644 --- a/Widgets/NLoader.qml +++ b/Widgets/NLoader.qml @@ -27,6 +27,7 @@ Loader { Connections { target: loader.item + ignoreUnknownSignals: true function onDismissed() { loader.isLoaded = false } diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 7733f4d..c4b7a4b 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -13,11 +13,20 @@ PanelWindow { signal dismissed function hide() { - visible = false + //visible = false dismissed() } function show() { + // Ensure only one panel is visible at a time using Settings as ephemeral store + try { + if (Settings.openPanel && Settings.openPanel !== outerPanel && Settings.openPanel.hide) { + Settings.openPanel.hide() + } + Settings.openPanel = outerPanel + } catch (e) { + // ignore + } visible = true } @@ -44,4 +53,16 @@ PanelWindow { easing.type: Easing.InOutCubic } } + + Component.onDestruction: { + try { + if (visible && Settings.openPanel === outerPanel) Settings.openPanel = null + } catch (e) {} + } + + onVisibleChanged: function() { + try { + if (!visible && Settings.openPanel === outerPanel) Settings.openPanel = null + } catch (e) {} + } } diff --git a/shell.qml b/shell.qml index adac8ca..4f3eed4 100644 --- a/shell.qml +++ b/shell.qml @@ -5,6 +5,7 @@ import QtQuick import Quickshell import Quickshell.Io import Quickshell.Widgets +import qs.Widgets import qs.Modules.Bar import qs.Modules.DemoPanel import qs.Modules.Background @@ -26,7 +27,7 @@ ShellRoot { Background {} Overview {} - Corner{} + ScreenCorner {} DemoPanel { id: demoPanel From dacb40921c4924e2d2aa19c99fd8244c195fffc3 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 11:56:24 -0400 Subject: [PATCH 049/394] unused property --- Modules/Bar/Workspace.qml | 1 - 1 file changed, 1 deletion(-) diff --git a/Modules/Bar/Workspace.qml b/Modules/Bar/Workspace.qml index 43f9e32..5428ba2 100644 --- a/Modules/Bar/Workspace.qml +++ b/Modules/Bar/Workspace.qml @@ -13,7 +13,6 @@ Item { property bool hovered: false readonly property real scaling: Scaling.scale(screen) - property var modelData signal workspaceChanged(int workspaceId, color accentColor) From 2ed34884535e950e16a79a1c1b36076e6ff4bc54 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 11:56:43 -0400 Subject: [PATCH 050/394] Only one ShellRoot in shell.qml --- Modules/Background/Background.qml | 63 +++++++++++----------- Modules/Background/Overview.qml | 86 ++++++++++++++++--------------- Modules/Bar/Bars.qml | 0 3 files changed, 75 insertions(+), 74 deletions(-) create mode 100644 Modules/Bar/Bars.qml diff --git a/Modules/Background/Background.qml b/Modules/Background/Background.qml index 2756aed..25c4236 100644 --- a/Modules/Background/Background.qml +++ b/Modules/Background/Background.qml @@ -3,42 +3,41 @@ import Quickshell import Quickshell.Wayland import qs.Services -ShellRoot { - - property var modelData - property string wallpaperSource: Qt.resolvedUrl( - "../../Assets/Tests/wallpaper.png") - - Variants { +Variants { model: Quickshell.screens PanelWindow { - required property ShellScreen modelData + required property ShellScreen modelData + property string wallpaperSource: Qt.resolvedUrl("../../Assets/Tests/wallpaper.png") - visible: wallpaperSource !== "" - anchors { - bottom: true - top: true - right: true - left: true - } - margins { - top: 0 - } - color: "transparent" - screen: modelData - WlrLayershell.layer: WlrLayer.Background - WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.namespace: "quickshell-wallpaper" - Image { - anchors.fill: parent - fillMode: Image.PreserveAspectCrop - source: wallpaperSource visible: wallpaperSource !== "" - cache: true - smooth: true - mipmap: false - } + color: "transparent" + screen: modelData + WlrLayershell.layer: WlrLayer.Background + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.namespace: "quickshell-wallpaper" + + anchors { + bottom: true + top: true + right: true + left: true + } + + margins { + top: 0 + } + + Image { + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + source: wallpaperSource + visible: wallpaperSource !== "" + cache: true + smooth: true + mipmap: false + } + } - } + } diff --git a/Modules/Background/Overview.qml b/Modules/Background/Overview.qml index 0179aad..454ba18 100644 --- a/Modules/Background/Overview.qml +++ b/Modules/Background/Overview.qml @@ -4,52 +4,54 @@ import Quickshell import Quickshell.Wayland import qs.Services -ShellRoot { - property string wallpaperSource: Qt.resolvedUrl( - "../../Assets/Tests/wallpaper.png") - property var modelData - - Variants { +Variants { model: Quickshell.screens PanelWindow { - required property ShellScreen modelData + required property ShellScreen modelData + property string wallpaperSource: Qt.resolvedUrl("../../Assets/Tests/wallpaper.png") - visible: wallpaperSource !== "" - anchors { - top: true - bottom: true - right: true - left: true - } - color: "transparent" - screen: modelData - WlrLayershell.layer: WlrLayer.Background - WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.namespace: "quickshell-overview" - Image { - id: bgImage - anchors.fill: parent - fillMode: Image.PreserveAspectCrop - source: wallpaperSource - cache: true - smooth: true - mipmap: false visible: wallpaperSource !== "" - } - MultiEffect { - id: overviewBgBlur - anchors.fill: parent - source: bgImage - blurEnabled: true - blur: 0.48 - blurMax: 128 - } - Rectangle { - anchors.fill: parent - color: Qt.rgba(Colors.backgroundPrimary.r, Colors.backgroundPrimary.g, - Colors.backgroundPrimary.b, 0.5) - } + color: "transparent" + screen: modelData + WlrLayershell.layer: WlrLayer.Background + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.namespace: "quickshell-overview" + + anchors { + top: true + bottom: true + right: true + left: true + } + + Image { + id: bgImage + + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + source: wallpaperSource + cache: true + smooth: true + mipmap: false + visible: wallpaperSource !== "" + } + + MultiEffect { + id: overviewBgBlur + + anchors.fill: parent + source: bgImage + blurEnabled: true + blur: 0.48 + blurMax: 128 + } + + Rectangle { + anchors.fill: parent + color: Qt.rgba(Colors.backgroundPrimary.r, Colors.backgroundPrimary.g, Colors.backgroundPrimary.b, 0.5) + } + } - } + } diff --git a/Modules/Bar/Bars.qml b/Modules/Bar/Bars.qml new file mode 100644 index 0000000..e69de29 From 61abcddeebd6969b375bd63decde98300ed2eecf Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 11:59:54 -0400 Subject: [PATCH 051/394] qmlformat + fix build --- Modules/Background/Background.qml | 61 +++++++------ Modules/Background/Overview.qml | 88 +++++++++---------- .../{ScreenCorner.qml => ScreenCorners.qml} | 64 +++++++------- Modules/Bar/Bars.qml | 10 +++ Services/Settings.qml | 3 +- Services/Time.qml | 6 +- Services/Wallpapers.qml | 3 +- Widgets/NPanel.qml | 20 +++-- shell.qml | 15 +--- 9 files changed, 140 insertions(+), 130 deletions(-) rename Modules/Background/{ScreenCorner.qml => ScreenCorners.qml} (65%) diff --git a/Modules/Background/Background.qml b/Modules/Background/Background.qml index 25c4236..f81f79b 100644 --- a/Modules/Background/Background.qml +++ b/Modules/Background/Background.qml @@ -4,40 +4,39 @@ import Quickshell.Wayland import qs.Services Variants { - model: Quickshell.screens + model: Quickshell.screens - PanelWindow { - required property ShellScreen modelData - property string wallpaperSource: Qt.resolvedUrl("../../Assets/Tests/wallpaper.png") + PanelWindow { + required property ShellScreen modelData + property string wallpaperSource: Qt.resolvedUrl( + "../../Assets/Tests/wallpaper.png") - visible: wallpaperSource !== "" - color: "transparent" - screen: modelData - WlrLayershell.layer: WlrLayer.Background - WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.namespace: "quickshell-wallpaper" - - anchors { - bottom: true - top: true - right: true - left: true - } - - margins { - top: 0 - } - - Image { - anchors.fill: parent - fillMode: Image.PreserveAspectCrop - source: wallpaperSource - visible: wallpaperSource !== "" - cache: true - smooth: true - mipmap: false - } + visible: wallpaperSource !== "" + color: "transparent" + screen: modelData + WlrLayershell.layer: WlrLayer.Background + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.namespace: "quickshell-wallpaper" + anchors { + bottom: true + top: true + right: true + left: true } + margins { + top: 0 + } + + Image { + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + source: wallpaperSource + visible: wallpaperSource !== "" + cache: true + smooth: true + mipmap: false + } + } } diff --git a/Modules/Background/Overview.qml b/Modules/Background/Overview.qml index 454ba18..2d9cea8 100644 --- a/Modules/Background/Overview.qml +++ b/Modules/Background/Overview.qml @@ -5,53 +5,53 @@ import Quickshell.Wayland import qs.Services Variants { - model: Quickshell.screens + model: Quickshell.screens - PanelWindow { - required property ShellScreen modelData - property string wallpaperSource: Qt.resolvedUrl("../../Assets/Tests/wallpaper.png") + PanelWindow { + required property ShellScreen modelData + property string wallpaperSource: Qt.resolvedUrl( + "../../Assets/Tests/wallpaper.png") - visible: wallpaperSource !== "" - color: "transparent" - screen: modelData - WlrLayershell.layer: WlrLayer.Background - WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.namespace: "quickshell-overview" - - anchors { - top: true - bottom: true - right: true - left: true - } - - Image { - id: bgImage - - anchors.fill: parent - fillMode: Image.PreserveAspectCrop - source: wallpaperSource - cache: true - smooth: true - mipmap: false - visible: wallpaperSource !== "" - } - - MultiEffect { - id: overviewBgBlur - - anchors.fill: parent - source: bgImage - blurEnabled: true - blur: 0.48 - blurMax: 128 - } - - Rectangle { - anchors.fill: parent - color: Qt.rgba(Colors.backgroundPrimary.r, Colors.backgroundPrimary.g, Colors.backgroundPrimary.b, 0.5) - } + visible: wallpaperSource !== "" + color: "transparent" + screen: modelData + WlrLayershell.layer: WlrLayer.Background + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.namespace: "quickshell-overview" + anchors { + top: true + bottom: true + right: true + left: true } + Image { + id: bgImage + + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + source: wallpaperSource + cache: true + smooth: true + mipmap: false + visible: wallpaperSource !== "" + } + + MultiEffect { + id: overviewBgBlur + + anchors.fill: parent + source: bgImage + blurEnabled: true + blur: 0.48 + blurMax: 128 + } + + Rectangle { + anchors.fill: parent + color: Qt.rgba(Colors.backgroundPrimary.r, Colors.backgroundPrimary.g, + Colors.backgroundPrimary.b, 0.5) + } + } } diff --git a/Modules/Background/ScreenCorner.qml b/Modules/Background/ScreenCorners.qml similarity index 65% rename from Modules/Background/ScreenCorner.qml rename to Modules/Background/ScreenCorners.qml index 46b9595..4167b46 100644 --- a/Modules/Background/ScreenCorner.qml +++ b/Modules/Background/ScreenCorners.qml @@ -66,37 +66,38 @@ NLoader { anchors.fill: parent antialiasing: true renderTarget: Canvas.FramebufferObject - onPaint: function() { - const ctx = getContext("2d"); - ctx.reset(); - ctx.clearRect(0, 0, width, height); + onPaint: function () { + const ctx = getContext("2d") + ctx.reset() + ctx.clearRect(0, 0, width, height) // Solid white base (alpha=1) - ctx.globalCompositeOperation = "source-over"; - ctx.fillStyle = "#ffffffff"; - ctx.fillRect(0, 0, width, height); + ctx.globalCompositeOperation = "source-over" + ctx.fillStyle = "#ffffffff" + ctx.fillRect(0, 0, width, height) // Punch hole using destination-out with rounded rect path - const x = Math.round(root.borderWidth / 2); - const y = Math.round(root.borderWidth / 2); - const w = Math.max(0, width - root.borderWidth); - const h = Math.max(0, height - root.borderWidth); - const r = Math.max(0, Math.min(root.innerRadius, Math.min(w, h) / 2)); + const x = Math.round(root.borderWidth / 2) + const y = Math.round(root.borderWidth / 2) + const w = Math.max(0, width - root.borderWidth) + const h = Math.max(0, height - root.borderWidth) + const r = Math.max(0, Math.min(root.innerRadius, + Math.min(w, h) / 2)) - ctx.globalCompositeOperation = "destination-out"; - ctx.fillStyle = "#ffffffff"; - ctx.beginPath(); + ctx.globalCompositeOperation = "destination-out" + ctx.fillStyle = "#ffffffff" + ctx.beginPath() // rounded rectangle path using arcTo - ctx.moveTo(x + r, y); - ctx.lineTo(x + w - r, y); - ctx.arcTo(x + w, y, x + w, y + r, r); - ctx.lineTo(x + w, y + h - r); - ctx.arcTo(x + w, y + h, x + w - r, y + h, r); - ctx.lineTo(x + r, y + h); - ctx.arcTo(x, y + h, x, y + h - r, r); - ctx.lineTo(x, y + r); - ctx.arcTo(x, y, x + r, y, r); - ctx.closePath(); - ctx.fill(); + ctx.moveTo(x + r, y) + ctx.lineTo(x + w - r, y) + ctx.arcTo(x + w, y, x + w, y + r, r) + ctx.lineTo(x + w, y + h - r) + ctx.arcTo(x + w, y + h, x + w - r, y + h, r) + ctx.lineTo(x + r, y + h) + ctx.arcTo(x, y + h, x, y + h - r, r) + ctx.lineTo(x, y + r) + ctx.arcTo(x, y, x + r, y, r) + ctx.closePath() + ctx.fill() } onWidthChanged: requestPaint() onHeightChanged: requestPaint() @@ -105,9 +106,13 @@ NLoader { // Repaint mask when properties change Connections { target: root - function onBorderWidthChanged() { maskSource.requestPaint() } - function onRingColorChanged() { /* no-op for mask */ } - function onInnerRadiusChanged() { maskSource.requestPaint() } + function onBorderWidthChanged() { + maskSource.requestPaint() + } + function onRingColorChanged() {/* no-op for mask */ } + function onInnerRadiusChanged() { + maskSource.requestPaint() + } } // Texture for maskSource; hides the original @@ -133,4 +138,3 @@ NLoader { } } } - diff --git a/Modules/Bar/Bars.qml b/Modules/Bar/Bars.qml index e69de29..0b549e4 100644 --- a/Modules/Bar/Bars.qml +++ b/Modules/Bar/Bars.qml @@ -0,0 +1,10 @@ +import Quickshell +import qs.Modules.Bar + +Variants { + model: Quickshell.screens + + delegate: Bar { + modelData: item + } +} diff --git a/Services/Settings.qml b/Services/Settings.qml index 5b4869a..feb44b5 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -15,7 +15,7 @@ Singleton { property string colorsFile: Quickshell.env("NOCTALIA_COLORS_FILE") || (settingsDir + "colors.json") property var data: settingAdapter - + // Needed to only have one NPanel loaded at a time. property var openPanel: null @@ -32,7 +32,6 @@ Singleton { // Qt.callLater(function () { // WallpaperManager.setCurrentWallpaper(settings.currentWallpaper, true); // }) - id: settingFileView path: settingsFile diff --git a/Services/Time.qml b/Services/Time.qml index c9838b8..27718ef 100644 --- a/Services/Time.qml +++ b/Services/Time.qml @@ -9,9 +9,9 @@ Singleton { property var date: new Date() property string time: Settings.data.location.use12HourClock ? Qt.formatDateTime( - date, - "h:mm AP") : Qt.formatDateTime( - date, "HH:mm") + date, + "h:mm AP") : Qt.formatDateTime( + date, "HH:mm") property string dateString: { let now = date let dayName = now.toLocaleDateString(Qt.locale(), "ddd") diff --git a/Services/Wallpapers.qml b/Services/Wallpapers.qml index 2540a7a..545c083 100644 --- a/Services/Wallpapers.qml +++ b/Services/Wallpapers.qml @@ -69,8 +69,7 @@ Singleton { if (Settings.data.wallpaper.isRandom && !randomWallpaperTimer.running) { randomWallpaperTimer.start() setRandomWallpaper() - } else if (!Settings.data.randomWallpaper - && randomWallpaperTimer.running) { + } else if (!Settings.data.randomWallpaper && randomWallpaperTimer.running) { randomWallpaperTimer.stop() } } diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index c4b7a4b..7fc42ab 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -20,11 +20,13 @@ PanelWindow { function show() { // Ensure only one panel is visible at a time using Settings as ephemeral store try { - if (Settings.openPanel && Settings.openPanel !== outerPanel && Settings.openPanel.hide) { + if (Settings.openPanel && Settings.openPanel !== outerPanel + && Settings.openPanel.hide) { Settings.openPanel.hide() } Settings.openPanel = outerPanel } catch (e) { + // ignore } visible = true @@ -56,13 +58,19 @@ PanelWindow { Component.onDestruction: { try { - if (visible && Settings.openPanel === outerPanel) Settings.openPanel = null - } catch (e) {} + if (visible && Settings.openPanel === outerPanel) + Settings.openPanel = null + } catch (e) { + + } } - onVisibleChanged: function() { + onVisibleChanged: function () { try { - if (!visible && Settings.openPanel === outerPanel) Settings.openPanel = null - } catch (e) {} + if (!visible && Settings.openPanel === outerPanel) + Settings.openPanel = null + } catch (e) { + + } } } diff --git a/shell.qml b/shell.qml index 4f3eed4..4230c8c 100644 --- a/shell.qml +++ b/shell.qml @@ -1,6 +1,6 @@ + // Disable reload popup //@ pragma Env QS_NO_RELOAD_POPUP=1 - import QtQuick import Quickshell import Quickshell.Io @@ -15,19 +15,10 @@ import qs.Services ShellRoot { id: root - - Variants { - model: Quickshell.screens - - delegate: Bar { - modelData: item - } - } - Background {} Overview {} - - ScreenCorner {} + ScreenCorners {} + Bars {} DemoPanel { id: demoPanel From 975cb6018f0f07d382b7fe52be45c6d9201ebd32 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 12:04:34 -0400 Subject: [PATCH 052/394] Removing unecessary wrapper in ScreenCorners.qml --- Modules/Background/ScreenCorners.qml | 235 ++++++++++++++------------- shell.qml | 1 - 2 files changed, 119 insertions(+), 117 deletions(-) diff --git a/Modules/Background/ScreenCorners.qml b/Modules/Background/ScreenCorners.qml index 4167b46..1557f19 100644 --- a/Modules/Background/ScreenCorners.qml +++ b/Modules/Background/ScreenCorners.qml @@ -6,12 +6,16 @@ import qs.Services import qs.Widgets NLoader { - id: cornerLoader isLoaded: Settings.data.general.showScreenCorners - panel: Component { - ShellRoot { + + panel: Variants { + model: Quickshell.screens + + PanelWindow { id: root + required property ShellScreen modelData + // Visible ring color property color ringColor: Colors.backgroundPrimary // The amount subtracted from full size for the inner cutout @@ -20,121 +24,120 @@ NLoader { // Rounded radius for the inner cutout property int innerRadius: 20 - Variants { - model: Quickshell.screens + color: "transparent" + screen: modelData + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.namespace: "quickshell-corner" + // Do not take keyboard focus and make the surface click-through + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None - PanelWindow { - required property ShellScreen modelData - - anchors { - top: true - bottom: true - left: true - right: true - } - margins { - top: Math.round(Style.barHeight * Scaling.scale(screen)) - } - color: "transparent" - screen: modelData - WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.namespace: "quickshell-corner" - // Do not take keyboard focus and make the surface click-through - WlrLayershell.keyboardFocus: WlrKeyboardFocus.None - mask: Region {} - - // Source we want to show only as a ring - Rectangle { - id: overlaySource - anchors.fill: parent - color: root.ringColor - } - - // Texture for overlaySource - ShaderEffectSource { - id: overlayTexture - anchors.fill: parent - sourceItem: overlaySource - hideSource: true - live: true - visible: false - } - - // Mask via Canvas: paint opaque white, then punch rounded inner hole - Canvas { - id: maskSource - anchors.fill: parent - antialiasing: true - renderTarget: Canvas.FramebufferObject - onPaint: function () { - const ctx = getContext("2d") - ctx.reset() - ctx.clearRect(0, 0, width, height) - // Solid white base (alpha=1) - ctx.globalCompositeOperation = "source-over" - ctx.fillStyle = "#ffffffff" - ctx.fillRect(0, 0, width, height) - - // Punch hole using destination-out with rounded rect path - const x = Math.round(root.borderWidth / 2) - const y = Math.round(root.borderWidth / 2) - const w = Math.max(0, width - root.borderWidth) - const h = Math.max(0, height - root.borderWidth) - const r = Math.max(0, Math.min(root.innerRadius, - Math.min(w, h) / 2)) - - ctx.globalCompositeOperation = "destination-out" - ctx.fillStyle = "#ffffffff" - ctx.beginPath() - // rounded rectangle path using arcTo - ctx.moveTo(x + r, y) - ctx.lineTo(x + w - r, y) - ctx.arcTo(x + w, y, x + w, y + r, r) - ctx.lineTo(x + w, y + h - r) - ctx.arcTo(x + w, y + h, x + w - r, y + h, r) - ctx.lineTo(x + r, y + h) - ctx.arcTo(x, y + h, x, y + h - r, r) - ctx.lineTo(x, y + r) - ctx.arcTo(x, y, x + r, y, r) - ctx.closePath() - ctx.fill() - } - onWidthChanged: requestPaint() - onHeightChanged: requestPaint() - } - - // Repaint mask when properties change - Connections { - target: root - function onBorderWidthChanged() { - maskSource.requestPaint() - } - function onRingColorChanged() {/* no-op for mask */ } - function onInnerRadiusChanged() { - maskSource.requestPaint() - } - } - - // Texture for maskSource; hides the original - ShaderEffectSource { - id: maskTexture - anchors.fill: parent - sourceItem: maskSource - hideSource: true - live: true - visible: false - } - - // Apply mask to show only the ring area - MultiEffect { - anchors.fill: parent - source: overlayTexture - maskEnabled: true - maskSource: maskTexture - maskInverted: false - } - } + anchors { + top: true + bottom: true + left: true + right: true } + + margins { + top: Math.round(Style.barHeight * Scaling.scale(screen)) + } + + // Source we want to show only as a ring + Rectangle { + id: overlaySource + + anchors.fill: parent + color: root.ringColor + } + + // Texture for overlaySource + ShaderEffectSource { + id: overlayTexture + + anchors.fill: parent + sourceItem: overlaySource + hideSource: true + live: true + visible: false + } + + // Mask via Canvas: paint opaque white, then punch rounded inner hole + Canvas { + id: maskSource + + anchors.fill: parent + antialiasing: true + renderTarget: Canvas.FramebufferObject + onPaint: function () { + const ctx = getContext("2d") + ctx.reset() + ctx.clearRect(0, 0, width, height) + // Solid white base (alpha=1) + ctx.globalCompositeOperation = "source-over" + ctx.fillStyle = "#ffffffff" + ctx.fillRect(0, 0, width, height) + // Punch hole using destination-out with rounded rect path + const x = Math.round(root.borderWidth / 2) + const y = Math.round(root.borderWidth / 2) + const w = Math.max(0, width - root.borderWidth) + const h = Math.max(0, height - root.borderWidth) + const r = Math.max(0, Math.min(root.innerRadius, Math.min(w, h) / 2)) + ctx.globalCompositeOperation = "destination-out" + ctx.fillStyle = "#ffffffff" + ctx.beginPath() + // rounded rectangle path using arcTo + ctx.moveTo(x + r, y) + ctx.lineTo(x + w - r, y) + ctx.arcTo(x + w, y, x + w, y + r, r) + ctx.lineTo(x + w, y + h - r) + ctx.arcTo(x + w, y + h, x + w - r, y + h, r) + ctx.lineTo(x + r, y + h) + ctx.arcTo(x, y + h, x, y + h - r, r) + ctx.lineTo(x, y + r) + ctx.arcTo(x, y, x + r, y, r) + ctx.closePath() + ctx.fill() + } + onWidthChanged: requestPaint() + onHeightChanged: requestPaint() + } + + // Repaint mask when properties change + Connections { + function onBorderWidthChanged() { + maskSource.requestPaint() + } + + function onRingColorChanged() {} + + function onInnerRadiusChanged() { + maskSource.requestPaint() + } + + target: root + } + + // Texture for maskSource; hides the original + ShaderEffectSource { + id: maskTexture + + anchors.fill: parent + sourceItem: maskSource + hideSource: true + live: true + visible: false + } + + // Apply mask to show only the ring area + MultiEffect { + anchors.fill: parent + source: overlayTexture + maskEnabled: true + maskSource: maskTexture + maskInverted: false + } + + mask: Region {} } } } diff --git a/shell.qml b/shell.qml index 4230c8c..54db807 100644 --- a/shell.qml +++ b/shell.qml @@ -1,4 +1,3 @@ - // Disable reload popup //@ pragma Env QS_NO_RELOAD_POPUP=1 import QtQuick From b0ff67e2e48b11e41fdeccfc9e77ba7e5bb6d70f Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 13:24:11 -0400 Subject: [PATCH 053/394] Removed holidays on the calendar --- Helpers/Holidays.js | 81 ------------------------------------------- Widgets/NCalendar.qml | 72 ++------------------------------------ 2 files changed, 2 insertions(+), 151 deletions(-) delete mode 100644 Helpers/Holidays.js diff --git a/Helpers/Holidays.js b/Helpers/Holidays.js deleted file mode 100644 index 1b67774..0000000 --- a/Helpers/Holidays.js +++ /dev/null @@ -1,81 +0,0 @@ -var _countryCode = null; -var _regionCode = null; -var _regionName = null; -var _holidaysCache = {}; - -function getCountryCode(callback) { - if (_countryCode) { - callback(_countryCode); - return; - } - var xhr = new XMLHttpRequest(); - xhr.open("GET", "https://nominatim.openstreetmap.org/search?city="+ Settings.data.location.name+"&country=&format=json&addressdetails=1&extratags=1", true); - xhr.onreadystatechange = function() { - if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { - var response = JSON.parse(xhr.responseText); - _countryCode = response?.[0]?.address?.country_code ?? "US"; - _regionCode = response?.[0]?.address?.["ISO3166-2-lvl4"] ?? ""; - _regionName = response?.[0]?.address?.state ?? ""; - callback(_countryCode); - } - } - xhr.send(); -} - -function getHolidays(year, countryCode, callback) { - var cacheKey = year + "-" + countryCode; - if (_holidaysCache[cacheKey]) { - callback(_holidaysCache[cacheKey]); - return; - } - var url = "https://date.nager.at/api/v3/PublicHolidays/" + year + "/" + countryCode; - var xhr = new XMLHttpRequest(); - xhr.open("GET", url, true); - xhr.onreadystatechange = function() { - if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { - var holidays = JSON.parse(xhr.responseText); - var augmentedHolidays = filterHolidaysByRegion(holidays); - _holidaysCache[cacheKey] = augmentedHolidays; - callback(augmentedHolidays); - } - } - xhr.send(); -} - -function filterHolidaysByRegion(holidays) { - if (!_regionCode) { - return holidays; - } - const retHolidays = []; - holidays.forEach(function(holiday) { - if (holiday.counties?.length > 0) { - let found = false; - holiday.counties.forEach(function(county) { - if (county.toLowerCase() === _regionCode.toLowerCase()) { - found = true; - } - }); - if (found) { - var regionText = " (" + _regionName + ")"; - holiday.name = holiday.name + regionText; - holiday.localName = holiday.localName + regionText; - retHolidays.push(holiday); - } - } else { - retHolidays.push(holiday); - } - }); - return retHolidays; -} - -function getHolidaysForMonth(year, month, callback) { - getCountryCode(function(countryCode) { - getHolidays(year, countryCode, function(holidays) { - var filtered = holidays.filter(function(h) { - var date = new Date(h.date); - return date.getFullYear() === year && date.getMonth() === month; - }); - callback(filtered); - }); - }); -} \ No newline at end of file diff --git a/Widgets/NCalendar.qml b/Widgets/NCalendar.qml index d1518e8..c3ffda8 100644 --- a/Widgets/NCalendar.qml +++ b/Widgets/NCalendar.qml @@ -5,8 +5,6 @@ import Quickshell import Quickshell.Wayland import qs.Services -import "../Helpers/Holidays.js" as Holidays - NPanel { id: root @@ -88,25 +86,12 @@ NPanel { MonthGrid { id: calendar - property var holidays: [] - - // Fetch holidays when calendar is opened or month/year changes - function updateHolidays() { - Holidays.getHolidaysForMonth(calendar.year, calendar.month, - function (holidays) { - calendar.holidays = holidays - }) - } - Layout.fillWidth: true Layout.leftMargin: Style.marginSmall * scaling Layout.rightMargin: Style.marginSmall * scaling spacing: 0 month: Time.date.getMonth() year: Time.date.getFullYear() - onMonthChanged: updateHolidays() - onYearChanged: updateHolidays() - Component.onCompleted: updateHolidays() // Optionally, update when the panel becomes visible Connections { @@ -114,7 +99,6 @@ NPanel { if (root.visible) { calendar.month = Time.date.getMonth() calendar.year = Time.date.getFullYear() - calendar.updateHolidays() } } @@ -122,72 +106,20 @@ NPanel { } delegate: Rectangle { - property var holidayInfo: calendar.holidays.filter(function (h) { - var d = new Date(h.date) - return d.getDate() === model.day && d.getMonth() === model.month - && d.getFullYear() === model.year - }) - property bool isHoliday: holidayInfo.length > 0 - width: Style.baseWidgetSize * scaling height: Style.baseWidgetSize * scaling radius: Style.radiusSmall * scaling - color: { - if (model.today) - return Colors.accentPrimary - - if (mouseArea2.containsMouse) - return Colors.backgroundTertiary - - return "transparent" - } - - // Holiday dot indicator - Rectangle { - visible: isHoliday - width: Style.baseWidgetSize / 8 * scaling - height: Style.baseWidgetSize / 8 * scaling - radius: Style.radiusSmall * scaling - color: Colors.accentTertiary - anchors.top: parent.top - anchors.right: parent.right - anchors.topMargin: Style.marginTiny * scaling - anchors.rightMargin: Style.marginTiny * scaling - z: 2 - } + color: model.today ? Colors.accentPrimary : "transparent" NText { anchors.centerIn: parent text: model.day color: model.today ? Colors.onAccent : Colors.textPrimary - opacity: model.month === calendar.month ? (mouseArea2.containsMouse ? Style.opacityFull : Style.opacityHeavy) : Style.opacityLight + opacity: model.month === calendar.month ? Style.opacityHeavy : Style.opacityLight font.pointSize: Style.fontSizeMedium * scaling font.bold: model.today ? true : false } - MouseArea { - id: mouseArea2 - - anchors.fill: parent - hoverEnabled: true - onEntered: { - if (isHoliday) { - holidayTooltip.text = holidayInfo.map(function (h) { - return h.localName + (h.name !== h.localName ? " (" + h.name + ")" : "") - + (h.global ? " [Global]" : "") - }).join(", ") - holidayTooltip.target = parent - holidayTooltip.show() - } - } - onExited: holidayTooltip.hide() - } - - NTooltip { - id: holidayTooltip - text: "" - } - Behavior on color { ColorAnimation { duration: 150 From 92e121b356e93825dfcd53907c86b00160ee27cb Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 10 Aug 2025 19:25:44 +0200 Subject: [PATCH 054/394] Tons of small changes, add SidePanel, NCard, NCircleStat, NSystemMnitor --- Modules/DemoPanel/DemoPanel.qml | 24 ++++++- Modules/SidePanel/MediaCard.qml | 44 ++++++++++++ Modules/SidePanel/ProfileCard.qml | 68 ++++++++++++++++++ Modules/SidePanel/SidePanel.qml | 110 ++++++++++++++++++++++++++++- Modules/SidePanel/SystemCard.qml | 40 +++++++++++ Modules/SidePanel/WeatherCard.qml | 59 ++++++++++++++++ Services/Scaling.qml | 14 ++++ Services/Settings.qml | 2 +- Widgets/NBox.qml | 20 ++++++ Widgets/NCard.qml | 18 +++++ Widgets/NCircleStat.qml | 112 ++++++++++++++++++++++++++++++ Widgets/NSystemMonitor.qml | 73 +++++++++++++++++++ 12 files changed, 579 insertions(+), 5 deletions(-) create mode 100644 Modules/SidePanel/MediaCard.qml create mode 100644 Modules/SidePanel/ProfileCard.qml create mode 100644 Modules/SidePanel/SystemCard.qml create mode 100644 Modules/SidePanel/WeatherCard.qml create mode 100644 Widgets/NBox.qml create mode 100644 Widgets/NCard.qml create mode 100644 Widgets/NCircleStat.qml create mode 100644 Widgets/NSystemMonitor.qml diff --git a/Modules/DemoPanel/DemoPanel.qml b/Modules/DemoPanel/DemoPanel.qml index 62c3959..38a1a86 100644 --- a/Modules/DemoPanel/DemoPanel.qml +++ b/Modules/DemoPanel/DemoPanel.qml @@ -73,8 +73,28 @@ NLoader { // NSlider ColumnLayout { spacing: 16 * scaling - NText { text: "NSlider"; color: Colors.accentSecondary } - NSlider {} + NText { text: "Scaling"; color: Colors.accentSecondary } + RowLayout { + spacing: Style.marginSmall * scaling + NText { text: `${Math.round(Scaling.overrideScale * 100)}%`; Layout.alignment: Qt.AlignVCenter } + NSlider { + id: scaleSlider + from: 0.6 + to: 1.8 + stepSize: 0.01 + value: Scaling.overrideScale + onMoved: function() { Scaling.overrideScale = value } + onPressedChanged: function() { Scaling.overrideEnabled = true } + } + NIconButton { + icon: "restart_alt" + sizeMultiplier: 0.7 + onClicked: function() { + Scaling.overrideEnabled = false + Scaling.overrideScale = 1.0 + } + } + } NDivider { Layout.fillWidth: true } } } diff --git a/Modules/SidePanel/MediaCard.qml b/Modules/SidePanel/MediaCard.qml new file mode 100644 index 0000000..193b579 --- /dev/null +++ b/Modules/SidePanel/MediaCard.qml @@ -0,0 +1,44 @@ +import QtQuick +import QtQuick.Layouts +import qs.Services +import qs.Widgets + +// Media player area (placeholder until MediaPlayer service is wired) +NBox { + id: root + + readonly property real scaling: Scaling.scale(screen) + + Layout.fillWidth: true + // Let content dictate the height (no hardcoded height here) + // Height can be overridden by parent layout (SidePanel binds it to stats card) + implicitHeight: content.implicitHeight + Style.marginLarge * 2 * scaling + + Column { + id: content + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Style.marginXL * scaling + spacing: Style.marginSmall * scaling + + Item { height: 36 * scaling } + + Text { + text: "music_note" + font.family: "Material Symbols Outlined" + font.pointSize: 28 * scaling + color: Colors.textSecondary + anchors.horizontalCenter: parent.horizontalCenter + } + NText { + text: "No music player detected" + color: Colors.textSecondary + horizontalAlignment: Text.AlignHCenter + anchors.horizontalCenter: parent.horizontalCenter + } + + Item { height: 36 * scaling } + } +} + diff --git a/Modules/SidePanel/ProfileCard.qml b/Modules/SidePanel/ProfileCard.qml new file mode 100644 index 0000000..c04da96 --- /dev/null +++ b/Modules/SidePanel/ProfileCard.qml @@ -0,0 +1,68 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import QtQuick.Effects +import qs.Services +import qs.Widgets + +// Header card with avatar, user and quick actions +NBox { + id: root + + readonly property real scaling: Scaling.scale(screen) + + Layout.fillWidth: true + // Height driven by content + implicitHeight: content.implicitHeight + Style.marginMedium * 2 * scaling + + RowLayout { + id: content + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Style.marginMedium * scaling + spacing: Style.marginMedium * scaling + + Item { + id: avatarBox + width: 40 * scaling + height: 40 * scaling + + Image { + id: avatarImage + anchors.fill: parent + source: Settings.data.general.avatarImage + fillMode: Image.PreserveAspectCrop + asynchronous: true + } + + // Ensure rounded corners consistently across renderers + MultiEffect { + anchors.fill: avatarImage + source: avatarImage + maskEnabled: true + maskSource: Rectangle { + anchors.fill: parent + color: "white" + radius: Style.radiusMedium * scaling + } + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 2 * scaling + NText { text: Quickshell.env("USER") || "user" } + NText { text: "System Uptime: —"; color: Colors.textSecondary } + } + + RowLayout { + spacing: Style.marginSmall * scaling + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + Item { Layout.fillWidth: true } + NIconButton { icon: "settings"; sizeMultiplier: 0.8 } + NIconButton { icon: "power_settings_new"; sizeMultiplier: 0.8 } + } + } +} + diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml index 8068a5c..541f171 100644 --- a/Modules/SidePanel/SidePanel.qml +++ b/Modules/SidePanel/SidePanel.qml @@ -6,6 +6,7 @@ import Quickshell.Wayland import qs.Services import qs.Widgets + NLoader { id: root @@ -34,6 +35,8 @@ NLoader { id: sidePanel readonly property real scaling: Scaling.scale(screen) + // Single source of truth for spacing between cards (both axes) + property real cardSpacing: Style.marginLarge * scaling // X coordinate from the bar to align this panel under property real anchorX: root.anchorX // Ensure this panel attaches to the intended screen @@ -42,13 +45,19 @@ NLoader { // Ensure panel shows itself once created Component.onCompleted: show() + // Inline helpers moved to dedicated widgets: NCard and NCircleStat + Rectangle { + id: panelBackground color: Colors.backgroundPrimary radius: Style.radiusLarge * scaling border.color: Colors.backgroundTertiary border.width: Math.min(1, Style.borderMedium * scaling) - width: 500 * scaling - height: 400 + layer.enabled: true + width: 460 * scaling + property real innerMargin: sidePanel.cardSpacing + // Height scales to content plus vertical padding + height: content.implicitHeight + innerMargin * 2 // Place the panel just below the bar (overlay content starts below bar due to topMargin) y: Style.marginSmall * scaling // Center horizontally under the anchorX, clamped to the screen bounds @@ -60,6 +69,103 @@ NLoader { // Prevent closing when clicking in the panel bg MouseArea { anchors.fill: parent } + // Content wrapper to ensure childrenRect drives implicit height + Item { + id: content + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: panelBackground.innerMargin + implicitHeight: layout.implicitHeight + + // Layout content (not vertically anchored so implicitHeight is valid) + ColumnLayout { + id: layout + // Use the same spacing value horizontally and vertically + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + spacing: sidePanel.cardSpacing + + // Cards (consistent inter-card spacing via ColumnLayout spacing) + ProfileCard { Layout.topMargin: 0; Layout.bottomMargin: 0 } + WeatherCard { Layout.topMargin: 0; Layout.bottomMargin: 0 } + + // Middle section: media + stats column + RowLayout { + Layout.fillWidth: true + Layout.topMargin: 0 + Layout.bottomMargin: 0 + spacing: sidePanel.cardSpacing + + // Media card + MediaCard { id: mediaCard; Layout.fillWidth: true; implicitHeight: statsCard.implicitHeight } + + // System monitors combined in one card + SystemCard { id: statsCard } + } + + // Bottom actions (two grouped rows of round buttons) + RowLayout { + Layout.fillWidth: true + Layout.topMargin: 0 + Layout.bottomMargin: 0 + spacing: sidePanel.cardSpacing + + // Power Profiles: performance, balanced, eco + NBox { + Layout.fillWidth: true + Layout.preferredWidth: 1 + implicitHeight: powerRow.implicitHeight + Style.marginSmall * 2 * scaling + RowLayout { + id: powerRow + anchors.fill: parent + anchors.margins: Style.marginSmall * scaling + spacing: sidePanel.cardSpacing + Item { Layout.fillWidth: true } + // Performance + NIconButton { + icon: "speed" + sizeMultiplier: 1.0 + onClicked: function () { /* TODO: hook to power profile */ } + } + // Balanced + NIconButton { + icon: "balance" + sizeMultiplier: 1.0 + onClicked: function () { /* TODO: hook to power profile */ } + } + // Eco + NIconButton { + icon: "eco" + sizeMultiplier: 1.0 + onClicked: function () { /* TODO: hook to power profile */ } + } + Item { Layout.fillWidth: true } + } + } + + // Utilities: record & wallpaper + NBox { + Layout.fillWidth: true + Layout.preferredWidth: 1 + implicitHeight: utilRow.implicitHeight + Style.marginSmall * 2 * scaling + RowLayout { + id: utilRow + anchors.fill: parent + anchors.margins: Style.marginSmall * scaling + spacing: sidePanel.cardSpacing + Item { Layout.fillWidth: true } + // Record + NIconButton { icon: "fiber_manual_record"; sizeMultiplier: 1.0 } + // Wallpaper + NIconButton { icon: "image"; sizeMultiplier: 1.0 } + Item { Layout.fillWidth: true } + } + } + } + } + } } } } diff --git a/Modules/SidePanel/SystemCard.qml b/Modules/SidePanel/SystemCard.qml new file mode 100644 index 0000000..57e0441 --- /dev/null +++ b/Modules/SidePanel/SystemCard.qml @@ -0,0 +1,40 @@ +import QtQuick +import QtQuick.Layouts +import qs.Services +import qs.Widgets + +// Unified system card: monitors CPU, temp, memory, disk +NBox { + id: root + + readonly property real scaling: Scaling.scale(screen) + + Layout.preferredWidth: 84 * scaling + implicitHeight: content.implicitHeight + Style.marginTiny * 2 * scaling + + Column { + id: content + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.leftMargin: Style.marginSmall * scaling + anchors.rightMargin: Style.marginSmall * scaling + anchors.topMargin: Style.marginTiny * scaling + anchors.bottomMargin: Style.marginMedium * scaling + spacing: Style.marginSmall * scaling + + // Slight top padding + Item { height: Style.marginTiny * scaling } + + NSystemMonitor { id: sysMon; intervalSeconds: 1 } + + NCircleStat { value: sysMon.cpuUsage || SysInfo.cpuUsage; icon: "speed"; flat: true; contentScale: 0.8; width: 72 * scaling; height: 68 * scaling } + NCircleStat { value: sysMon.cpuTemp || SysInfo.cpuTemp; suffix: "°C"; icon: "device_thermostat"; flat: true; contentScale: 0.8; width: 72 * scaling; height: 68 * scaling } + NCircleStat { value: sysMon.memoryUsagePer || SysInfo.memoryUsagePer; icon: "memory"; flat: true; contentScale: 0.8; width: 72 * scaling; height: 68 * scaling } + NCircleStat { value: sysMon.diskUsage || SysInfo.diskUsage; icon: "data_usage"; flat: true; contentScale: 0.8; width: 72 * scaling; height: 68 * scaling } + + // Extra bottom padding to shift the perceived stack slightly upward + Item { height: Style.marginMedium * scaling } + } +} + diff --git a/Modules/SidePanel/WeatherCard.qml b/Modules/SidePanel/WeatherCard.qml new file mode 100644 index 0000000..359f8d1 --- /dev/null +++ b/Modules/SidePanel/WeatherCard.qml @@ -0,0 +1,59 @@ +import QtQuick +import QtQuick.Layouts +import qs.Services +import qs.Widgets + +// Weather overview card (placeholder data) +NBox { + id: root + + readonly property real scaling: Scaling.scale(screen) + + Layout.fillWidth: true + // Height driven by content + implicitHeight: content.implicitHeight + Style.marginLarge * 2 * scaling + + ColumnLayout { + id: content + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Style.marginLarge * scaling + spacing: Style.marginSmall * scaling + + RowLayout { + spacing: Style.marginSmall * scaling + Text { + text: "sunny" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXL * scaling + color: Colors.accentSecondary + } + ColumnLayout { + NText { text: "Dinslaken (GMT+2)" } + NText { text: "26°C"; font.pointSize: (Style.fontSizeXL + 6) * scaling } + } + } + + Rectangle { height: 1; width: parent.width; color: Colors.backgroundTertiary } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginLarge * scaling + Repeater { + model: 5 + delegate: ColumnLayout { + spacing: 2 * scaling + NText { text: ["Sun","Mon","Tue","Wed","Thu"][index] } + Text { + text: index % 2 === 0 ? "wb_sunny" : "cloud" + font.family: "Material Symbols Outlined" + color: Colors.textSecondary + } + NText { text: "26° / 14°"; color: Colors.textSecondary } + } + } + } + } +} + diff --git a/Services/Scaling.qml b/Services/Scaling.qml index 7c15215..b921e14 100644 --- a/Services/Scaling.qml +++ b/Services/Scaling.qml @@ -5,12 +5,26 @@ import Quickshell Singleton { id: root + // Manual override for testing UI scale across the whole shell + // Enable this from the DemoPanel slider + property bool overrideEnabled: false + property real overrideScale: 1.0 + // Design reference resolution (for scale = 1.0) readonly property int designScreenWidth: 2560 readonly property int designScreenHeight: 1440 // Automatic, orientation-agnostic scaling function scale(aScreen) { + // 0) Manual override (for development/testing) + try { + if (overrideEnabled && isFinite(overrideScale)) { + // Clamp to keep UI usable + const clamped = Math.max(0.6, Math.min(1.8, overrideScale)) + return clamped + } + } catch (e) {} + if (typeof aScreen !== 'undefined' & aScreen) { // // 1) Per-monitor override wins diff --git a/Services/Settings.qml b/Services/Settings.qml index feb44b5..61eeafa 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -17,7 +17,7 @@ Singleton { property var data: settingAdapter // Needed to only have one NPanel loaded at a time. - property var openPanel: null + // property var openPanel: null Item { Component.onCompleted: { diff --git a/Widgets/NBox.qml b/Widgets/NBox.qml new file mode 100644 index 0000000..2bc24b0 --- /dev/null +++ b/Widgets/NBox.qml @@ -0,0 +1,20 @@ +import QtQuick +import qs.Services + +// Rounded group container using the variant surface color. +// To be used in side panels and settings panes to group fields or buttons. +Rectangle { + id: root + + readonly property real scaling: Scaling.scale(screen) + + implicitWidth: childrenRect.width + implicitHeight: childrenRect.height + + color: Colors.surfaceVariant + radius: Style.radiusMedium * scaling + border.color: Colors.backgroundTertiary + border.width: Math.min(1, Style.borderThin * scaling) + clip: true +} + diff --git a/Widgets/NCard.qml b/Widgets/NCard.qml new file mode 100644 index 0000000..8901bb2 --- /dev/null +++ b/Widgets/NCard.qml @@ -0,0 +1,18 @@ +import QtQuick +import qs.Services + +// Generic themed card container +Rectangle { + id: root + + readonly property real scaling: Scaling.scale(screen) + + implicitWidth: childrenRect.width + implicitHeight: childrenRect.height + + color: Colors.backgroundSecondary + radius: Style.radiusMedium * scaling + border.color: Colors.backgroundTertiary + border.width: Math.min(1, Style.borderThin * scaling) +} + diff --git a/Widgets/NCircleStat.qml b/Widgets/NCircleStat.qml new file mode 100644 index 0000000..84d6a71 --- /dev/null +++ b/Widgets/NCircleStat.qml @@ -0,0 +1,112 @@ +import QtQuick +import qs.Services + +// Compact circular statistic display used in the SidePanel +Rectangle { + id: root + + readonly property real scaling: Scaling.scale(screen) + property real value: 0 // 0..100 (or any range visually mapped) + property string icon: "" + property string suffix: "%" + + // When nested inside a parent group (NBox), you can make it flat + property bool flat: false + // Scales the internal content (labels, gauge, icon) without changing the + // outer width/height footprint of the component + property real contentScale: 1.0 + + width: 68 * scaling + height: 92 * scaling + color: flat ? "transparent" : Colors.backgroundSecondary + radius: Style.radiusSmall * scaling + border.color: flat ? "transparent" : Colors.backgroundTertiary + border.width: flat ? 0 : Math.min(1, Style.borderThin * scaling) + clip: true + + // Repaint gauge when the bound value changes + onValueChanged: gauge.requestPaint() + + Row { + id: innerRow + anchors.fill: parent + anchors.margins: Style.marginSmall * scaling * contentScale + spacing: Style.marginSmall * scaling * contentScale + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + + // Gauge with percentage label placed inside the open gap (right side) + Item { + id: gaugeWrap + anchors.verticalCenter: innerRow.verticalCenter + width: 68 * scaling * contentScale + height: 68 * scaling * contentScale + + Canvas { + id: gauge + anchors.fill: parent + renderStrategy: Canvas.Cooperative + onPaint: { + const ctx = getContext("2d") + const w = width, h = height + const cx = w / 2, cy = h / 2 + const r = Math.min(w, h) / 2 - 5 * root.scaling * contentScale + // 240° arc with a 120° gap centered on the right side + // Start at 60° and end at 300° → balanced right-side opening + const start = Math.PI / 3 + const endBg = Math.PI * 5 / 3 + ctx.reset() + ctx.lineWidth = 6 * root.scaling * contentScale + // Track uses backgroundPrimary for stronger contrast + ctx.strokeStyle = Colors.backgroundPrimary + ctx.beginPath() + ctx.arc(cx, cy, r, start, endBg) + ctx.stroke() + // Value arc + const ratio = Math.max(0, Math.min(1, root.value / 100)) + const end = start + (endBg - start) * ratio + ctx.strokeStyle = Colors.accentPrimary + ctx.beginPath() + ctx.arc(cx, cy, r, start, end) + ctx.stroke() + } + } + + // Percent centered in the circle + Text { + id: valueLabel + anchors.centerIn: parent + text: `${Math.round(root.value)}${root.suffix}` + font.pointSize: Style.fontSizeMedium * scaling * contentScale + color: Colors.textPrimary + horizontalAlignment: Text.AlignHCenter + } + + // Tiny circular badge for the icon, inside the right-side gap + Rectangle { + id: iconBadge + width: 22 * scaling * contentScale + height: width + radius: width / 2 + color: Colors.backgroundPrimary + border.color: Colors.accentPrimary + border.width: Math.min(1, Style.borderThin * scaling) + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.rightMargin: 4 * scaling * contentScale + anchors.bottomMargin: 4 * scaling * contentScale + + Text { + anchors.centerIn: parent + text: root.icon + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeLarge * scaling * contentScale + color: Colors.accentPrimary + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + } + } + } +} + diff --git a/Widgets/NSystemMonitor.qml b/Widgets/NSystemMonitor.qml new file mode 100644 index 0000000..1cb89da --- /dev/null +++ b/Widgets/NSystemMonitor.qml @@ -0,0 +1,73 @@ +import QtQuick +import Quickshell +import Quickshell.Io + +// Lightweight system monitor using standard Linux interfaces. +// Provides cpu usage %, cpu temperature (°C), and memory usage %. +// No external helpers; uses /proc and /sys via a shell loop. +Item { + id: root + + // Public values + property real cpuUsage: 0 + property real cpuTemp: 0 + property real memoryUsagePer: 0 + property real diskUsage: 0 + + // Interval in seconds between updates + property int intervalSeconds: 1 + + // Background process emitting one JSON line per sample + Process { + id: reader + running: true + command: [ + "sh", "-c", + // Outputs: {"cpu":,"memper":,"cputemp":} + "interval=" + intervalSeconds + "; " + + "while true; do " + + // First /proc/stat snapshot + "read _ u1 n1 s1 id1 iw1 ir1 si1 st1 gs1 < /proc/stat; " + + "t1=$((u1+n1+s1+id1+iw1+ir1+si1+st1)); i1=$((id1+iw1)); " + + "sleep $interval; " + + // Second /proc/stat snapshot + "read _ u2 n2 s2 id2 iw2 ir2 si2 st2 gs2 < /proc/stat; " + + "t2=$((u2+n2+s2+id2+iw2+ir2+si2+st2)); i2=$((id2+iw2)); " + + "dt=$((t2 - t1)); di=$((i2 - i1)); " + + "cpu=$(( (100*(dt - di)) / (dt>0?dt:1) )); " + + // Memory percent via /proc/meminfo (kB) + "mt=$(awk '/MemTotal/ {print $2}' /proc/meminfo); " + + "ma=$(awk '/MemAvailable/ {print $2}' /proc/meminfo); " + + "mm=$((mt - ma)); mp=$(( (100*mm) / (mt>0?mt:1) )); " + + // Temperature: scan hwmon and thermal zones, choose max; convert m°C → °C + "ct=0; " + + "for f in /sys/class/hwmon/hwmon*/temp*_input /sys/class/thermal/thermal_zone*/temp; do " + + "[ -r \"$f\" ] || continue; v=$(cat \"$f\" 2>/dev/null); " + + "[ -z \"$v\" ] && continue; " + + "if [ \"$v\" -gt 1000 ] 2>/dev/null; then v=$((v/1000)); fi; " + + "[ \"$v\" -gt \"$ct\" ] 2>/dev/null && ct=$v; " + + "done; " + + // Disk usage percent for root filesystem + "dp=$(df -P / 2>/dev/null | awk 'NR==2{gsub(/%/,\"\",$5); print $5}'); " + + "[ -z \"$dp\" ] && dp=0; " + + // Emit JSON line + "echo \"{\\\"cpu\\\":$cpu,\\\"memper\\\":$mp,\\\"cputemp\\\":$ct,\\\"diskper\\\":$dp}\"; " + + "done" + ] + + stdout: SplitParser { + onRead: function (line) { + try { + const data = JSON.parse(line) + root.cpuUsage = +data.cpu + root.cpuTemp = +data.cputemp + root.memoryUsagePer = +data.memper + root.diskUsage = +data.diskper + } catch (e) { + // ignore malformed lines + } + } + } + } +} + From c62e199ea7e6fdf3a70f11acf069fc66adcd558e Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 13:26:37 -0400 Subject: [PATCH 055/394] Bringing back 2 helpers --- Helpers/Duration.js | 18 ++ Helpers/FuzzySort.js | 678 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 696 insertions(+) create mode 100644 Helpers/Duration.js create mode 100644 Helpers/FuzzySort.js diff --git a/Helpers/Duration.js b/Helpers/Duration.js new file mode 100644 index 0000000..6402051 --- /dev/null +++ b/Helpers/Duration.js @@ -0,0 +1,18 @@ +// Use to display the time remaining on the Battery widget +function formatVagueHumanReadableDuration(totalSeconds) { + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds - (hours * 3600)) / 60); + const seconds = totalSeconds - (hours * 3600) - (minutes * 60); + + var str = ""; + if (hours) { + str += hours.toString() + "h"; + } + if (minutes) { + str += minutes.toString() + "m"; + } + if (!hours && !minutes) { + str += seconds.toString() + "s"; + } + return str; +} \ No newline at end of file diff --git a/Helpers/FuzzySort.js b/Helpers/FuzzySort.js new file mode 100644 index 0000000..0e1f68b --- /dev/null +++ b/Helpers/FuzzySort.js @@ -0,0 +1,678 @@ +.pragma library + +var single = (search, target) => { + if(!search || !target) return NULL + + var preparedSearch = getPreparedSearch(search) + if(!isPrepared(target)) target = getPrepared(target) + + var searchBitflags = preparedSearch.bitflags + if((searchBitflags & target._bitflags) !== searchBitflags) return NULL + + return algorithm(preparedSearch, target) +} + +var go = (search, targets, options) => { + if(!search) return options?.all ? all(targets, options) : noResults + + var preparedSearch = getPreparedSearch(search) + var searchBitflags = preparedSearch.bitflags + var containsSpace = preparedSearch.containsSpace + + var threshold = denormalizeScore( options?.threshold || 0 ) + var limit = options?.limit || INFINITY + + var resultsLen = 0; var limitedCount = 0 + var targetsLen = targets.length + + function push_result(result) { + if(resultsLen < limit) { q.add(result); ++resultsLen } + else { + ++limitedCount + if(result._score > q.peek()._score) q.replaceTop(result) + } + } + + // This code is copy/pasted 3 times for performance reasons [options.key, options.keys, no keys] + + // options.key + if(options?.key) { + var key = options.key + for(var i = 0; i < targetsLen; ++i) { var obj = targets[i] + var target = getValue(obj, key) + if(!target) continue + if(!isPrepared(target)) target = getPrepared(target) + + if((searchBitflags & target._bitflags) !== searchBitflags) continue + var result = algorithm(preparedSearch, target) + if(result === NULL) continue + if(result._score < threshold) continue + + result.obj = obj + push_result(result) + } + + // options.keys + } else if(options?.keys) { + var keys = options.keys + var keysLen = keys.length + + outer: for(var i = 0; i < targetsLen; ++i) { var obj = targets[i] + + { // early out based on bitflags + var keysBitflags = 0 + for (var keyI = 0; keyI < keysLen; ++keyI) { + var key = keys[keyI] + var target = getValue(obj, key) + if(!target) { tmpTargets[keyI] = noTarget; continue } + if(!isPrepared(target)) target = getPrepared(target) + tmpTargets[keyI] = target + + keysBitflags |= target._bitflags + } + + if((searchBitflags & keysBitflags) !== searchBitflags) continue + } + + if(containsSpace) for(let i=0; i -1000) { + if(keysSpacesBestScores[i] > NEGATIVE_INFINITY) { + var tmp = (keysSpacesBestScores[i] + allowPartialMatchScores[i]) / 4/*bonus score for having multiple matches*/ + if(tmp > keysSpacesBestScores[i]) keysSpacesBestScores[i] = tmp + } + } + if(allowPartialMatchScores[i] > keysSpacesBestScores[i]) keysSpacesBestScores[i] = allowPartialMatchScores[i] + } + } + + if(containsSpace) { + for(let i=0; i -1000) { + if(score > NEGATIVE_INFINITY) { + var tmp = (score + result._score) / 4/*bonus score for having multiple matches*/ + if(tmp > score) score = tmp + } + } + if(result._score > score) score = result._score + } + } + + objResults.obj = obj + objResults._score = score + if(options?.scoreFn) { + score = options.scoreFn(objResults) + if(!score) continue + score = denormalizeScore(score) + objResults._score = score + } + + if(score < threshold) continue + push_result(objResults) + } + + // no keys + } else { + for(var i = 0; i < targetsLen; ++i) { var target = targets[i] + if(!target) continue + if(!isPrepared(target)) target = getPrepared(target) + + if((searchBitflags & target._bitflags) !== searchBitflags) continue + var result = algorithm(preparedSearch, target) + if(result === NULL) continue + if(result._score < threshold) continue + + push_result(result) + } + } + + if(resultsLen === 0) return noResults + var results = new Array(resultsLen) + for(var i = resultsLen - 1; i >= 0; --i) results[i] = q.poll() + results.total = resultsLen + limitedCount + return results +} + + +// this is written as 1 function instead of 2 for minification. perf seems fine ... +// except when minified. the perf is very slow +var highlight = (result, open='', close='') => { + var callback = typeof open === 'function' ? open : undefined + + var target = result.target + var targetLen = target.length + var indexes = result.indexes + var highlighted = '' + var matchI = 0 + var indexesI = 0 + var opened = false + var parts = [] + + for(var i = 0; i < targetLen; ++i) { var char = target[i] + if(indexes[indexesI] === i) { + ++indexesI + if(!opened) { opened = true + if(callback) { + parts.push(highlighted); highlighted = '' + } else { + highlighted += open + } + } + + if(indexesI === indexes.length) { + if(callback) { + highlighted += char + parts.push(callback(highlighted, matchI++)); highlighted = '' + parts.push(target.substr(i+1)) + } else { + highlighted += char + close + target.substr(i+1) + } + break + } + } else { + if(opened) { opened = false + if(callback) { + parts.push(callback(highlighted, matchI++)); highlighted = '' + } else { + highlighted += close + } + } + } + highlighted += char + } + + return callback ? parts : highlighted +} + + +var prepare = (target) => { + if(typeof target === 'number') target = ''+target + else if(typeof target !== 'string') target = '' + var info = prepareLowerInfo(target) + return new_result(target, {_targetLower:info._lower, _targetLowerCodes:info.lowerCodes, _bitflags:info.bitflags}) +} + +var cleanup = () => { preparedCache.clear(); preparedSearchCache.clear() } + + +// Below this point is only internal code +// Below this point is only internal code +// Below this point is only internal code +// Below this point is only internal code + + +class Result { + get ['indexes']() { return this._indexes.slice(0, this._indexes.len).sort((a,b)=>a-b) } + set ['indexes'](indexes) { return this._indexes = indexes } + ['highlight'](open, close) { return highlight(this, open, close) } + get ['score']() { return normalizeScore(this._score) } + set ['score'](score) { this._score = denormalizeScore(score) } +} + +class KeysResult extends Array { + get ['score']() { return normalizeScore(this._score) } + set ['score'](score) { this._score = denormalizeScore(score) } +} + +var new_result = (target, options) => { + const result = new Result() + result['target'] = target + result['obj'] = options.obj ?? NULL + result._score = options._score ?? NEGATIVE_INFINITY + result._indexes = options._indexes ?? [] + result._targetLower = options._targetLower ?? '' + result._targetLowerCodes = options._targetLowerCodes ?? NULL + result._nextBeginningIndexes = options._nextBeginningIndexes ?? NULL + result._bitflags = options._bitflags ?? 0 + return result +} + + +var normalizeScore = score => { + if(score === NEGATIVE_INFINITY) return 0 + if(score > 1) return score + return Math.E ** ( ((-score + 1)**.04307 - 1) * -2) +} +var denormalizeScore = normalizedScore => { + if(normalizedScore === 0) return NEGATIVE_INFINITY + if(normalizedScore > 1) return normalizedScore + return 1 - Math.pow((Math.log(normalizedScore) / -2 + 1), 1 / 0.04307) +} + + +var prepareSearch = (search) => { + if(typeof search === 'number') search = ''+search + else if(typeof search !== 'string') search = '' + search = search.trim() + var info = prepareLowerInfo(search) + + var spaceSearches = [] + if(info.containsSpace) { + var searches = search.split(/\s+/) + searches = [...new Set(searches)] // distinct + for(var i=0; i { + if(target.length > 999) return prepare(target) // don't cache huge targets + var targetPrepared = preparedCache.get(target) + if(targetPrepared !== undefined) return targetPrepared + targetPrepared = prepare(target) + preparedCache.set(target, targetPrepared) + return targetPrepared +} +var getPreparedSearch = (search) => { + if(search.length > 999) return prepareSearch(search) // don't cache huge searches + var searchPrepared = preparedSearchCache.get(search) + if(searchPrepared !== undefined) return searchPrepared + searchPrepared = prepareSearch(search) + preparedSearchCache.set(search, searchPrepared) + return searchPrepared +} + + +var all = (targets, options) => { + var results = []; results.total = targets.length // this total can be wrong if some targets are skipped + + var limit = options?.limit || INFINITY + + if(options?.key) { + for(var i=0;i= limit) return results + } + } else if(options?.keys) { + for(var i=0;i= 0; --keyI) { + var target = getValue(obj, options.keys[keyI]) + if(!target) { objResults[keyI] = noTarget; continue } + if(!isPrepared(target)) target = getPrepared(target) + target._score = NEGATIVE_INFINITY + target._indexes.len = 0 + objResults[keyI] = target + } + objResults.obj = obj + objResults._score = NEGATIVE_INFINITY + results.push(objResults); if(results.length >= limit) return results + } + } else { + for(var i=0;i= limit) return results + } + } + + return results +} + + +var algorithm = (preparedSearch, prepared, allowSpaces=false, allowPartialMatch=false) => { + if(allowSpaces===false && preparedSearch.containsSpace) return algorithmSpaces(preparedSearch, prepared, allowPartialMatch) + + var searchLower = preparedSearch._lower + var searchLowerCodes = preparedSearch.lowerCodes + var searchLowerCode = searchLowerCodes[0] + var targetLowerCodes = prepared._targetLowerCodes + var searchLen = searchLowerCodes.length + var targetLen = targetLowerCodes.length + var searchI = 0 // where we at + var targetI = 0 // where you at + var matchesSimpleLen = 0 + + // very basic fuzzy match; to remove non-matching targets ASAP! + // walk through target. find sequential matches. + // if all chars aren't found then exit + for(;;) { + var isMatch = searchLowerCode === targetLowerCodes[targetI] + if(isMatch) { + matchesSimple[matchesSimpleLen++] = targetI + ++searchI; if(searchI === searchLen) break + searchLowerCode = searchLowerCodes[searchI] + } + ++targetI; if(targetI >= targetLen) return NULL // Failed to find searchI + } + + var searchI = 0 + var successStrict = false + var matchesStrictLen = 0 + + var nextBeginningIndexes = prepared._nextBeginningIndexes + if(nextBeginningIndexes === NULL) nextBeginningIndexes = prepared._nextBeginningIndexes = prepareNextBeginningIndexes(prepared.target) + targetI = matchesSimple[0]===0 ? 0 : nextBeginningIndexes[matchesSimple[0]-1] + + // Our target string successfully matched all characters in sequence! + // Let's try a more advanced and strict test to improve the score + // only count it as a match if it's consecutive or a beginning character! + var backtrackCount = 0 + if(targetI !== targetLen) for(;;) { + if(targetI >= targetLen) { + // We failed to find a good spot for this search char, go back to the previous search char and force it forward + if(searchI <= 0) break // We failed to push chars forward for a better match + + ++backtrackCount; if(backtrackCount > 200) break // exponential backtracking is taking too long, just give up and return a bad match + + --searchI + var lastMatch = matchesStrict[--matchesStrictLen] + targetI = nextBeginningIndexes[lastMatch] + + } else { + var isMatch = searchLowerCodes[searchI] === targetLowerCodes[targetI] + if(isMatch) { + matchesStrict[matchesStrictLen++] = targetI + ++searchI; if(searchI === searchLen) { successStrict = true; break } + ++targetI + } else { + targetI = nextBeginningIndexes[targetI] + } + } + } + + // check if it's a substring match + var substringIndex = searchLen <= 1 ? -1 : prepared._targetLower.indexOf(searchLower, matchesSimple[0]) // perf: this is slow + var isSubstring = !!~substringIndex + var isSubstringBeginning = !isSubstring ? false : substringIndex===0 || prepared._nextBeginningIndexes[substringIndex-1] === substringIndex + + // if it's a substring match but not at a beginning index, let's try to find a substring starting at a beginning index for a better score + if(isSubstring && !isSubstringBeginning) { + for(var i=0; i { + var score = 0 + + var extraMatchGroupCount = 0 + for(var i = 1; i < searchLen; ++i) { + if(matches[i] - matches[i-1] !== 1) {score -= matches[i]; ++extraMatchGroupCount} + } + var unmatchedDistance = matches[searchLen-1] - matches[0] - (searchLen-1) + + score -= (12+unmatchedDistance) * extraMatchGroupCount // penality for more groups + + if(matches[0] !== 0) score -= matches[0]*matches[0]*.2 // penality for not starting near the beginning + + if(!successStrict) { + score *= 1000 + } else { + // successStrict on a target with too many beginning indexes loses points for being a bad target + var uniqueBeginningIndexes = 1 + for(var i = nextBeginningIndexes[0]; i < targetLen; i=nextBeginningIndexes[i]) ++uniqueBeginningIndexes + + if(uniqueBeginningIndexes > 24) score *= (uniqueBeginningIndexes-24)*10 // quite arbitrary numbers here ... + } + + score -= (targetLen - searchLen)/2 // penality for longer targets + + if(isSubstring) score /= 1+searchLen*searchLen*1 // bonus for being a full substring + if(isSubstringBeginning) score /= 1+searchLen*searchLen*1 // bonus for substring starting on a beginningIndex + + score -= (targetLen - searchLen)/2 // penality for longer targets + + return score + } + + if(!successStrict) { + if(isSubstring) for(var i=0; i { + var seen_indexes = new Set() + var score = 0 + var result = NULL + + var first_seen_index_last_search = 0 + var searches = preparedSearch.spaceSearches + var searchesLen = searches.length + var changeslen = 0 + + // Return _nextBeginningIndexes back to its normal state + var resetNextBeginningIndexes = () => { + for(let i=changeslen-1; i>=0; i--) target._nextBeginningIndexes[nextBeginningIndexesChanges[i*2 + 0]] = nextBeginningIndexesChanges[i*2 + 1] + } + + var hasAtLeast1Match = false + for(var i=0; i=0; i--) { + if(toReplace !== target._nextBeginningIndexes[i]) break + target._nextBeginningIndexes[i] = newBeginningIndex + nextBeginningIndexesChanges[changeslen*2 + 0] = i + nextBeginningIndexesChanges[changeslen*2 + 1] = toReplace + changeslen++ + } + } + } + + score += result._score / searchesLen + allowPartialMatchScores[i] = result._score / searchesLen + + // dock points based on order otherwise "c man" returns Manifest.cpp instead of CheatManager.h + if(result._indexes[0] < first_seen_index_last_search) { + score -= (first_seen_index_last_search - result._indexes[0]) * 2 + } + first_seen_index_last_search = result._indexes[0] + + for(var j=0; j score) { + if(allowPartialMatch) { + for(var i=0; i str.replace(/\p{Script=Latin}+/gu, match => match.normalize('NFD')).replace(/[\u0300-\u036f]/g, '') + +var prepareLowerInfo = (str) => { + str = remove_accents(str) + var strLen = str.length + var lower = str.toLowerCase() + var lowerCodes = [] // new Array(strLen) sparse array is too slow + var bitflags = 0 + var containsSpace = false // space isn't stored in bitflags because of how searching with a space works + + for(var i = 0; i < strLen; ++i) { + var lowerCode = lowerCodes[i] = lower.charCodeAt(i) + + if(lowerCode === 32) { + containsSpace = true + continue // it's important that we don't set any bitflags for space + } + + var bit = lowerCode>=97&&lowerCode<=122 ? lowerCode-97 // alphabet + : lowerCode>=48&&lowerCode<=57 ? 26 // numbers + // 3 bits available + : lowerCode<=127 ? 30 // other ascii + : 31 // other utf8 + bitflags |= 1< { + var targetLen = target.length + var beginningIndexes = []; var beginningIndexesLen = 0 + var wasUpper = false + var wasAlphanum = false + for(var i = 0; i < targetLen; ++i) { + var targetCode = target.charCodeAt(i) + var isUpper = targetCode>=65&&targetCode<=90 + var isAlphanum = isUpper || targetCode>=97&&targetCode<=122 || targetCode>=48&&targetCode<=57 + var isBeginning = isUpper && !wasUpper || !wasAlphanum || !isAlphanum + wasUpper = isUpper + wasAlphanum = isAlphanum + if(isBeginning) beginningIndexes[beginningIndexesLen++] = i + } + return beginningIndexes +} +var prepareNextBeginningIndexes = (target) => { + target = remove_accents(target) + var targetLen = target.length + var beginningIndexes = prepareBeginningIndexes(target) + var nextBeginningIndexes = [] // new Array(targetLen) sparse array is too slow + var lastIsBeginning = beginningIndexes[0] + var lastIsBeginningI = 0 + for(var i = 0; i < targetLen; ++i) { + if(lastIsBeginning > i) { + nextBeginningIndexes[i] = lastIsBeginning + } else { + lastIsBeginning = beginningIndexes[++lastIsBeginningI] + nextBeginningIndexes[i] = lastIsBeginning===undefined ? targetLen : lastIsBeginning + } + } + return nextBeginningIndexes +} + +var preparedCache = new Map() +var preparedSearchCache = new Map() + +// the theory behind these being globals is to reduce garbage collection by not making new arrays +var matchesSimple = []; var matchesStrict = [] +var nextBeginningIndexesChanges = [] // allows straw berry to match strawberry well, by modifying the end of a substring to be considered a beginning index for the rest of the search +var keysSpacesBestScores = []; var allowPartialMatchScores = [] +var tmpTargets = []; var tmpResults = [] + +// prop = 'key' 2.5ms optimized for this case, seems to be about as fast as direct obj[prop] +// prop = 'key1.key2' 10ms +// prop = ['key1', 'key2'] 27ms +// prop = obj => obj.tags.join() ??ms +var getValue = (obj, prop) => { + var tmp = obj[prop]; if(tmp !== undefined) return tmp + if(typeof prop === 'function') return prop(obj) // this should run first. but that makes string props slower + var segs = prop + if(!Array.isArray(prop)) segs = prop.split('.') + var len = segs.length + var i = -1 + while (obj && (++i < len)) obj = obj[segs[i]] + return obj +} + +var isPrepared = (x) => { return typeof x === 'object' && typeof x._bitflags === 'number' } +var INFINITY = Infinity; var NEGATIVE_INFINITY = -INFINITY +var noResults = []; noResults.total = 0 +var NULL = null + +var noTarget = prepare('') + +// Hacked version of https://github.com/lemire/FastPriorityQueue.js +var fastpriorityqueue=r=>{var e=[],o=0,a={},v=r=>{for(var a=0,v=e[a],c=1;c>1]=e[a],c=1+(a<<1)}for(var f=a-1>>1;a>0&&v._score>1)e[a]=e[f];e[a]=v};return a.add=(r=>{var a=o;e[o++]=r;for(var v=a-1>>1;a>0&&r._score>1)e[a]=e[v];e[a]=r}),a.poll=(r=>{if(0!==o){var a=e[0];return e[0]=e[--o],v(),a}}),a.peek=(r=>{if(0!==o)return e[0]}),a.replaceTop=(r=>{e[0]=r,v()}),a} +var q = fastpriorityqueue() // reuse this \ No newline at end of file From 1d13bbe0f777d6dca6f55a7d776c8f8f4a7afcff Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 13:26:46 -0400 Subject: [PATCH 056/394] Code formatting --- Modules/SidePanel/MediaCard.qml | 9 +++-- Modules/SidePanel/ProfileCard.qml | 26 +++++++++---- Modules/SidePanel/SidePanel.qml | 65 +++++++++++++++++++++---------- Modules/SidePanel/SystemCard.qml | 51 ++++++++++++++++++++---- Modules/SidePanel/WeatherCard.qml | 25 +++++++++--- Services/Scaling.qml | 4 +- Services/Settings.qml | 3 +- Widgets/NBox.qml | 1 - Widgets/NCard.qml | 1 - Widgets/NCircleStat.qml | 3 +- Widgets/NSystemMonitor.qml | 58 ++++++++++++--------------- 11 files changed, 161 insertions(+), 85 deletions(-) diff --git a/Modules/SidePanel/MediaCard.qml b/Modules/SidePanel/MediaCard.qml index 193b579..6e574f9 100644 --- a/Modules/SidePanel/MediaCard.qml +++ b/Modules/SidePanel/MediaCard.qml @@ -22,7 +22,9 @@ NBox { anchors.margins: Style.marginXL * scaling spacing: Style.marginSmall * scaling - Item { height: 36 * scaling } + Item { + height: 36 * scaling + } Text { text: "music_note" @@ -38,7 +40,8 @@ NBox { anchors.horizontalCenter: parent.horizontalCenter } - Item { height: 36 * scaling } + Item { + height: 36 * scaling + } } } - diff --git a/Modules/SidePanel/ProfileCard.qml b/Modules/SidePanel/ProfileCard.qml index c04da96..cd75e56 100644 --- a/Modules/SidePanel/ProfileCard.qml +++ b/Modules/SidePanel/ProfileCard.qml @@ -27,7 +27,7 @@ NBox { id: avatarBox width: 40 * scaling height: 40 * scaling - + Image { id: avatarImage anchors.fill: parent @@ -52,17 +52,29 @@ NBox { ColumnLayout { Layout.fillWidth: true spacing: 2 * scaling - NText { text: Quickshell.env("USER") || "user" } - NText { text: "System Uptime: —"; color: Colors.textSecondary } + NText { + text: Quickshell.env("USER") || "user" + } + NText { + text: "System Uptime: —" + color: Colors.textSecondary + } } RowLayout { spacing: Style.marginSmall * scaling Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - Item { Layout.fillWidth: true } - NIconButton { icon: "settings"; sizeMultiplier: 0.8 } - NIconButton { icon: "power_settings_new"; sizeMultiplier: 0.8 } + Item { + Layout.fillWidth: true + } + NIconButton { + icon: "settings" + sizeMultiplier: 0.8 + } + NIconButton { + icon: "power_settings_new" + sizeMultiplier: 0.8 + } } } } - diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml index 541f171..39796b7 100644 --- a/Modules/SidePanel/SidePanel.qml +++ b/Modules/SidePanel/SidePanel.qml @@ -6,7 +6,6 @@ import Quickshell.Wayland import qs.Services import qs.Widgets - NLoader { id: root @@ -46,7 +45,6 @@ NLoader { Component.onCompleted: show() // Inline helpers moved to dedicated widgets: NCard and NCircleStat - Rectangle { id: panelBackground color: Colors.backgroundPrimary @@ -61,13 +59,14 @@ NLoader { // Place the panel just below the bar (overlay content starts below bar due to topMargin) y: Style.marginSmall * scaling // Center horizontally under the anchorX, clamped to the screen bounds - x: Math.max( - Style.marginSmall * scaling, - Math.min(parent.width - width - Style.marginSmall * scaling, + x: Math.max(Style.marginSmall * scaling, Math.min( + parent.width - width - Style.marginSmall * scaling, Math.round(anchorX - width / 2))) // Prevent closing when clicking in the panel bg - MouseArea { anchors.fill: parent } + MouseArea { + anchors.fill: parent + } // Content wrapper to ensure childrenRect drives implicit height Item { @@ -88,8 +87,14 @@ NLoader { spacing: sidePanel.cardSpacing // Cards (consistent inter-card spacing via ColumnLayout spacing) - ProfileCard { Layout.topMargin: 0; Layout.bottomMargin: 0 } - WeatherCard { Layout.topMargin: 0; Layout.bottomMargin: 0 } + ProfileCard { + Layout.topMargin: 0 + Layout.bottomMargin: 0 + } + WeatherCard { + Layout.topMargin: 0 + Layout.bottomMargin: 0 + } // Middle section: media + stats column RowLayout { @@ -99,10 +104,16 @@ NLoader { spacing: sidePanel.cardSpacing // Media card - MediaCard { id: mediaCard; Layout.fillWidth: true; implicitHeight: statsCard.implicitHeight } + MediaCard { + id: mediaCard + Layout.fillWidth: true + implicitHeight: statsCard.implicitHeight + } // System monitors combined in one card - SystemCard { id: statsCard } + SystemCard { + id: statsCard + } } // Bottom actions (two grouped rows of round buttons) @@ -111,7 +122,7 @@ NLoader { Layout.topMargin: 0 Layout.bottomMargin: 0 spacing: sidePanel.cardSpacing - + // Power Profiles: performance, balanced, eco NBox { Layout.fillWidth: true @@ -122,26 +133,30 @@ NLoader { anchors.fill: parent anchors.margins: Style.marginSmall * scaling spacing: sidePanel.cardSpacing - Item { Layout.fillWidth: true } + Item { + Layout.fillWidth: true + } // Performance NIconButton { icon: "speed" sizeMultiplier: 1.0 - onClicked: function () { /* TODO: hook to power profile */ } + onClicked: function () {/* TODO: hook to power profile */ } } // Balanced NIconButton { icon: "balance" sizeMultiplier: 1.0 - onClicked: function () { /* TODO: hook to power profile */ } + onClicked: function () {/* TODO: hook to power profile */ } } // Eco NIconButton { icon: "eco" sizeMultiplier: 1.0 - onClicked: function () { /* TODO: hook to power profile */ } + onClicked: function () {/* TODO: hook to power profile */ } + } + Item { + Layout.fillWidth: true } - Item { Layout.fillWidth: true } } } @@ -155,12 +170,22 @@ NLoader { anchors.fill: parent anchors.margins: Style.marginSmall * scaling spacing: sidePanel.cardSpacing - Item { Layout.fillWidth: true } + Item { + Layout.fillWidth: true + } // Record - NIconButton { icon: "fiber_manual_record"; sizeMultiplier: 1.0 } + NIconButton { + icon: "fiber_manual_record" + sizeMultiplier: 1.0 + } // Wallpaper - NIconButton { icon: "image"; sizeMultiplier: 1.0 } - Item { Layout.fillWidth: true } + NIconButton { + icon: "image" + sizeMultiplier: 1.0 + } + Item { + Layout.fillWidth: true + } } } } diff --git a/Modules/SidePanel/SystemCard.qml b/Modules/SidePanel/SystemCard.qml index 57e0441..f66c405 100644 --- a/Modules/SidePanel/SystemCard.qml +++ b/Modules/SidePanel/SystemCard.qml @@ -24,17 +24,52 @@ NBox { spacing: Style.marginSmall * scaling // Slight top padding - Item { height: Style.marginTiny * scaling } + Item { + height: Style.marginTiny * scaling + } - NSystemMonitor { id: sysMon; intervalSeconds: 1 } + NSystemMonitor { + id: sysMon + intervalSeconds: 1 + } - NCircleStat { value: sysMon.cpuUsage || SysInfo.cpuUsage; icon: "speed"; flat: true; contentScale: 0.8; width: 72 * scaling; height: 68 * scaling } - NCircleStat { value: sysMon.cpuTemp || SysInfo.cpuTemp; suffix: "°C"; icon: "device_thermostat"; flat: true; contentScale: 0.8; width: 72 * scaling; height: 68 * scaling } - NCircleStat { value: sysMon.memoryUsagePer || SysInfo.memoryUsagePer; icon: "memory"; flat: true; contentScale: 0.8; width: 72 * scaling; height: 68 * scaling } - NCircleStat { value: sysMon.diskUsage || SysInfo.diskUsage; icon: "data_usage"; flat: true; contentScale: 0.8; width: 72 * scaling; height: 68 * scaling } + NCircleStat { + value: sysMon.cpuUsage || SysInfo.cpuUsage + icon: "speed" + flat: true + contentScale: 0.8 + width: 72 * scaling + height: 68 * scaling + } + NCircleStat { + value: sysMon.cpuTemp || SysInfo.cpuTemp + suffix: "°C" + icon: "device_thermostat" + flat: true + contentScale: 0.8 + width: 72 * scaling + height: 68 * scaling + } + NCircleStat { + value: sysMon.memoryUsagePer || SysInfo.memoryUsagePer + icon: "memory" + flat: true + contentScale: 0.8 + width: 72 * scaling + height: 68 * scaling + } + NCircleStat { + value: sysMon.diskUsage || SysInfo.diskUsage + icon: "data_usage" + flat: true + contentScale: 0.8 + width: 72 * scaling + height: 68 * scaling + } // Extra bottom padding to shift the perceived stack slightly upward - Item { height: Style.marginMedium * scaling } + Item { + height: Style.marginMedium * scaling + } } } - diff --git a/Modules/SidePanel/WeatherCard.qml b/Modules/SidePanel/WeatherCard.qml index 359f8d1..a9881fc 100644 --- a/Modules/SidePanel/WeatherCard.qml +++ b/Modules/SidePanel/WeatherCard.qml @@ -30,12 +30,21 @@ NBox { color: Colors.accentSecondary } ColumnLayout { - NText { text: "Dinslaken (GMT+2)" } - NText { text: "26°C"; font.pointSize: (Style.fontSizeXL + 6) * scaling } + NText { + text: "Dinslaken (GMT+2)" + } + NText { + text: "26°C" + font.pointSize: (Style.fontSizeXL + 6) * scaling + } } } - Rectangle { height: 1; width: parent.width; color: Colors.backgroundTertiary } + Rectangle { + height: 1 + width: parent.width + color: Colors.backgroundTertiary + } RowLayout { Layout.fillWidth: true @@ -44,16 +53,20 @@ NBox { model: 5 delegate: ColumnLayout { spacing: 2 * scaling - NText { text: ["Sun","Mon","Tue","Wed","Thu"][index] } + NText { + text: ["Sun", "Mon", "Tue", "Wed", "Thu"][index] + } Text { text: index % 2 === 0 ? "wb_sunny" : "cloud" font.family: "Material Symbols Outlined" color: Colors.textSecondary } - NText { text: "26° / 14°"; color: Colors.textSecondary } + NText { + text: "26° / 14°" + color: Colors.textSecondary + } } } } } } - diff --git a/Services/Scaling.qml b/Services/Scaling.qml index b921e14..912cee5 100644 --- a/Services/Scaling.qml +++ b/Services/Scaling.qml @@ -23,7 +23,9 @@ Singleton { const clamped = Math.max(0.6, Math.min(1.8, overrideScale)) return clamped } - } catch (e) {} + } catch (e) { + + } if (typeof aScreen !== 'undefined' & aScreen) { diff --git a/Services/Settings.qml b/Services/Settings.qml index 61eeafa..738a14f 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -17,8 +17,7 @@ Singleton { property var data: settingAdapter // Needed to only have one NPanel loaded at a time. - // property var openPanel: null - + // property var openPanel: null Item { Component.onCompleted: { // ensure settings dir diff --git a/Widgets/NBox.qml b/Widgets/NBox.qml index 2bc24b0..aa5a28d 100644 --- a/Widgets/NBox.qml +++ b/Widgets/NBox.qml @@ -17,4 +17,3 @@ Rectangle { border.width: Math.min(1, Style.borderThin * scaling) clip: true } - diff --git a/Widgets/NCard.qml b/Widgets/NCard.qml index 8901bb2..d0ff71d 100644 --- a/Widgets/NCard.qml +++ b/Widgets/NCard.qml @@ -15,4 +15,3 @@ Rectangle { border.color: Colors.backgroundTertiary border.width: Math.min(1, Style.borderThin * scaling) } - diff --git a/Widgets/NCircleStat.qml b/Widgets/NCircleStat.qml index 84d6a71..6ef5eaa 100644 --- a/Widgets/NCircleStat.qml +++ b/Widgets/NCircleStat.qml @@ -6,7 +6,7 @@ Rectangle { id: root readonly property real scaling: Scaling.scale(screen) - property real value: 0 // 0..100 (or any range visually mapped) + property real value: 0 // 0..100 (or any range visually mapped) property string icon: "" property string suffix: "%" @@ -109,4 +109,3 @@ Rectangle { } } } - diff --git a/Widgets/NSystemMonitor.qml b/Widgets/NSystemMonitor.qml index 1cb89da..05b4083 100644 --- a/Widgets/NSystemMonitor.qml +++ b/Widgets/NSystemMonitor.qml @@ -21,39 +21,29 @@ Item { Process { id: reader running: true - command: [ - "sh", "-c", - // Outputs: {"cpu":,"memper":,"cputemp":} - "interval=" + intervalSeconds + "; " + - "while true; do " + - // First /proc/stat snapshot - "read _ u1 n1 s1 id1 iw1 ir1 si1 st1 gs1 < /proc/stat; " + - "t1=$((u1+n1+s1+id1+iw1+ir1+si1+st1)); i1=$((id1+iw1)); " + - "sleep $interval; " + - // Second /proc/stat snapshot - "read _ u2 n2 s2 id2 iw2 ir2 si2 st2 gs2 < /proc/stat; " + - "t2=$((u2+n2+s2+id2+iw2+ir2+si2+st2)); i2=$((id2+iw2)); " + - "dt=$((t2 - t1)); di=$((i2 - i1)); " + - "cpu=$(( (100*(dt - di)) / (dt>0?dt:1) )); " + - // Memory percent via /proc/meminfo (kB) - "mt=$(awk '/MemTotal/ {print $2}' /proc/meminfo); " + - "ma=$(awk '/MemAvailable/ {print $2}' /proc/meminfo); " + - "mm=$((mt - ma)); mp=$(( (100*mm) / (mt>0?mt:1) )); " + - // Temperature: scan hwmon and thermal zones, choose max; convert m°C → °C - "ct=0; " + - "for f in /sys/class/hwmon/hwmon*/temp*_input /sys/class/thermal/thermal_zone*/temp; do " + - "[ -r \"$f\" ] || continue; v=$(cat \"$f\" 2>/dev/null); " + - "[ -z \"$v\" ] && continue; " + - "if [ \"$v\" -gt 1000 ] 2>/dev/null; then v=$((v/1000)); fi; " + - "[ \"$v\" -gt \"$ct\" ] 2>/dev/null && ct=$v; " + - "done; " + - // Disk usage percent for root filesystem - "dp=$(df -P / 2>/dev/null | awk 'NR==2{gsub(/%/,\"\",$5); print $5}'); " + - "[ -z \"$dp\" ] && dp=0; " + - // Emit JSON line - "echo \"{\\\"cpu\\\":$cpu,\\\"memper\\\":$mp,\\\"cputemp\\\":$ct,\\\"diskper\\\":$dp}\"; " + - "done" - ] + command: ["sh", "-c", // Outputs: {"cpu":,"memper":,"cputemp":} + "interval=" + intervalSeconds + "; " + "while true; do " + // First /proc/stat snapshot + "read _ u1 n1 s1 id1 iw1 ir1 si1 st1 gs1 < /proc/stat; " + + "t1=$((u1+n1+s1+id1+iw1+ir1+si1+st1)); i1=$((id1+iw1)); " + + "sleep $interval; " + // Second /proc/stat snapshot + "read _ u2 n2 s2 id2 iw2 ir2 si2 st2 gs2 < /proc/stat; " + + "t2=$((u2+n2+s2+id2+iw2+ir2+si2+st2)); i2=$((id2+iw2)); " + + "dt=$((t2 - t1)); di=$((i2 - i1)); " + "cpu=$(( (100*(dt - di)) / (dt>0?dt:1) )); " + + // Memory percent via /proc/meminfo (kB) + "mt=$(awk '/MemTotal/ {print $2}' /proc/meminfo); " + + "ma=$(awk '/MemAvailable/ {print $2}' /proc/meminfo); " + + "mm=$((mt - ma)); mp=$(( (100*mm) / (mt>0?mt:1) )); " + + // Temperature: scan hwmon and thermal zones, choose max; convert m°C → °C + "ct=0; " + + "for f in /sys/class/hwmon/hwmon*/temp*_input /sys/class/thermal/thermal_zone*/temp; do " + + "[ -r \"$f\" ] || continue; v=$(cat \"$f\" 2>/dev/null); " + "[ -z \"$v\" ] && continue; " + + "if [ \"$v\" -gt 1000 ] 2>/dev/null; then v=$((v/1000)); fi; " + + "[ \"$v\" -gt \"$ct\" ] 2>/dev/null && ct=$v; " + "done; " + + // Disk usage percent for root filesystem + "dp=$(df -P / 2>/dev/null | awk 'NR==2{gsub(/%/,\"\",$5); print $5}'); " + + "[ -z \"$dp\" ] && dp=0; " + // Emit JSON line + "echo \"{\\\"cpu\\\":$cpu,\\\"memper\\\":$mp,\\\"cputemp\\\":$ct,\\\"diskper\\\":$dp}\"; " + + "done"] stdout: SplitParser { onRead: function (line) { @@ -64,10 +54,10 @@ Item { root.memoryUsagePer = +data.memper root.diskUsage = +data.diskper } catch (e) { + // ignore malformed lines } } } } } - From 8b31da594c6f0abe169786b3a3d8a533f1ee04af Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 13:29:37 -0400 Subject: [PATCH 057/394] DemoPanel proper tooltip --- Modules/DemoPanel/DemoPanel.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/DemoPanel/DemoPanel.qml b/Modules/DemoPanel/DemoPanel.qml index 38a1a86..8236509 100644 --- a/Modules/DemoPanel/DemoPanel.qml +++ b/Modules/DemoPanel/DemoPanel.qml @@ -50,6 +50,7 @@ NLoader { onEntered: function() { myTooltip.show() } onExited: function() { myTooltip.hide() } } + NTooltip { id: myTooltip; target: myIconButton; positionAbove: false; text: "Hello world"; } NDivider { Layout.fillWidth: true } } @@ -66,7 +67,7 @@ NLoader { onToggled: function(value: bool) { console.log("NToggle: " + value) } } - NTooltip { id: myTooltip; target: myIconButton; positionAbove: false; text: "Hello world" } + NDivider { Layout.fillWidth: true } } From aaf2aba0f835e74b74f066ae0465e9706de4be46 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 13:49:27 -0400 Subject: [PATCH 058/394] Everything in Bar --- Modules/Background/Background.qml | 2 +- Modules/Background/Overview.qml | 2 +- Modules/Bar/Bar.qml | 195 ++++++++++++++++-------------- Modules/Bar/Bars.qml | 10 -- shell.qml | 2 +- 5 files changed, 108 insertions(+), 103 deletions(-) delete mode 100644 Modules/Bar/Bars.qml diff --git a/Modules/Background/Background.qml b/Modules/Background/Background.qml index f81f79b..055cfcc 100644 --- a/Modules/Background/Background.qml +++ b/Modules/Background/Background.qml @@ -6,7 +6,7 @@ import qs.Services Variants { model: Quickshell.screens - PanelWindow { + delegate: PanelWindow { required property ShellScreen modelData property string wallpaperSource: Qt.resolvedUrl( "../../Assets/Tests/wallpaper.png") diff --git a/Modules/Background/Overview.qml b/Modules/Background/Overview.qml index 2d9cea8..438e612 100644 --- a/Modules/Background/Overview.qml +++ b/Modules/Background/Overview.qml @@ -7,7 +7,7 @@ import qs.Services Variants { model: Quickshell.screens - PanelWindow { + delegate: PanelWindow { required property ShellScreen modelData property string wallpaperSource: Qt.resolvedUrl( "../../Assets/Tests/wallpaper.png") diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 5b91d8d..9f3669d 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -1,104 +1,119 @@ import QtQuick -import Quickshell import QtQuick.Controls import QtQuick.Layouts -import qs.Widgets +import Quickshell import qs.Services +import qs.Widgets -PanelWindow { - id: root +Variants { + model: Quickshell.screens - readonly property real scaling: Scaling.scale(screen) - property var modelData + delegate: PanelWindow { + id: root - screen: modelData - implicitHeight: Style.barHeight * scaling - color: "transparent" - visible: Settings.data.bar.monitors.includes(modelData.name) - || (Settings.data.bar.monitors.length === 0) + required property ShellScreen modelData + readonly property real scaling: Scaling.scale(screen) - anchors { - top: true - left: true - right: true - } + screen: modelData + implicitHeight: Style.barHeight * scaling + color: "transparent" + visible: Settings.data.bar.monitors.includes(modelData.name) || (Settings.data.bar.monitors.length === 0) - Item { - anchors.fill: parent - clip: true - - // Background fill - Rectangle { - id: bar - anchors.fill: parent - color: Colors.backgroundPrimary - layer.enabled: true - } - - Row { - id: leftSection - height: parent.height - anchors.left: parent.left - anchors.leftMargin: Style.marginSmall * scaling - anchors.verticalCenter: parent.verticalCenter - spacing: Style.marginSmall * scaling - - NText { - text: screen.name - anchors.verticalCenter: parent.verticalCenter - } - } - - Row { - id: centerSection - height: parent.height - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - spacing: Style.marginSmall * scaling - - Workspace {} - } - - Row { - id: rightSection - height: parent.height - anchors.right: bar.right - anchors.rightMargin: Style.marginSmall * scaling - anchors.verticalCenter: bar.verticalCenter - spacing: Style.marginSmall * scaling - - Clock { - anchors.verticalCenter: parent.verticalCenter - } - - NIconButton { - id: demoPanelToggle - icon: "experiment" - anchors.verticalCenter: parent.verticalCenter - onClicked: function () { - demoPanel.isLoaded = !demoPanel.isLoaded + anchors { + top: true + left: true + right: true } - } - NIconButton { - id: sidePanelToggle - icon: "widgets" - anchors.verticalCenter: parent.verticalCenter - onClicked: function () { - // Map this button's center to the screen and open the side panel below it - const localCenterX = width / 2 - const localCenterY = height / 2 - const globalPoint = mapToItem(null, localCenterX, localCenterY) - if (sidePanel.isLoaded) { - sidePanel.isLoaded = false - } else if (sidePanel.openAt) { - sidePanel.openAt(globalPoint.x, screen) - } else { - // Fallback: toggle if API unavailable - sidePanel.isLoaded = true - } + Item { + anchors.fill: parent + clip: true + + // Background fill + Rectangle { + id: bar + + anchors.fill: parent + color: Colors.backgroundPrimary + layer.enabled: true + } + + Row { + id: leftSection + + height: parent.height + anchors.left: parent.left + anchors.leftMargin: Style.marginSmall * scaling + anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginSmall * scaling + + NText { + text: screen.name + anchors.verticalCenter: parent.verticalCenter + } + + } + + Row { + id: centerSection + + height: parent.height + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginSmall * scaling + + Workspace { + } + + } + + Row { + id: rightSection + + height: parent.height + anchors.right: bar.right + anchors.rightMargin: Style.marginSmall * scaling + anchors.verticalCenter: bar.verticalCenter + spacing: Style.marginSmall * scaling + + Clock { + anchors.verticalCenter: parent.verticalCenter + } + + NIconButton { + id: demoPanelToggle + + icon: "experiment" + anchors.verticalCenter: parent.verticalCenter + onClicked: function() { + demoPanel.isLoaded = !demoPanel.isLoaded; + } + } + + NIconButton { + id: sidePanelToggle + + icon: "widgets" + anchors.verticalCenter: parent.verticalCenter + onClicked: function() { + // Map this button's center to the screen and open the side panel below it + const localCenterX = width / 2; + const localCenterY = height / 2; + const globalPoint = mapToItem(null, localCenterX, localCenterY); + if (sidePanel.isLoaded) + sidePanel.isLoaded = false; + else if (sidePanel.openAt) + sidePanel.openAt(globalPoint.x, screen); + else + // Fallback: toggle if API unavailable + sidePanel.isLoaded = true; + } + } + + } + } - } + } - } + } diff --git a/Modules/Bar/Bars.qml b/Modules/Bar/Bars.qml deleted file mode 100644 index 0b549e4..0000000 --- a/Modules/Bar/Bars.qml +++ /dev/null @@ -1,10 +0,0 @@ -import Quickshell -import qs.Modules.Bar - -Variants { - model: Quickshell.screens - - delegate: Bar { - modelData: item - } -} diff --git a/shell.qml b/shell.qml index 54db807..8d4748b 100644 --- a/shell.qml +++ b/shell.qml @@ -17,7 +17,7 @@ ShellRoot { Background {} Overview {} ScreenCorners {} - Bars {} + Bar {} DemoPanel { id: demoPanel From 2f691fc72afb89619cde1b68b1f4ef213351a56a Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 14:16:03 -0400 Subject: [PATCH 059/394] NPill + Battery (WIP) --- Modules/Bar/Bar.qml | 4 + Modules/Bar/Battery.qml | 119 +++++++++++++++++++++++ Services/Style.qml | 5 +- Widgets/NPill.qml | 202 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 329 insertions(+), 1 deletion(-) create mode 100644 Modules/Bar/Battery.qml create mode 100644 Widgets/NPill.qml diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 9f3669d..9492961 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -76,6 +76,10 @@ Variants { anchors.verticalCenter: bar.verticalCenter spacing: Style.marginSmall * scaling + Battery { + anchors.verticalCenter: parent.verticalCenter + } + Clock { anchors.verticalCenter: parent.verticalCenter } diff --git a/Modules/Bar/Battery.qml b/Modules/Bar/Battery.qml new file mode 100644 index 0000000..7b1f33a --- /dev/null +++ b/Modules/Bar/Battery.qml @@ -0,0 +1,119 @@ +import QtQuick +import Quickshell +import Quickshell.Services.UPower +import QtQuick.Layouts +import qs.Services +import qs.Widgets +import "../../Helpers/Duration.js" as Duration + +Item { + id: root + + // Test mode + property bool testMode: true + property int testPercent: 49 + property bool testCharging: false + + property var battery: UPower.displayDevice + property bool isReady: testMode ? true : (battery && battery.ready && battery.isLaptopBattery && battery.isPresent) + property real percent: testMode ? testPercent : (isReady ? (battery.percentage * 100) : 0) + property bool charging: testMode ? testCharging : (isReady ? battery.state === UPowerDeviceState.Charging : false) + property bool show: isReady && percent > 0 + + // Choose icon based on charge and charging state + function batteryIcon() { + if (!show) + return ""; + + if (charging) + return "battery_android_bolt"; + + if (percent >= 95) + return "battery_android_full"; + + // Hardcoded battery symbols + if (percent >= 85) + return "battery_android_6"; + if (percent >= 70) + return "battery_android_5"; + if (percent >= 55) + return "battery_android_4"; + if (percent >= 40) + return "battery_android_3"; + if (percent >= 25) + return "battery_android_2"; + if (percent >= 10) + return "battery_android_1"; + if (percent >= 0) + return "battery_android_0"; + } + + visible: testMode || (isReady && battery.isLaptopBattery) + width: pill.width + height: pill.height + + NPill { + id: pill + icon: root.batteryIcon() + text: Math.round(root.percent) + "%" + pillColor: Colors.surfaceVariant + iconCircleColor: Colors.accentPrimary + iconTextColor: Colors.backgroundPrimary + textColor: charging ? Colors.accentPrimary : Colors.textPrimary + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: { + pill.showDelayed(); + batteryTooltip.show(); + } + onExited: { + pill.hide(); + batteryTooltip.show(); + } + } + NTooltip { + id: batteryTooltip + positionAbove: false + target: pill + delay: Style.tooltipDelayLong + text: { + let lines = []; + if (!root.isReady) { + return ""; + } + + if (root.battery.timeToEmpty > 0) { + lines.push("Time left: " + Time.formatVagueHumanReadableTime(root.battery.timeToEmpty)); + } + + if (root.battery.timeToFull > 0) { + lines.push("Time until full: " + Time.formatVagueHumanReadableTime(root.battery.timeToFull)); + } + + if (root.battery.changeRate !== undefined) { + const rate = root.battery.changeRate; + if (rate > 0) { + lines.push(root.charging ? "Charging rate: " + rate.toFixed(2) + " W" : "Discharging rate: " + rate.toFixed(2) + " W"); + } + else if (rate < 0) { + lines.push("Discharging rate: " + Math.abs(rate).toFixed(2) + " W"); + } + else { + lines.push("Estimating..."); + } + } + else { + lines.push(root.charging ? "Charging" : "Discharging"); + } + + + if (root.battery.healthPercentage !== undefined && root.battery.healthPercentage > 0) { + lines.push("Health: " + Math.round(root.battery.healthPercentage) + "%"); + } + return lines.join("\n"); + } + + } + } +} \ No newline at end of file diff --git a/Services/Style.qml b/Services/Style.qml index 12b0b6e..44c2031 100644 --- a/Services/Style.qml +++ b/Services/Style.qml @@ -44,12 +44,15 @@ Singleton { property int animationNormal: 300 property int animationSlow: 500 + // Dimensions property int barHeight: 36 property int baseWidgetSize: 32 property int sliderWidth: 200 - // Delay + // Delays property int tooltipDelay: 300 + property int tooltipDelayLong: 1500 + property int pillDelay: 500 // Margins and spacing property int marginTiny: 4 diff --git a/Widgets/NPill.qml b/Widgets/NPill.qml new file mode 100644 index 0000000..71aaa06 --- /dev/null +++ b/Widgets/NPill.qml @@ -0,0 +1,202 @@ +import QtQuick +import QtQuick.Controls +import qs.Services + +Item { + id: revealPill + + readonly property real scaling: Scaling.scale(screen) + + property string icon: "" + property string text: "" + property color pillColor: Colors.surfaceVariant + property color textColor: Colors.textPrimary + property color iconCircleColor: Colors.accentPrimary + property color iconTextColor: Colors.backgroundPrimary + property color collapsedIconColor: Colors.textPrimary + property real sizeMultiplier: 0.8 + property int pillHeight: Style.baseWidgetSize * sizeMultiplier * scaling + property int iconSize: Style.baseWidgetSize * sizeMultiplier * scaling + property int pillPaddingHorizontal: 14 * scaling + property bool autoHide: false + + // Internal state + property bool showPill: false + property bool shouldAnimateHide: false + + // Exposed width logic + readonly property int pillOverlap: iconSize / 2 + readonly property int maxPillWidth: Math.max(1, textItem.implicitWidth + pillPaddingHorizontal * 2 + pillOverlap) + + signal shown + signal hidden + + width: iconSize + (showPill ? maxPillWidth - pillOverlap : 0) + height: pillHeight + + Rectangle { + id: pill + width: showPill ? maxPillWidth : 1 + height: pillHeight + x: (iconCircle.x + iconCircle.width / 2) - width + opacity: showPill ? 1 : 0 + color: pillColor + topLeftRadius: pillHeight / 2 + bottomLeftRadius: pillHeight / 2 + anchors.verticalCenter: parent.verticalCenter + + Text { + id: textItem + anchors.centerIn: parent + text: revealPill.text + font.pointSize: Colors.fontSizeSmall * scaling + font.family: Settings.data.ui.fontFamily + font.weight: Font.Bold + color: textColor + visible: showPill + } + + Behavior on width { + enabled: showAnim.running || hideAnim.running + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + Behavior on opacity { + enabled: showAnim.running || hideAnim.running + NumberAnimation { + duration: 250 + easing.type: Easing.OutCubic + } + } + } + + Rectangle { + id: iconCircle + width: iconSize + height: iconSize + radius: width / 2 + color: showPill ? iconCircleColor : "transparent" + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + + Behavior on color { + ColorAnimation { + duration: 200 + easing.type: Easing.InOutQuad + } + } + + Text { + anchors.centerIn: parent + font.family: showPill ? "Material Symbols Rounded" : "Material Symbols Outlined" + font.pointSize: Colors.fontSizeSmall * scaling + text: revealPill.icon + color: showPill ? iconTextColor : collapsedIconColor + } + } + + ParallelAnimation { + id: showAnim + running: false + NumberAnimation { + target: pill + property: "width" + from: 1 + to: maxPillWidth + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + NumberAnimation { + target: pill + property: "opacity" + from: 0 + to: 1 + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + onStarted: { + showPill = true; + } + onStopped: { + delayedHideAnim.start(); + shown(); + } + } + + SequentialAnimation { + id: delayedHideAnim + running: false + PauseAnimation { + duration: 2500 + } + ScriptAction { + script: if (shouldAnimateHide) + hideAnim.start() + } + } + + ParallelAnimation { + id: hideAnim + running: false + NumberAnimation { + target: pill + property: "width" + from: maxPillWidth + to: 1 + duration: Style.animationNormal + easing.type: Easing.InCubic + } + NumberAnimation { + target: pill + property: "opacity" + from: 1 + to: 0 + duration: Style.animationNormal + easing.type: Easing.InCubic + } + onStopped: { + showPill = false; + shouldAnimateHide = false; + hidden(); + } + } + + function show() { + if (!showPill) { + shouldAnimateHide = autoHide; + showAnim.start(); + } else { + hideAnim.stop(); + delayedHideAnim.restart(); + } + } + + function hide() { + if (showPill) { + hideAnim.start(); + } + showTimer.stop(); + } + + function showDelayed() { + if (!showPill) { + shouldAnimateHide = autoHide; + showTimer.start(); + } else { + hideAnim.stop(); + delayedHideAnim.restart(); + } + } + + Timer { + id: showTimer + interval: Style.pillDelay + onTriggered: { + if (!showPill) { + showAnim.start(); + } + } + } +} \ No newline at end of file From 408f9a73a649c31175fbaf5888294f1778a8f9e9 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 14:24:07 -0400 Subject: [PATCH 060/394] qmlformat --- Modules/Bar/Bar.qml | 210 ++++++++++++------------- Modules/Bar/Battery.qml | 202 ++++++++++++------------ Widgets/NPill.qml | 340 ++++++++++++++++++++-------------------- 3 files changed, 376 insertions(+), 376 deletions(-) diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 9492961..9b59c1c 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -6,118 +6,112 @@ import qs.Services import qs.Widgets Variants { - model: Quickshell.screens + model: Quickshell.screens - delegate: PanelWindow { - id: root + delegate: PanelWindow { + id: root - required property ShellScreen modelData - readonly property real scaling: Scaling.scale(screen) + required property ShellScreen modelData + readonly property real scaling: Scaling.scale(screen) - screen: modelData - implicitHeight: Style.barHeight * scaling - color: "transparent" - visible: Settings.data.bar.monitors.includes(modelData.name) || (Settings.data.bar.monitors.length === 0) - - anchors { - top: true - left: true - right: true - } - - Item { - anchors.fill: parent - clip: true - - // Background fill - Rectangle { - id: bar - - anchors.fill: parent - color: Colors.backgroundPrimary - layer.enabled: true - } - - Row { - id: leftSection - - height: parent.height - anchors.left: parent.left - anchors.leftMargin: Style.marginSmall * scaling - anchors.verticalCenter: parent.verticalCenter - spacing: Style.marginSmall * scaling - - NText { - text: screen.name - anchors.verticalCenter: parent.verticalCenter - } - - } - - Row { - id: centerSection - - height: parent.height - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - spacing: Style.marginSmall * scaling - - Workspace { - } - - } - - Row { - id: rightSection - - height: parent.height - anchors.right: bar.right - anchors.rightMargin: Style.marginSmall * scaling - anchors.verticalCenter: bar.verticalCenter - spacing: Style.marginSmall * scaling - - Battery { - anchors.verticalCenter: parent.verticalCenter - } - - Clock { - anchors.verticalCenter: parent.verticalCenter - } - - NIconButton { - id: demoPanelToggle - - icon: "experiment" - anchors.verticalCenter: parent.verticalCenter - onClicked: function() { - demoPanel.isLoaded = !demoPanel.isLoaded; - } - } - - NIconButton { - id: sidePanelToggle - - icon: "widgets" - anchors.verticalCenter: parent.verticalCenter - onClicked: function() { - // Map this button's center to the screen and open the side panel below it - const localCenterX = width / 2; - const localCenterY = height / 2; - const globalPoint = mapToItem(null, localCenterX, localCenterY); - if (sidePanel.isLoaded) - sidePanel.isLoaded = false; - else if (sidePanel.openAt) - sidePanel.openAt(globalPoint.x, screen); - else - // Fallback: toggle if API unavailable - sidePanel.isLoaded = true; - } - } - - } - - } + screen: modelData + implicitHeight: Style.barHeight * scaling + color: "transparent" + visible: Settings.data.bar.monitors.includes(modelData.name) + || (Settings.data.bar.monitors.length === 0) + anchors { + top: true + left: true + right: true } + Item { + anchors.fill: parent + clip: true + + // Background fill + Rectangle { + id: bar + + anchors.fill: parent + color: Colors.backgroundPrimary + layer.enabled: true + } + + Row { + id: leftSection + + height: parent.height + anchors.left: parent.left + anchors.leftMargin: Style.marginSmall * scaling + anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginSmall * scaling + + NText { + text: screen.name + anchors.verticalCenter: parent.verticalCenter + } + } + + Row { + id: centerSection + + height: parent.height + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginSmall * scaling + + Workspace {} + } + + Row { + id: rightSection + + height: parent.height + anchors.right: bar.right + anchors.rightMargin: Style.marginSmall * scaling + anchors.verticalCenter: bar.verticalCenter + spacing: Style.marginSmall * scaling + + Battery { + anchors.verticalCenter: parent.verticalCenter + } + + Clock { + anchors.verticalCenter: parent.verticalCenter + } + + NIconButton { + id: demoPanelToggle + + icon: "experiment" + anchors.verticalCenter: parent.verticalCenter + onClicked: function () { + demoPanel.isLoaded = !demoPanel.isLoaded + } + } + + NIconButton { + id: sidePanelToggle + + icon: "widgets" + anchors.verticalCenter: parent.verticalCenter + onClicked: function () { + // Map this button's center to the screen and open the side panel below it + const localCenterX = width / 2 + const localCenterY = height / 2 + const globalPoint = mapToItem(null, localCenterX, localCenterY) + if (sidePanel.isLoaded) + sidePanel.isLoaded = false + else if (sidePanel.openAt) + sidePanel.openAt(globalPoint.x, screen) + else + // Fallback: toggle if API unavailable + sidePanel.isLoaded = true + } + } + } + } + } } diff --git a/Modules/Bar/Battery.qml b/Modules/Bar/Battery.qml index 7b1f33a..4342abd 100644 --- a/Modules/Bar/Battery.qml +++ b/Modules/Bar/Battery.qml @@ -7,113 +7,117 @@ import qs.Widgets import "../../Helpers/Duration.js" as Duration Item { - id: root + id: root - // Test mode - property bool testMode: true - property int testPercent: 49 - property bool testCharging: false + // Test mode + property bool testMode: true + property int testPercent: 49 + property bool testCharging: false - property var battery: UPower.displayDevice - property bool isReady: testMode ? true : (battery && battery.ready && battery.isLaptopBattery && battery.isPresent) - property real percent: testMode ? testPercent : (isReady ? (battery.percentage * 100) : 0) - property bool charging: testMode ? testCharging : (isReady ? battery.state === UPowerDeviceState.Charging : false) - property bool show: isReady && percent > 0 + property var battery: UPower.displayDevice + property bool isReady: testMode ? true : (battery && battery.ready + && battery.isLaptopBattery + && battery.isPresent) + property real percent: testMode ? testPercent : (isReady ? (battery.percentage * 100) : 0) + property bool charging: testMode ? testCharging : (isReady ? battery.state === UPowerDeviceState.Charging : false) + property bool show: isReady && percent > 0 - // Choose icon based on charge and charging state - function batteryIcon() { - if (!show) - return ""; + // Choose icon based on charge and charging state + function batteryIcon() { + if (!show) + return "" - if (charging) - return "battery_android_bolt"; + if (charging) + return "battery_android_bolt" - if (percent >= 95) - return "battery_android_full"; + if (percent >= 95) + return "battery_android_full" - // Hardcoded battery symbols - if (percent >= 85) - return "battery_android_6"; - if (percent >= 70) - return "battery_android_5"; - if (percent >= 55) - return "battery_android_4"; - if (percent >= 40) - return "battery_android_3"; - if (percent >= 25) - return "battery_android_2"; - if (percent >= 10) - return "battery_android_1"; - if (percent >= 0) - return "battery_android_0"; + // Hardcoded battery symbols + if (percent >= 85) + return "battery_android_6" + if (percent >= 70) + return "battery_android_5" + if (percent >= 55) + return "battery_android_4" + if (percent >= 40) + return "battery_android_3" + if (percent >= 25) + return "battery_android_2" + if (percent >= 10) + return "battery_android_1" + if (percent >= 0) + return "battery_android_0" + } + + visible: testMode || (isReady && battery.isLaptopBattery) + width: pill.width + height: pill.height + + NPill { + id: pill + icon: root.batteryIcon() + text: Math.round(root.percent) + "%" + pillColor: Colors.surfaceVariant + iconCircleColor: Colors.accentPrimary + iconTextColor: Colors.backgroundPrimary + textColor: charging ? Colors.accentPrimary : Colors.textPrimary + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: { + pill.showDelayed() + batteryTooltip.show() + } + onExited: { + pill.hide() + batteryTooltip.show() + } } - - visible: testMode || (isReady && battery.isLaptopBattery) - width: pill.width - height: pill.height - - NPill { - id: pill - icon: root.batteryIcon() - text: Math.round(root.percent) + "%" - pillColor: Colors.surfaceVariant - iconCircleColor: Colors.accentPrimary - iconTextColor: Colors.backgroundPrimary - textColor: charging ? Colors.accentPrimary : Colors.textPrimary - MouseArea { - anchors.fill: parent - hoverEnabled: true - onEntered: { - pill.showDelayed(); - batteryTooltip.show(); - } - onExited: { - pill.hide(); - batteryTooltip.show(); - } + NTooltip { + id: batteryTooltip + positionAbove: false + target: pill + delay: Style.tooltipDelayLong + text: { + let lines = [] + if (!root.isReady) { + return "" } - NTooltip { - id: batteryTooltip - positionAbove: false - target: pill - delay: Style.tooltipDelayLong - text: { - let lines = []; - if (!root.isReady) { - return ""; - } - - if (root.battery.timeToEmpty > 0) { - lines.push("Time left: " + Time.formatVagueHumanReadableTime(root.battery.timeToEmpty)); - } - - if (root.battery.timeToFull > 0) { - lines.push("Time until full: " + Time.formatVagueHumanReadableTime(root.battery.timeToFull)); - } - - if (root.battery.changeRate !== undefined) { - const rate = root.battery.changeRate; - if (rate > 0) { - lines.push(root.charging ? "Charging rate: " + rate.toFixed(2) + " W" : "Discharging rate: " + rate.toFixed(2) + " W"); - } - else if (rate < 0) { - lines.push("Discharging rate: " + Math.abs(rate).toFixed(2) + " W"); - } - else { - lines.push("Estimating..."); - } - } - else { - lines.push(root.charging ? "Charging" : "Discharging"); - } - - - if (root.battery.healthPercentage !== undefined && root.battery.healthPercentage > 0) { - lines.push("Health: " + Math.round(root.battery.healthPercentage) + "%"); - } - return lines.join("\n"); - } + if (root.battery.timeToEmpty > 0) { + lines.push("Time left: " + Time.formatVagueHumanReadableTime( + root.battery.timeToEmpty)) } + + if (root.battery.timeToFull > 0) { + lines.push("Time until full: " + Time.formatVagueHumanReadableTime( + root.battery.timeToFull)) + } + + if (root.battery.changeRate !== undefined) { + const rate = root.battery.changeRate + if (rate > 0) { + lines.push( + root.charging ? "Charging rate: " + rate.toFixed( + 2) + " W" : "Discharging rate: " + rate.toFixed( + 2) + " W") + } else if (rate < 0) { + lines.push("Discharging rate: " + Math.abs(rate).toFixed(2) + " W") + } else { + lines.push("Estimating...") + } + } else { + lines.push(root.charging ? "Charging" : "Discharging") + } + + if (root.battery.healthPercentage !== undefined + && root.battery.healthPercentage > 0) { + lines.push("Health: " + Math.round( + root.battery.healthPercentage) + "%") + } + return lines.join("\n") + } } -} \ No newline at end of file + } +} diff --git a/Widgets/NPill.qml b/Widgets/NPill.qml index 71aaa06..8448e17 100644 --- a/Widgets/NPill.qml +++ b/Widgets/NPill.qml @@ -3,200 +3,202 @@ import QtQuick.Controls import qs.Services Item { - id: revealPill + id: root - readonly property real scaling: Scaling.scale(screen) + readonly property real scaling: Scaling.scale(screen) - property string icon: "" - property string text: "" - property color pillColor: Colors.surfaceVariant - property color textColor: Colors.textPrimary - property color iconCircleColor: Colors.accentPrimary - property color iconTextColor: Colors.backgroundPrimary - property color collapsedIconColor: Colors.textPrimary - property real sizeMultiplier: 0.8 - property int pillHeight: Style.baseWidgetSize * sizeMultiplier * scaling - property int iconSize: Style.baseWidgetSize * sizeMultiplier * scaling - property int pillPaddingHorizontal: 14 * scaling - property bool autoHide: false + property string icon: "" + property string text: "" + property color pillColor: Colors.surfaceVariant + property color textColor: Colors.textPrimary + property color iconCircleColor: Colors.accentPrimary + property color iconTextColor: Colors.backgroundPrimary + property color collapsedIconColor: Colors.textPrimary + property real sizeMultiplier: 0.8 + property bool autoHide: false - // Internal state - property bool showPill: false - property bool shouldAnimateHide: false + // Internal state + property bool showPill: false + property bool shouldAnimateHide: false - // Exposed width logic - readonly property int pillOverlap: iconSize / 2 - readonly property int maxPillWidth: Math.max(1, textItem.implicitWidth + pillPaddingHorizontal * 2 + pillOverlap) + // Exposed width logic + readonly property int pillHeight: Style.baseWidgetSize * sizeMultiplier * scaling + readonly property int iconSize: Style.baseWidgetSize * sizeMultiplier * scaling + readonly property int pillPaddingHorizontal: 14 * scaling + readonly property int pillOverlap: iconSize * 0.5 + readonly property int maxPillWidth: Math.max( + 1, textItem.implicitWidth + + pillPaddingHorizontal * 2 + pillOverlap) - signal shown - signal hidden + signal shown + signal hidden - width: iconSize + (showPill ? maxPillWidth - pillOverlap : 0) + width: iconSize + (showPill ? maxPillWidth - pillOverlap : 0) + height: pillHeight + + Rectangle { + id: pill + width: showPill ? maxPillWidth : 1 height: pillHeight + x: (iconCircle.x + iconCircle.width / 2) - width + opacity: showPill ? 1 : 0 + color: pillColor + topLeftRadius: pillHeight * 0.5 + bottomLeftRadius: pillHeight * 0.5 + anchors.verticalCenter: parent.verticalCenter - Rectangle { - id: pill - width: showPill ? maxPillWidth : 1 - height: pillHeight - x: (iconCircle.x + iconCircle.width / 2) - width - opacity: showPill ? 1 : 0 - color: pillColor - topLeftRadius: pillHeight / 2 - bottomLeftRadius: pillHeight / 2 - anchors.verticalCenter: parent.verticalCenter - - Text { - id: textItem - anchors.centerIn: parent - text: revealPill.text - font.pointSize: Colors.fontSizeSmall * scaling - font.family: Settings.data.ui.fontFamily - font.weight: Font.Bold - color: textColor - visible: showPill - } - - Behavior on width { - enabled: showAnim.running || hideAnim.running - NumberAnimation { - duration: Style.animationNormal - easing.type: Easing.OutCubic - } - } - Behavior on opacity { - enabled: showAnim.running || hideAnim.running - NumberAnimation { - duration: 250 - easing.type: Easing.OutCubic - } - } + Text { + id: textItem + anchors.centerIn: parent + text: root.text + font.pointSize: Colors.fontSizeSmall * scaling + font.family: Settings.data.ui.fontFamily + font.weight: Font.Bold + color: textColor + visible: showPill } - Rectangle { - id: iconCircle - width: iconSize - height: iconSize - radius: width / 2 - color: showPill ? iconCircleColor : "transparent" - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right + Behavior on width { + enabled: showAnim.running || hideAnim.running + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + Behavior on opacity { + enabled: showAnim.running || hideAnim.running + NumberAnimation { + duration: 250 + easing.type: Easing.OutCubic + } + } + } - Behavior on color { - ColorAnimation { - duration: 200 - easing.type: Easing.InOutQuad - } - } + Rectangle { + id: iconCircle + width: iconSize + height: iconSize + radius: width / 2 + color: showPill ? iconCircleColor : "transparent" + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right - Text { - anchors.centerIn: parent - font.family: showPill ? "Material Symbols Rounded" : "Material Symbols Outlined" - font.pointSize: Colors.fontSizeSmall * scaling - text: revealPill.icon - color: showPill ? iconTextColor : collapsedIconColor - } + Behavior on color { + ColorAnimation { + duration: 200 + easing.type: Easing.InOutQuad + } } - ParallelAnimation { - id: showAnim - running: false - NumberAnimation { - target: pill - property: "width" - from: 1 - to: maxPillWidth - duration: Style.animationNormal - easing.type: Easing.OutCubic - } - NumberAnimation { - target: pill - property: "opacity" - from: 0 - to: 1 - duration: Style.animationNormal - easing.type: Easing.OutCubic - } - onStarted: { - showPill = true; - } - onStopped: { - delayedHideAnim.start(); - shown(); - } + Text { + anchors.centerIn: parent + font.family: showPill ? "Material Symbols Rounded" : "Material Symbols Outlined" + font.pointSize: Colors.fontSizeSmall * scaling + text: root.icon + color: showPill ? iconTextColor : collapsedIconColor } + } - SequentialAnimation { - id: delayedHideAnim - running: false - PauseAnimation { - duration: 2500 - } - ScriptAction { - script: if (shouldAnimateHide) + ParallelAnimation { + id: showAnim + running: false + NumberAnimation { + target: pill + property: "width" + from: 1 + to: maxPillWidth + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + NumberAnimation { + target: pill + property: "opacity" + from: 0 + to: 1 + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + onStarted: { + showPill = true + } + onStopped: { + delayedHideAnim.start() + shown() + } + } + + SequentialAnimation { + id: delayedHideAnim + running: false + PauseAnimation { + duration: 2500 + } + ScriptAction { + script: if (shouldAnimateHide) hideAnim.start() - } } + } - ParallelAnimation { - id: hideAnim - running: false - NumberAnimation { - target: pill - property: "width" - from: maxPillWidth - to: 1 - duration: Style.animationNormal - easing.type: Easing.InCubic - } - NumberAnimation { - target: pill - property: "opacity" - from: 1 - to: 0 - duration: Style.animationNormal - easing.type: Easing.InCubic - } - onStopped: { - showPill = false; - shouldAnimateHide = false; - hidden(); - } + ParallelAnimation { + id: hideAnim + running: false + NumberAnimation { + target: pill + property: "width" + from: maxPillWidth + to: 1 + duration: Style.animationNormal + easing.type: Easing.InCubic } + NumberAnimation { + target: pill + property: "opacity" + from: 1 + to: 0 + duration: Style.animationNormal + easing.type: Easing.InCubic + } + onStopped: { + showPill = false + shouldAnimateHide = false + hidden() + } + } - function show() { - if (!showPill) { - shouldAnimateHide = autoHide; - showAnim.start(); - } else { - hideAnim.stop(); - delayedHideAnim.restart(); - } + function show() { + if (!showPill) { + shouldAnimateHide = autoHide + showAnim.start() + } else { + hideAnim.stop() + delayedHideAnim.restart() } + } - function hide() { - if (showPill) { - hideAnim.start(); - } - showTimer.stop(); + function hide() { + if (showPill) { + hideAnim.start() } + showTimer.stop() + } - function showDelayed() { - if (!showPill) { - shouldAnimateHide = autoHide; - showTimer.start(); - } else { - hideAnim.stop(); - delayedHideAnim.restart(); - } + function showDelayed() { + if (!showPill) { + shouldAnimateHide = autoHide + showTimer.start() + } else { + hideAnim.stop() + delayedHideAnim.restart() } + } - Timer { - id: showTimer - interval: Style.pillDelay - onTriggered: { - if (!showPill) { - showAnim.start(); - } - } + Timer { + id: showTimer + interval: Style.pillDelay + onTriggered: { + if (!showPill) { + showAnim.start() + } } -} \ No newline at end of file + } +} From def932c4473c14fd30190550b31b56e218e2f0d4 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 14:33:51 -0400 Subject: [PATCH 061/394] Battery + NPill working and factorized --- Modules/Bar/Battery.qml | 79 ++++++++++++++++------------------------- Widgets/NPill.qml | 43 ++++++++++++++++------ 2 files changed, 63 insertions(+), 59 deletions(-) diff --git a/Modules/Bar/Battery.qml b/Modules/Bar/Battery.qml index 4342abd..ccdea95 100644 --- a/Modules/Bar/Battery.qml +++ b/Modules/Bar/Battery.qml @@ -62,62 +62,43 @@ Item { iconCircleColor: Colors.accentPrimary iconTextColor: Colors.backgroundPrimary textColor: charging ? Colors.accentPrimary : Colors.textPrimary - MouseArea { - anchors.fill: parent - hoverEnabled: true - onEntered: { - pill.showDelayed() - batteryTooltip.show() + tooltipText: { + let lines = [] + if (!root.isReady) { + return "" } - onExited: { - pill.hide() - batteryTooltip.show() + + if (root.battery.timeToEmpty > 0) { + lines.push("Time left: " + Time.formatVagueHumanReadableTime( + root.battery.timeToEmpty)) } - } - NTooltip { - id: batteryTooltip - positionAbove: false - target: pill - delay: Style.tooltipDelayLong - text: { - let lines = [] - if (!root.isReady) { - return "" - } - if (root.battery.timeToEmpty > 0) { - lines.push("Time left: " + Time.formatVagueHumanReadableTime( - root.battery.timeToEmpty)) - } + if (root.battery.timeToFull > 0) { + lines.push("Time until full: " + Time.formatVagueHumanReadableTime( + root.battery.timeToFull)) + } - if (root.battery.timeToFull > 0) { - lines.push("Time until full: " + Time.formatVagueHumanReadableTime( - root.battery.timeToFull)) - } - - if (root.battery.changeRate !== undefined) { - const rate = root.battery.changeRate - if (rate > 0) { - lines.push( - root.charging ? "Charging rate: " + rate.toFixed( - 2) + " W" : "Discharging rate: " + rate.toFixed( - 2) + " W") - } else if (rate < 0) { - lines.push("Discharging rate: " + Math.abs(rate).toFixed(2) + " W") - } else { - lines.push("Estimating...") - } + if (root.battery.changeRate !== undefined) { + const rate = root.battery.changeRate + if (rate > 0) { + lines.push( + root.charging ? "Charging rate: " + rate.toFixed( + 2) + " W" : "Discharging rate: " + rate.toFixed( + 2) + " W") + } else if (rate < 0) { + lines.push("Discharging rate: " + Math.abs(rate).toFixed(2) + " W") } else { - lines.push(root.charging ? "Charging" : "Discharging") + lines.push("Estimating...") } - - if (root.battery.healthPercentage !== undefined - && root.battery.healthPercentage > 0) { - lines.push("Health: " + Math.round( - root.battery.healthPercentage) + "%") - } - return lines.join("\n") + } else { + lines.push(root.charging ? "Charging" : "Discharging") } + + if (root.battery.healthPercentage !== undefined + && root.battery.healthPercentage > 0) { + lines.push("Health: " + Math.round(root.battery.healthPercentage) + "%") + } + return lines.join("\n") } } } diff --git a/Widgets/NPill.qml b/Widgets/NPill.qml index 8448e17..20b8817 100644 --- a/Widgets/NPill.qml +++ b/Widgets/NPill.qml @@ -9,6 +9,7 @@ Item { property string icon: "" property string text: "" + property string tooltipText: "" property color pillColor: Colors.surfaceVariant property color textColor: Colors.textPrimary property color iconCircleColor: Colors.accentPrimary @@ -30,6 +31,7 @@ Item { 1, textItem.implicitWidth + pillPaddingHorizontal * 2 + pillOverlap) + // TBC, do we use those ? signal shown signal hidden @@ -165,6 +167,37 @@ Item { } } + NTooltip { + id: tooltip + positionAbove: false + target: pill + delay: Style.tooltipDelayLong + text: root.tooltipText + } + + Timer { + id: showTimer + interval: Style.pillDelay + onTriggered: { + if (!showPill) { + showAnim.start() + } + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: { + showDelayed() + tooltip.show() + } + onExited: { + hide() + tooltip.hide() + } + } + function show() { if (!showPill) { shouldAnimateHide = autoHide @@ -191,14 +224,4 @@ Item { delayedHideAnim.restart() } } - - Timer { - id: showTimer - interval: Style.pillDelay - onTriggered: { - if (!showPill) { - showAnim.start() - } - } - } } From d3f66b1737dddaf6f616350cdea23f6e1358d638 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 14:39:25 -0400 Subject: [PATCH 062/394] qmlfmt with 120 chars width (rulers) --- Bin/run-qmlfmt.sh | 2 +- Modules/Background/Background.qml | 3 +- Modules/Background/Overview.qml | 6 +-- Modules/Bar/Bar.qml | 3 +- Modules/Bar/Battery.qml | 90 ++++++++++++++----------------- Modules/Bar/Workspace.qml | 9 ++-- Modules/SidePanel/SidePanel.qml | 5 +- Services/Niri.qml | 6 +-- Services/Settings.qml | 9 ++-- Services/Time.qml | 7 ++- Services/Wallpapers.qml | 9 ++-- Services/Workspaces.qml | 4 +- Widgets/NPanel.qml | 3 +- Widgets/NPill.qml | 4 +- Widgets/NSlider.qml | 6 +-- Widgets/NSystemMonitor.qml | 29 ++++------ Widgets/NTooltip.qml | 8 +-- 17 files changed, 77 insertions(+), 126 deletions(-) diff --git a/Bin/run-qmlfmt.sh b/Bin/run-qmlfmt.sh index f9aaa46..85b8693 100755 --- a/Bin/run-qmlfmt.sh +++ b/Bin/run-qmlfmt.sh @@ -4,4 +4,4 @@ # Can be installed from AUR "qmlfmt-git" # Requires qt6-5compat -find . -name "*.qml" -exec qmlfmt -t 2 -i 2 -w {} \; \ No newline at end of file +find . -name "*.qml" -exec qmlfmt -e -b 120 -t 2 -i 2 -w {} \; \ No newline at end of file diff --git a/Modules/Background/Background.qml b/Modules/Background/Background.qml index 055cfcc..551c2d3 100644 --- a/Modules/Background/Background.qml +++ b/Modules/Background/Background.qml @@ -8,8 +8,7 @@ Variants { delegate: PanelWindow { required property ShellScreen modelData - property string wallpaperSource: Qt.resolvedUrl( - "../../Assets/Tests/wallpaper.png") + property string wallpaperSource: Qt.resolvedUrl("../../Assets/Tests/wallpaper.png") visible: wallpaperSource !== "" color: "transparent" diff --git a/Modules/Background/Overview.qml b/Modules/Background/Overview.qml index 438e612..9dc5f6f 100644 --- a/Modules/Background/Overview.qml +++ b/Modules/Background/Overview.qml @@ -9,8 +9,7 @@ Variants { delegate: PanelWindow { required property ShellScreen modelData - property string wallpaperSource: Qt.resolvedUrl( - "../../Assets/Tests/wallpaper.png") + property string wallpaperSource: Qt.resolvedUrl("../../Assets/Tests/wallpaper.png") visible: wallpaperSource !== "" color: "transparent" @@ -50,8 +49,7 @@ Variants { Rectangle { anchors.fill: parent - color: Qt.rgba(Colors.backgroundPrimary.r, Colors.backgroundPrimary.g, - Colors.backgroundPrimary.b, 0.5) + color: Qt.rgba(Colors.backgroundPrimary.r, Colors.backgroundPrimary.g, Colors.backgroundPrimary.b, 0.5) } } } diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 9b59c1c..e803582 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -17,8 +17,7 @@ Variants { screen: modelData implicitHeight: Style.barHeight * scaling color: "transparent" - visible: Settings.data.bar.monitors.includes(modelData.name) - || (Settings.data.bar.monitors.length === 0) + visible: Settings.data.bar.monitors.includes(modelData.name) || (Settings.data.bar.monitors.length === 0) anchors { top: true diff --git a/Modules/Bar/Battery.qml b/Modules/Bar/Battery.qml index ccdea95..b278e71 100644 --- a/Modules/Bar/Battery.qml +++ b/Modules/Bar/Battery.qml @@ -6,7 +6,7 @@ import qs.Services import qs.Widgets import "../../Helpers/Duration.js" as Duration -Item { +NPill { id: root // Test mode @@ -15,9 +15,7 @@ Item { property bool testCharging: false property var battery: UPower.displayDevice - property bool isReady: testMode ? true : (battery && battery.ready - && battery.isLaptopBattery - && battery.isPresent) + property bool isReady: testMode ? true : (battery && battery.ready && battery.isLaptopBattery && battery.isPresent) property real percent: testMode ? testPercent : (isReady ? (battery.percentage * 100) : 0) property bool charging: testMode ? testCharging : (isReady ? battery.state === UPowerDeviceState.Charging : false) property bool show: isReady && percent > 0 @@ -51,54 +49,44 @@ Item { } visible: testMode || (isReady && battery.isLaptopBattery) - width: pill.width - height: pill.height - NPill { - id: pill - icon: root.batteryIcon() - text: Math.round(root.percent) + "%" - pillColor: Colors.surfaceVariant - iconCircleColor: Colors.accentPrimary - iconTextColor: Colors.backgroundPrimary - textColor: charging ? Colors.accentPrimary : Colors.textPrimary - tooltipText: { - let lines = [] - if (!root.isReady) { - return "" - } - - if (root.battery.timeToEmpty > 0) { - lines.push("Time left: " + Time.formatVagueHumanReadableTime( - root.battery.timeToEmpty)) - } - - if (root.battery.timeToFull > 0) { - lines.push("Time until full: " + Time.formatVagueHumanReadableTime( - root.battery.timeToFull)) - } - - if (root.battery.changeRate !== undefined) { - const rate = root.battery.changeRate - if (rate > 0) { - lines.push( - root.charging ? "Charging rate: " + rate.toFixed( - 2) + " W" : "Discharging rate: " + rate.toFixed( - 2) + " W") - } else if (rate < 0) { - lines.push("Discharging rate: " + Math.abs(rate).toFixed(2) + " W") - } else { - lines.push("Estimating...") - } - } else { - lines.push(root.charging ? "Charging" : "Discharging") - } - - if (root.battery.healthPercentage !== undefined - && root.battery.healthPercentage > 0) { - lines.push("Health: " + Math.round(root.battery.healthPercentage) + "%") - } - return lines.join("\n") + icon: root.batteryIcon() + text: Math.round(root.percent) + "%" + pillColor: Colors.surfaceVariant + iconCircleColor: Colors.accentPrimary + iconTextColor: Colors.backgroundPrimary + textColor: charging ? Colors.accentPrimary : Colors.textPrimary + tooltipText: { + let lines = [] + if (!root.isReady) { + return "" } + + if (root.battery.timeToEmpty > 0) { + lines.push("Time left: " + Time.formatVagueHumanReadableTime(root.battery.timeToEmpty)) + } + + if (root.battery.timeToFull > 0) { + lines.push("Time until full: " + Time.formatVagueHumanReadableTime(root.battery.timeToFull)) + } + + if (root.battery.changeRate !== undefined) { + const rate = root.battery.changeRate + if (rate > 0) { + lines.push(root.charging ? "Charging rate: " + rate.toFixed(2) + " W" : "Discharging rate: " + rate.toFixed( + 2) + " W") + } else if (rate < 0) { + lines.push("Discharging rate: " + Math.abs(rate).toFixed(2) + " W") + } else { + lines.push("Estimating...") + } + } else { + lines.push(root.charging ? "Charging" : "Discharging") + } + + if (root.battery.healthPercentage !== undefined && root.battery.healthPercentage > 0) { + lines.push("Health: " + Math.round(root.battery.healthPercentage) + "%") + } + return lines.join("\n") } } diff --git a/Modules/Bar/Workspace.qml b/Modules/Bar/Workspace.qml index 5428ba2..4c03f68 100644 --- a/Modules/Bar/Workspace.qml +++ b/Modules/Bar/Workspace.qml @@ -123,8 +123,7 @@ Item { anchors.verticalCenter: parent.verticalCenter radius: Math.round(12 * s) color: Colors.surfaceVariant - border.color: Qt.rgba(Colors.textPrimary.r, Colors.textPrimary.g, - Colors.textPrimary.b, 0.1) + border.color: Qt.rgba(Colors.textPrimary.r, Colors.textPrimary.g, Colors.textPrimary.b, 0.1) border.width: Math.max(1, Math.round(1 * s)) layer.enabled: true layer.effect: MultiEffect { @@ -252,10 +251,8 @@ Item { radius: width / 2 color: "transparent" border.color: root.effectColor - border.width: Math.max(1, Math.round( - (2 + 6 * (1.0 - root.masterProgress)) * s)) - opacity: root.effectsActive - && model.isFocused ? (1.0 - root.masterProgress) * 0.7 : 0 + border.width: Math.max(1, Math.round((2 + 6 * (1.0 - root.masterProgress)) * s)) + opacity: root.effectsActive && model.isFocused ? (1.0 - root.masterProgress) * 0.7 : 0 visible: root.effectsActive && model.isFocused z: 1 } diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml index 39796b7..030b47f 100644 --- a/Modules/SidePanel/SidePanel.qml +++ b/Modules/SidePanel/SidePanel.qml @@ -59,9 +59,8 @@ NLoader { // Place the panel just below the bar (overlay content starts below bar due to topMargin) y: Style.marginSmall * scaling // Center horizontally under the anchorX, clamped to the screen bounds - x: Math.max(Style.marginSmall * scaling, Math.min( - parent.width - width - Style.marginSmall * scaling, - Math.round(anchorX - width / 2))) + x: Math.max(Style.marginSmall * scaling, Math.min(parent.width - width - Style.marginSmall * scaling, + Math.round(anchorX - width / 2))) // Prevent closing when clicking in the panel bg MouseArea { diff --git a/Services/Niri.qml b/Services/Niri.qml index bb7f1c4..992991e 100644 --- a/Services/Niri.qml +++ b/Services/Niri.qml @@ -17,8 +17,7 @@ Singleton { function updateFocusedWindowTitle() { if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) { - focusedWindowTitle = windows[focusedWindowIndex].title - || "(Unnamed window)" + focusedWindowTitle = windows[focusedWindowIndex].title || "(Unnamed window)" } else { focusedWindowTitle = "(No active window)" } @@ -113,8 +112,7 @@ Singleton { try { const focusedId = event.WindowFocusChanged.id if (focusedId) { - root.focusedWindowIndex = root.windows.findIndex( - w => w.id === focusedId) + root.focusedWindowIndex = root.windows.findIndex(w => w.id === focusedId) if (root.focusedWindowIndex < 0) { root.focusedWindowIndex = 0 } diff --git a/Services/Settings.qml b/Services/Settings.qml index 738a14f..2c034f3 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -7,13 +7,10 @@ pragma Singleton Singleton { property string shellName: "noctalia" property string settingsDir: Quickshell.env("NOCTALIA_SETTINGS_DIR") - || (Quickshell.env("XDG_CONFIG_HOME") - || Quickshell.env( + || (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env( "HOME") + "/.config") + "/" + shellName + "/" - property string settingsFile: Quickshell.env("NOCTALIA_SETTINGS_FILE") - || (settingsDir + "settings.json") - property string colorsFile: Quickshell.env("NOCTALIA_COLORS_FILE") - || (settingsDir + "colors.json") + property string settingsFile: Quickshell.env("NOCTALIA_SETTINGS_FILE") || (settingsDir + "settings.json") + property string colorsFile: Quickshell.env("NOCTALIA_COLORS_FILE") || (settingsDir + "colors.json") property var data: settingAdapter // Needed to only have one NPanel loaded at a time. diff --git a/Services/Time.qml b/Services/Time.qml index 27718ef..07650db 100644 --- a/Services/Time.qml +++ b/Services/Time.qml @@ -8,9 +8,7 @@ Singleton { id: root property var date: new Date() - property string time: Settings.data.location.use12HourClock ? Qt.formatDateTime( - date, - "h:mm AP") : Qt.formatDateTime( + property string time: Settings.data.location.use12HourClock ? Qt.formatDateTime(date, "h:mm AP") : Qt.formatDateTime( date, "HH:mm") property string dateString: { let now = date @@ -36,7 +34,8 @@ Singleton { } let month = now.toLocaleDateString(Qt.locale(), "MMMM") let year = now.toLocaleDateString(Qt.locale(), "yyyy") - return `${dayName}, ` + (Settings.data.location.reverseDayMonth ? `${month} ${day}${suffix} ${year}` : `${day}${suffix} ${month} ${year}`) + return `${dayName}, ` + + (Settings.data.location.reverseDayMonth ? `${month} ${day}${suffix} ${year}` : `${day}${suffix} ${month} ${year}`) } Timer { diff --git a/Services/Wallpapers.qml b/Services/Wallpapers.qml index 545c083..d73d5cc 100644 --- a/Services/Wallpapers.qml +++ b/Services/Wallpapers.qml @@ -26,8 +26,7 @@ Singleton { scanning = true wallpaperList = [] folderModel.folder = "" - folderModel.folder = "file://" + (Settings.data.wallpaper.directory - !== undefined ? Settings.data.wallpaper.directory : "") + folderModel.folder = "file://" + (Settings.data.wallpaper.directory !== undefined ? Settings.data.wallpaper.directory : "") } function changeWallpaper(path) { @@ -41,8 +40,7 @@ Singleton { } if (Settings.data.swww.enabled) { if (Settings.data.swww.transitionType === "random") { - transitionType = randomChoices[Math.floor(Math.random( - ) * randomChoices.length)] + transitionType = randomChoices[Math.floor(Math.random() * randomChoices.length)] } else { transitionType = Settings.data.swww.transitionType } @@ -107,8 +105,7 @@ Singleton { var files = [] var filesSwww = [] for (var i = 0; i < count; i++) { - var filepath = (Settings.data.wallpaper.folder - !== undefined ? Settings.data.wallpaper.folder : "") + "/" + get( + var filepath = (Settings.data.wallpaper.folder !== undefined ? Settings.data.wallpaper.folder : "") + "/" + get( i, "fileName") files.push(filepath) } diff --git a/Services/Workspaces.qml b/Services/Workspaces.qml index 8f3fb5f..f5de53b 100644 --- a/Services/Workspaces.qml +++ b/Services/Workspaces.qml @@ -143,9 +143,7 @@ Singleton { } } else if (isNiri) { try { - Quickshell.execDetached( - ["niri", "msg", "action", "focus-workspace", workspaceId.toString( - )]) + Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", workspaceId.toString()]) } catch (e) { console.error("Error switching Niri workspace:", e) } diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 7fc42ab..e995273 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -20,8 +20,7 @@ PanelWindow { function show() { // Ensure only one panel is visible at a time using Settings as ephemeral store try { - if (Settings.openPanel && Settings.openPanel !== outerPanel - && Settings.openPanel.hide) { + if (Settings.openPanel && Settings.openPanel !== outerPanel && Settings.openPanel.hide) { Settings.openPanel.hide() } Settings.openPanel = outerPanel diff --git a/Widgets/NPill.qml b/Widgets/NPill.qml index 20b8817..a809bfd 100644 --- a/Widgets/NPill.qml +++ b/Widgets/NPill.qml @@ -27,9 +27,7 @@ Item { readonly property int iconSize: Style.baseWidgetSize * sizeMultiplier * scaling readonly property int pillPaddingHorizontal: 14 * scaling readonly property int pillOverlap: iconSize * 0.5 - readonly property int maxPillWidth: Math.max( - 1, textItem.implicitWidth - + pillPaddingHorizontal * 2 + pillOverlap) + readonly property int maxPillWidth: Math.max(1, textItem.implicitWidth + pillPaddingHorizontal * 2 + pillOverlap) // TBC, do we use those ? signal shown diff --git a/Widgets/NSlider.qml b/Widgets/NSlider.qml index e6fe997..60a8367 100644 --- a/Widgets/NSlider.qml +++ b/Widgets/NSlider.qml @@ -52,10 +52,8 @@ Slider { height: knobDiameter + cutoutExtra radius: width / 2 color: root.cutoutColor !== undefined ? root.cutoutColor : Colors.backgroundPrimary - x: Math.max( - 0, Math.min( - parent.width - width, - root.visualPosition * (parent.width - root.knobDiameter) - cutoutExtra / 2)) + x: Math.max(0, Math.min(parent.width - width, + root.visualPosition * (parent.width - root.knobDiameter) - cutoutExtra / 2)) y: (parent.height - height) / 2 } } diff --git a/Widgets/NSystemMonitor.qml b/Widgets/NSystemMonitor.qml index 05b4083..bb28716 100644 --- a/Widgets/NSystemMonitor.qml +++ b/Widgets/NSystemMonitor.qml @@ -24,26 +24,17 @@ Item { command: ["sh", "-c", // Outputs: {"cpu":,"memper":,"cputemp":} "interval=" + intervalSeconds + "; " + "while true; do " + // First /proc/stat snapshot "read _ u1 n1 s1 id1 iw1 ir1 si1 st1 gs1 < /proc/stat; " - + "t1=$((u1+n1+s1+id1+iw1+ir1+si1+st1)); i1=$((id1+iw1)); " - + "sleep $interval; " + // Second /proc/stat snapshot - "read _ u2 n2 s2 id2 iw2 ir2 si2 st2 gs2 < /proc/stat; " - + "t2=$((u2+n2+s2+id2+iw2+ir2+si2+st2)); i2=$((id2+iw2)); " - + "dt=$((t2 - t1)); di=$((i2 - i1)); " + "cpu=$(( (100*(dt - di)) / (dt>0?dt:1) )); " - + // Memory percent via /proc/meminfo (kB) - "mt=$(awk '/MemTotal/ {print $2}' /proc/meminfo); " - + "ma=$(awk '/MemAvailable/ {print $2}' /proc/meminfo); " - + "mm=$((mt - ma)); mp=$(( (100*mm) / (mt>0?mt:1) )); " - + // Temperature: scan hwmon and thermal zones, choose max; convert m°C → °C - "ct=0; " - + "for f in /sys/class/hwmon/hwmon*/temp*_input /sys/class/thermal/thermal_zone*/temp; do " + + "t1=$((u1+n1+s1+id1+iw1+ir1+si1+st1)); i1=$((id1+iw1)); " + "sleep $interval; " + // Second /proc/stat snapshot + "read _ u2 n2 s2 id2 iw2 ir2 si2 st2 gs2 < /proc/stat; " + "t2=$((u2+n2+s2+id2+iw2+ir2+si2+st2)); i2=$((id2+iw2)); " + + "dt=$((t2 - t1)); di=$((i2 - i1)); " + "cpu=$(( (100*(dt - di)) / (dt>0?dt:1) )); " + // Memory percent via /proc/meminfo (kB) + "mt=$(awk '/MemTotal/ {print $2}' /proc/meminfo); " + "ma=$(awk '/MemAvailable/ {print $2}' /proc/meminfo); " + + "mm=$((mt - ma)); mp=$(( (100*mm) / (mt>0?mt:1) )); " + // Temperature: scan hwmon and thermal zones, choose max; convert m°C → °C + "ct=0; " + "for f in /sys/class/hwmon/hwmon*/temp*_input /sys/class/thermal/thermal_zone*/temp; do " + "[ -r \"$f\" ] || continue; v=$(cat \"$f\" 2>/dev/null); " + "[ -z \"$v\" ] && continue; " - + "if [ \"$v\" -gt 1000 ] 2>/dev/null; then v=$((v/1000)); fi; " - + "[ \"$v\" -gt \"$ct\" ] 2>/dev/null && ct=$v; " + "done; " - + // Disk usage percent for root filesystem - "dp=$(df -P / 2>/dev/null | awk 'NR==2{gsub(/%/,\"\",$5); print $5}'); " - + "[ -z \"$dp\" ] && dp=0; " + // Emit JSON line - "echo \"{\\\"cpu\\\":$cpu,\\\"memper\\\":$mp,\\\"cputemp\\\":$ct,\\\"diskper\\\":$dp}\"; " - + "done"] + + "if [ \"$v\" -gt 1000 ] 2>/dev/null; then v=$((v/1000)); fi; " + "[ \"$v\" -gt \"$ct\" ] 2>/dev/null && ct=$v; " + + "done; " + // Disk usage percent for root filesystem + "dp=$(df -P / 2>/dev/null | awk 'NR==2{gsub(/%/,\"\",$5); print $5}'); " + "[ -z \"$dp\" ] && dp=0; " + // Emit JSON line + "echo \"{\\\"cpu\\\":$cpu,\\\"memper\\\":$mp,\\\"cputemp\\\":$ct,\\\"diskper\\\":$dp}\"; " + "done"] stdout: SplitParser { onRead: function (line) { diff --git a/Widgets/NTooltip.qml b/Widgets/NTooltip.qml index 482f9e2..31ac67f 100644 --- a/Widgets/NTooltip.qml +++ b/Widgets/NTooltip.qml @@ -37,12 +37,8 @@ Window { function _showNow() { // Compute new size everytime we show the tooltip - width = Math.max( - 50 * scaling, - tooltipText.implicitWidth + Style.marginLarge * 2 * scaling) - height = Math.max( - 50 * scaling, - tooltipText.implicitHeight + Style.marginSmall * 2 * scaling) + width = Math.max(50 * scaling, tooltipText.implicitWidth + Style.marginLarge * 2 * scaling) + height = Math.max(50 * scaling, tooltipText.implicitHeight + Style.marginSmall * 2 * scaling) if (!target) { return From b824cd7809eda4c8232517da8134c5c2b132ed54 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 14:44:36 -0400 Subject: [PATCH 063/394] qmlfmt: reports error and filename --- Bin/run-qmlfmt.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Bin/run-qmlfmt.sh b/Bin/run-qmlfmt.sh index 85b8693..39f2579 100755 --- a/Bin/run-qmlfmt.sh +++ b/Bin/run-qmlfmt.sh @@ -4,4 +4,5 @@ # Can be installed from AUR "qmlfmt-git" # Requires qt6-5compat -find . -name "*.qml" -exec qmlfmt -e -b 120 -t 2 -i 2 -w {} \; \ No newline at end of file +#find . -name "*.qml" -exec sh -c 'echo "Formatting $0..."; qmlfmt -e -b 120 -t 2 -i 2 -w "$0"' {} \; +find . -name "*.qml" -print -exec qmlfmt -e -b 120 -t 2 -i 2 -w {} \; \ No newline at end of file From 2f4d52403b17c6aeb4095d3cf50035fe411e4db7 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 14:49:43 -0400 Subject: [PATCH 064/394] Fixed all qmlfmt warnings/errors --- Bin/run-qmlfmt.sh | 1 - Modules/Background/WallpaperPicker.qml | 8 + Modules/DemoPanel/DemoPanel.qml | 68 ++++-- Services/Location.qml | 10 +- Services/MediaPlayer.qml | 284 ++++++++++++------------- Widgets/NToggle.qml | 8 +- 6 files changed, 210 insertions(+), 169 deletions(-) diff --git a/Bin/run-qmlfmt.sh b/Bin/run-qmlfmt.sh index 39f2579..71f4f90 100755 --- a/Bin/run-qmlfmt.sh +++ b/Bin/run-qmlfmt.sh @@ -4,5 +4,4 @@ # Can be installed from AUR "qmlfmt-git" # Requires qt6-5compat -#find . -name "*.qml" -exec sh -c 'echo "Formatting $0..."; qmlfmt -e -b 120 -t 2 -i 2 -w "$0"' {} \; find . -name "*.qml" -print -exec qmlfmt -e -b 120 -t 2 -i 2 -w {} \; \ No newline at end of file diff --git a/Modules/Background/WallpaperPicker.qml b/Modules/Background/WallpaperPicker.qml index e69de29..5fc2b8b 100644 --- a/Modules/Background/WallpaperPicker.qml +++ b/Modules/Background/WallpaperPicker.qml @@ -0,0 +1,8 @@ +pragma Singleton + +import Quickshell +import qs.Services + +Item { + id: root +} diff --git a/Modules/DemoPanel/DemoPanel.qml b/Modules/DemoPanel/DemoPanel.qml index 8236509..5b26c70 100644 --- a/Modules/DemoPanel/DemoPanel.qml +++ b/Modules/DemoPanel/DemoPanel.qml @@ -6,10 +6,10 @@ import Quickshell.Wayland import qs.Services import qs.Widgets + /* An experiment/demo panel to tweaks widgets */ - NLoader { id: root @@ -32,7 +32,9 @@ NLoader { anchors.centerIn: parent // Prevent closing when clicking in the panel bg - MouseArea { anchors.fill: parent } + MouseArea { + anchors.fill: parent + } ColumnLayout { anchors.fill: parent @@ -42,61 +44,93 @@ NLoader { // NIconButton ColumnLayout { spacing: 16 * scaling - NText { text: "NIconButton"; color: Colors.accentSecondary } + NText { + text: "NIconButton" + color: Colors.accentSecondary + } NIconButton { id: myIconButton icon: "refresh" - onEntered: function() { myTooltip.show() } - onExited: function() { myTooltip.hide() } + onEntered: function () { + myTooltip.show() + } + onExited: function () { + myTooltip.hide() + } + } + NTooltip { + id: myTooltip + target: myIconButton + positionAbove: false + text: "Hello world" } - NTooltip { id: myTooltip; target: myIconButton; positionAbove: false; text: "Hello world"; } - NDivider { Layout.fillWidth: true } + NDivider { + Layout.fillWidth: true + } } // NToggle ColumnLayout { spacing: Style.marginLarge * scaling uniformCellSizes: true - NText { text: "NToggle + NTooltip"; color: Colors.accentSecondary } + NText { + text: "NToggle + NTooltip" + color: Colors.accentSecondary + } NToggle { label: "Label" description: "Description" - onToggled: function(value: bool) { console.log("NToggle: " + value) } + onToggled: function (value) { + console.log("NToggle: " + value) + } } - - NDivider { Layout.fillWidth: true } + NDivider { + Layout.fillWidth: true + } } // NSlider ColumnLayout { spacing: 16 * scaling - NText { text: "Scaling"; color: Colors.accentSecondary } + NText { + text: "Scaling" + color: Colors.accentSecondary + } RowLayout { spacing: Style.marginSmall * scaling - NText { text: `${Math.round(Scaling.overrideScale * 100)}%`; Layout.alignment: Qt.AlignVCenter } + NText { + text: `${Math.round(Scaling.overrideScale * 100)}%` + Layout.alignment: Qt.AlignVCenter + } NSlider { id: scaleSlider from: 0.6 to: 1.8 stepSize: 0.01 value: Scaling.overrideScale - onMoved: function() { Scaling.overrideScale = value } - onPressedChanged: function() { Scaling.overrideEnabled = true } + onMoved: function () { + Scaling.overrideScale = value + } + onPressedChanged: function () { + Scaling.overrideEnabled = true + } } NIconButton { icon: "restart_alt" sizeMultiplier: 0.7 - onClicked: function() { + onClicked: function () { Scaling.overrideEnabled = false Scaling.overrideScale = 1.0 } } } - NDivider { Layout.fillWidth: true } + NDivider { + Layout.fillWidth: true + } } } } diff --git a/Services/Location.qml b/Services/Location.qml index 9458435..64d8820 100644 --- a/Services/Location.qml +++ b/Services/Location.qml @@ -1,2 +1,10 @@ +pragma Singleton + +import Quickshell +import qs.Services + // Weather logic and caching -// Calendar Hollidays logic and caching \ No newline at end of file +// Calendar Hollidays logic and caching +Singleton { + id: root +} diff --git a/Services/MediaPlayer.qml b/Services/MediaPlayer.qml index b9d17d0..806142c 100644 --- a/Services/MediaPlayer.qml +++ b/Services/MediaPlayer.qml @@ -1,169 +1,161 @@ -// pragma Singleton +pragma Singleton -// import QtQuick -// import Quickshell -// import Quickshell.Services.Mpris -// import qs.Services +import Quickshell +import Quickshell.Services.Mpris +import qs.Services -// Singleton { -// id: manager +Singleton { + id: root -// property var currentPlayer: null -// property real currentPosition: 0 -// property int selectedPlayerIndex: 0 -// property bool isPlaying: currentPlayer ? currentPlayer.isPlaying : false -// property string trackTitle: currentPlayer ? (currentPlayer.trackTitle -// || "Unknown Track") : "" -// property string trackArtist: currentPlayer ? (currentPlayer.trackArtist -// || "Unknown Artist") : "" -// property string trackAlbum: currentPlayer ? (currentPlayer.trackAlbum -// || "Unknown Album") : "" -// property string trackArtUrl: currentPlayer ? (currentPlayer.trackArtUrl -// || "") : "" -// property real trackLength: currentPlayer ? currentPlayer.length : 0 -// property bool canPlay: currentPlayer ? currentPlayer.canPlay : false -// property bool canPause: currentPlayer ? currentPlayer.canPause : false -// property bool canGoNext: currentPlayer ? currentPlayer.canGoNext : false -// property bool canGoPrevious: currentPlayer ? currentPlayer.canGoPrevious : false -// property bool canSeek: currentPlayer ? currentPlayer.canSeek : false -// property bool hasPlayer: getAvailablePlayers().length > 0 + // property var currentPlayer: null + // property real currentPosition: 0 + // property int selectedPlayerIndex: 0 + // property bool isPlaying: currentPlayer ? currentPlayer.isPlaying : false + // property string trackTitle: currentPlayer ? (currentPlayer.trackTitle || "Unknown Track") : "" + // property string trackArtist: currentPlayer ? (currentPlayer.trackArtist || "Unknown Artist") : "" + // property string trackAlbum: currentPlayer ? (currentPlayer.trackAlbum || "Unknown Album") : "" + // property string trackArtUrl: currentPlayer ? (currentPlayer.trackArtUrl || "") : "" + // property real trackLength: currentPlayer ? currentPlayer.length : 0 + // property bool canPlay: currentPlayer ? currentPlayer.canPlay : false + // property bool canPause: currentPlayer ? currentPlayer.canPause : false + // property bool canGoNext: currentPlayer ? currentPlayer.canGoNext : false + // property bool canGoPrevious: currentPlayer ? currentPlayer.canGoPrevious : false + // property bool canSeek: currentPlayer ? currentPlayer.canSeek : false + // property bool hasPlayer: getAvailablePlayers().length > 0 -// Item { -// Component.onCompleted: { -// updateCurrentPlayer() -// } -// } + // Item { + // Component.onCompleted: { + // updateCurrentPlayer() + // } + // } -// function getAvailablePlayers() { -// if (!Mpris.players || !Mpris.players.values) { -// return [] -// } + // function getAvailablePlayers() { + // if (!Mpris.players || !Mpris.players.values) { + // return [] + // } -// let allPlayers = Mpris.players.values -// let controllablePlayers = [] + // let allPlayers = Mpris.players.values + // let controllablePlayers = [] -// for (var i = 0; i < allPlayers.length; i++) { -// let player = allPlayers[i] -// if (player && player.canControl) { -// controllablePlayers.push(player) -// } -// } + // for (var i = 0; i < allPlayers.length; i++) { + // let player = allPlayers[i] + // if (player && player.canControl) { + // controllablePlayers.push(player) + // } + // } -// return controllablePlayers -// } + // return controllablePlayers + // } -// function findActivePlayer() { -// let availablePlayers = getAvailablePlayers() -// if (availablePlayers.length === 0) { -// return null -// } + // function findActivePlayer() { + // let availablePlayers = getAvailablePlayers() + // if (availablePlayers.length === 0) { + // return null + // } -// if (selectedPlayerIndex < availablePlayers.length) { -// return availablePlayers[selectedPlayerIndex] -// } else { -// selectedPlayerIndex = 0 -// return availablePlayers[0] -// } -// } + // if (selectedPlayerIndex < availablePlayers.length) { + // return availablePlayers[selectedPlayerIndex] + // } else { + // selectedPlayerIndex = 0 + // return availablePlayers[0] + // } + // } -// // Switch to the most recently active player -// function updateCurrentPlayer() { -// let newPlayer = findActivePlayer() -// if (newPlayer !== currentPlayer) { -// currentPlayer = newPlayer -// currentPosition = currentPlayer ? currentPlayer.position : 0 -// } -// } + // // Switch to the most recently active player + // function updateCurrentPlayer() { + // let newPlayer = findActivePlayer() + // if (newPlayer !== currentPlayer) { + // currentPlayer = newPlayer + // currentPosition = currentPlayer ? currentPlayer.position : 0 + // } + // } -// function playPause() { -// if (currentPlayer) { -// if (currentPlayer.isPlaying) { -// currentPlayer.pause() -// } else { -// currentPlayer.play() -// } -// } -// } + // function playPause() { + // if (currentPlayer) { + // if (currentPlayer.isPlaying) { + // currentPlayer.pause() + // } else { + // currentPlayer.play() + // } + // } + // } -// function play() { -// if (currentPlayer && currentPlayer.canPlay) { -// currentPlayer.play() -// } -// } + // function play() { + // if (currentPlayer && currentPlayer.canPlay) { + // currentPlayer.play() + // } + // } -// function pause() { -// if (currentPlayer && currentPlayer.canPause) { -// currentPlayer.pause() -// } -// } + // function pause() { + // if (currentPlayer && currentPlayer.canPause) { + // currentPlayer.pause() + // } + // } -// function next() { -// if (currentPlayer && currentPlayer.canGoNext) { -// currentPlayer.next() -// } -// } + // function next() { + // if (currentPlayer && currentPlayer.canGoNext) { + // currentPlayer.next() + // } + // } -// function previous() { -// if (currentPlayer && currentPlayer.canGoPrevious) { -// currentPlayer.previous() -// } -// } + // function previous() { + // if (currentPlayer && currentPlayer.canGoPrevious) { + // currentPlayer.previous() + // } + // } -// function seek(position) { -// if (currentPlayer && currentPlayer.canSeek) { -// currentPlayer.position = position -// currentPosition = position -// } -// } + // function seek(position) { + // if (currentPlayer && currentPlayer.canSeek) { + // currentPlayer.position = position + // currentPosition = position + // } + // } -// // Seek to position based on ratio (0.0 to 1.0) -// function seekByRatio(ratio) { -// if (currentPlayer && currentPlayer.canSeek && currentPlayer.length > 0) { -// let seekPosition = ratio * currentPlayer.length -// currentPlayer.position = seekPosition -// currentPosition = seekPosition -// } -// } + // // Seek to position based on ratio (0.0 to 1.0) + // function seekByRatio(ratio) { + // if (currentPlayer && currentPlayer.canSeek && currentPlayer.length > 0) { + // let seekPosition = ratio * currentPlayer.length + // currentPlayer.position = seekPosition + // currentPosition = seekPosition + // } + // } -// // Update progress bar every second while playing -// Timer { -// id: positionTimer -// interval: 1000 -// running: currentPlayer && currentPlayer.isPlaying -// && currentPlayer.length > 0 -// && currentPlayer.playbackState === MprisPlaybackState.Playing -// repeat: true -// onTriggered: { -// if (currentPlayer && currentPlayer.isPlaying -// && currentPlayer.playbackState === MprisPlaybackState.Playing) { -// currentPosition = currentPlayer.position -// } else { -// running = false -// } -// } -// } + // // Update progress bar every second while playing + // Timer { + // id: positionTimer + // interval: 1000 + // running: currentPlayer && currentPlayer.isPlaying && currentPlayer.length > 0 + // && currentPlayer.playbackState === MprisPlaybackState.Playing + // repeat: true + // onTriggered: { + // if (currentPlayer && currentPlayer.isPlaying && currentPlayer.playbackState === MprisPlaybackState.Playing) { + // currentPosition = currentPlayer.position + // } else { + // running = false + // } + // } + // } -// // Reset position when switching to inactive player -// onCurrentPlayerChanged: { -// if (!currentPlayer || !currentPlayer.isPlaying -// || currentPlayer.playbackState !== MprisPlaybackState.Playing) { -// currentPosition = 0 -// } -// } + // // Reset position when switching to inactive player + // onCurrentPlayerChanged: { + // if (!currentPlayer || !currentPlayer.isPlaying || currentPlayer.playbackState !== MprisPlaybackState.Playing) { + // currentPosition = 0 + // } + // } -// // Update current player when available players change -// Connections { -// target: Mpris.players -// function onValuesChanged() { -// updateCurrentPlayer() -// } -// } + // // Update current player when available players change + // Connections { + // target: Mpris.players + // function onValuesChanged() { + // updateCurrentPlayer() + // } + // } -// Cava { -// id: cava -// count: 44 -// } + // Cava { + // id: cava + // count: 44 + // } -// // Expose cava values -// property alias cavaValues: cava.values -// } + // // Expose cava values + // property alias cavaValues: cava.values +} diff --git a/Widgets/NToggle.qml b/Widgets/NToggle.qml index c4cfc95..1c191f7 100644 --- a/Widgets/NToggle.qml +++ b/Widgets/NToggle.qml @@ -11,7 +11,7 @@ RowLayout { property string description: "" property bool value: false property bool hovering: false - property var onToggled: function (value: bool) {} + property var onToggled: function (value) {} Layout.fillWidth: true @@ -41,7 +41,7 @@ RowLayout { width: Style.baseWidgetSize * 1.625 * scaling height: Style.baseWidgetSize * scaling radius: height * 0.5 - color: value ? Colors.accentPrimary :Colors.surfaceVariant + color: value ? Colors.accentPrimary : Colors.surfaceVariant border.color: value ? Colors.accentPrimary : Colors.outline border.width: Math.min(1, Style.borderMedium * scaling) @@ -70,8 +70,8 @@ RowLayout { onEntered: hovering = true onExited: hovering = false onClicked: { - value = !value; - root.onToggled(value); + value = !value + root.onToggled(value) } } } From a6e5fec5dbfb1121f09f5b59c9f74616192d4b35 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 14:54:47 -0400 Subject: [PATCH 065/394] Build fix --- Modules/Background/WallpaperPicker.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/Modules/Background/WallpaperPicker.qml b/Modules/Background/WallpaperPicker.qml index 5fc2b8b..0b1a417 100644 --- a/Modules/Background/WallpaperPicker.qml +++ b/Modules/Background/WallpaperPicker.qml @@ -1,5 +1,6 @@ pragma Singleton +import QtQuick import Quickshell import qs.Services From 08823733d0635b68590d0b464b4e82d8f40dc01c Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 14:55:23 -0400 Subject: [PATCH 066/394] Duration.js got merged into Time singleton --- Helpers/Duration.js | 18 ------------------ Modules/Bar/Battery.qml | 11 ++++++++--- Services/Time.qml | 20 ++++++++++++++++++++ 3 files changed, 28 insertions(+), 21 deletions(-) delete mode 100644 Helpers/Duration.js diff --git a/Helpers/Duration.js b/Helpers/Duration.js deleted file mode 100644 index 6402051..0000000 --- a/Helpers/Duration.js +++ /dev/null @@ -1,18 +0,0 @@ -// Use to display the time remaining on the Battery widget -function formatVagueHumanReadableDuration(totalSeconds) { - const hours = Math.floor(totalSeconds / 3600); - const minutes = Math.floor((totalSeconds - (hours * 3600)) / 60); - const seconds = totalSeconds - (hours * 3600) - (minutes * 60); - - var str = ""; - if (hours) { - str += hours.toString() + "h"; - } - if (minutes) { - str += minutes.toString() + "m"; - } - if (!hours && !minutes) { - str += seconds.toString() + "s"; - } - return str; -} \ No newline at end of file diff --git a/Modules/Bar/Battery.qml b/Modules/Bar/Battery.qml index b278e71..ac21dcc 100644 --- a/Modules/Bar/Battery.qml +++ b/Modules/Bar/Battery.qml @@ -4,7 +4,6 @@ import Quickshell.Services.UPower import QtQuick.Layouts import qs.Services import qs.Widgets -import "../../Helpers/Duration.js" as Duration NPill { id: root @@ -58,16 +57,22 @@ NPill { textColor: charging ? Colors.accentPrimary : Colors.textPrimary tooltipText: { let lines = [] + + if (testMode) { + lines.push("Time left: " + Time.formatVagueHumanReadableDuration(1234567)) + return lines.join("\n"); + } + if (!root.isReady) { return "" } if (root.battery.timeToEmpty > 0) { - lines.push("Time left: " + Time.formatVagueHumanReadableTime(root.battery.timeToEmpty)) + lines.push("Time left: " + Time.formatVagueHumanReadableDuration(root.battery.timeToEmpty)) } if (root.battery.timeToFull > 0) { - lines.push("Time until full: " + Time.formatVagueHumanReadableTime(root.battery.timeToFull)) + lines.push("Time until full: " + Time.formatVagueHumanReadableDuration(root.battery.timeToFull)) } if (root.battery.changeRate !== undefined) { diff --git a/Services/Time.qml b/Services/Time.qml index 07650db..702e1b9 100644 --- a/Services/Time.qml +++ b/Services/Time.qml @@ -38,6 +38,26 @@ Singleton { + (Settings.data.location.reverseDayMonth ? `${month} ${day}${suffix} ${year}` : `${day}${suffix} ${month} ${year}`) } + // Format an easy to read approximate duration ex: 4h32m + // Used to display the time remaining on the Battery widget + function formatVagueHumanReadableDuration(totalSeconds) { + const hours = Math.floor(totalSeconds / 3600) + const minutes = Math.floor((totalSeconds - (hours * 3600)) / 60) + const seconds = totalSeconds - (hours * 3600) - (minutes * 60) + + var str = "" + if (hours) { + str += hours.toString() + "h" + } + if (minutes) { + str += minutes.toString() + "m" + } + if (!hours && !minutes) { + str += seconds.toString() + "s" + } + return str + } + Timer { interval: 1000 repeat: true From 3a215de0f388e4fff8235e11e0b378aad76a8a11 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 14:56:19 -0400 Subject: [PATCH 067/394] Battery: less stupid test duration --- Modules/Bar/Battery.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Bar/Battery.qml b/Modules/Bar/Battery.qml index ac21dcc..ebbb6f3 100644 --- a/Modules/Bar/Battery.qml +++ b/Modules/Bar/Battery.qml @@ -59,7 +59,7 @@ NPill { let lines = [] if (testMode) { - lines.push("Time left: " + Time.formatVagueHumanReadableDuration(1234567)) + lines.push("Time left: " + Time.formatVagueHumanReadableDuration(12345)) return lines.join("\n"); } From 275c2ae8ba7d93f02f891e7f25bcfe470020a9d7 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 15:02:53 -0400 Subject: [PATCH 068/394] Tooltip: text should not be bold --- Widgets/NTooltip.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/Widgets/NTooltip.qml b/Widgets/NTooltip.qml index 31ac67f..076d1cf 100644 --- a/Widgets/NTooltip.qml +++ b/Widgets/NTooltip.qml @@ -112,6 +112,7 @@ Window { anchors.centerIn: parent text: root.text font.pointSize: Style.fontSizeMedium * scaling + font.weight: Style.fontWeightRegular horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter wrapMode: Text.Wrap From 9e0bb64d0708ea2bb1fc1fd41680743b665ca504 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 15:09:58 -0400 Subject: [PATCH 069/394] NIconButton: added native tooltip handling --- Modules/Bar/Bar.qml | 4 ++-- Modules/Bar/Battery.qml | 2 +- Modules/DemoPanel/DemoPanel.qml | 15 +-------------- Widgets/NIconButton.qml | 14 ++++++++++++++ 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index e803582..c9c8365 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -83,8 +83,8 @@ Variants { NIconButton { id: demoPanelToggle - icon: "experiment" + tooltipText: "Open demo panel" anchors.verticalCenter: parent.verticalCenter onClicked: function () { demoPanel.isLoaded = !demoPanel.isLoaded @@ -93,8 +93,8 @@ Variants { NIconButton { id: sidePanelToggle - icon: "widgets" + tooltipText: "Open side panel" anchors.verticalCenter: parent.verticalCenter onClicked: function () { // Map this button's center to the screen and open the side panel below it diff --git a/Modules/Bar/Battery.qml b/Modules/Bar/Battery.qml index ebbb6f3..ff80f00 100644 --- a/Modules/Bar/Battery.qml +++ b/Modules/Bar/Battery.qml @@ -60,7 +60,7 @@ NPill { if (testMode) { lines.push("Time left: " + Time.formatVagueHumanReadableDuration(12345)) - return lines.join("\n"); + return lines.join("\n") } if (!root.isReady) { diff --git a/Modules/DemoPanel/DemoPanel.qml b/Modules/DemoPanel/DemoPanel.qml index 5b26c70..167e849 100644 --- a/Modules/DemoPanel/DemoPanel.qml +++ b/Modules/DemoPanel/DemoPanel.qml @@ -52,18 +52,6 @@ NLoader { NIconButton { id: myIconButton icon: "refresh" - onEntered: function () { - myTooltip.show() - } - onExited: function () { - myTooltip.hide() - } - } - NTooltip { - id: myTooltip - target: myIconButton - positionAbove: false - text: "Hello world" } NDivider { @@ -74,9 +62,8 @@ NLoader { // NToggle ColumnLayout { spacing: Style.marginLarge * scaling - uniformCellSizes: true NText { - text: "NToggle + NTooltip" + text: "NToggle" color: Colors.accentSecondary } diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml index 1c2e370..40a996f 100644 --- a/Widgets/NIconButton.qml +++ b/Widgets/NIconButton.qml @@ -11,6 +11,7 @@ Rectangle { property real sizeMultiplier: 0.8 property real size: Style.baseWidgetSize * sizeMultiplier * scaling property string icon + property string tooltipText property bool enabled: true property bool hovering: false property var onEntered: function () {} @@ -37,16 +38,29 @@ Rectangle { opacity: root.enabled ? Style.opacityFull : Style.opacityMedium } + NTooltip { + id: tooltip + target: root + positionAbove: false + text: root.tooltipText + } + MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor hoverEnabled: true onEntered: { hovering = true + if (tooltipText) { + tooltip.show() + } root.onEntered() } onExited: { hovering = false + if (tooltipText) { + tooltip.hide() + } root.onExited() } onClicked: { From 5fd3c4a53ef55bcebbec60f730913fe92ae4f046 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 15:37:26 -0400 Subject: [PATCH 070/394] Working on volume --- Modules/Audio/AudioDeviceSelector.qml | 369 ++++++++++++++++++++++++++ Modules/Bar/Bar.qml | 7 + Modules/Bar/Volume.qml | 130 +++++++++ Services/Scaling.qml | 2 +- Services/Style.qml | 1 + Widgets/NPill.qml | 13 +- 6 files changed, 515 insertions(+), 7 deletions(-) create mode 100644 Modules/Audio/AudioDeviceSelector.qml create mode 100644 Modules/Bar/Volume.qml diff --git a/Modules/Audio/AudioDeviceSelector.qml b/Modules/Audio/AudioDeviceSelector.qml new file mode 100644 index 0000000..7cd5de6 --- /dev/null +++ b/Modules/Audio/AudioDeviceSelector.qml @@ -0,0 +1,369 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell.Services.Pipewire +import qs.Services +import qs.Widgets + +NPanel { + id: ioSelector + + // property int tabIndex: 0 + // property Item anchorItem: null + + // signal panelClosed() + + // function sinkNodes() { + // let nodes = Pipewire.nodes && Pipewire.nodes.values ? Pipewire.nodes.values.filter(function(n) { + // return n.isSink && n.audio && n.isStream === false; + // }) : []; + // if (Pipewire.defaultAudioSink) + // nodes = nodes.slice().sort(function(a, b) { + // if (a.id === Pipewire.defaultAudioSink.id) + // return -1; + + // if (b.id === Pipewire.defaultAudioSink.id) + // return 1; + + // return 0; + // }); + + // return nodes; + // } + + // function sourceNodes() { + // let nodes = Pipewire.nodes && Pipewire.nodes.values ? Pipewire.nodes.values.filter(function(n) { + // return !n.isSink && n.audio && n.isStream === false; + // }) : []; + // if (Pipewire.defaultAudioSource) + // nodes = nodes.slice().sort(function(a, b) { + // if (a.id === Pipewire.defaultAudioSource.id) + // return -1; + + // if (b.id === Pipewire.defaultAudioSource.id) + // return 1; + + // return 0; + // }); + + // return nodes; + // } + + // Component.onCompleted: { + // if (Pipewire.nodes && Pipewire.nodes.values) { + // for (var i = 0; i < Pipewire.nodes.values.length; ++i) { + // var n = Pipewire.nodes.values[i]; + // } + // } + // } + // Component.onDestruction: { + // } + // onVisibleChanged: { + // if (!visible) + // panelClosed(); + + // } + + // // Bind all Pipewire nodes so their properties are valid + // PwObjectTracker { + // id: nodeTracker + + // objects: Pipewire.nodes + // } + + // Rectangle { + // color: Theme.backgroundPrimary + // radius: 20 + // width: 340 + // height: 340 + // anchors.top: parent.top + // anchors.right: parent.right + // anchors.topMargin: 4 + // anchors.rightMargin: 4 + + // // Prevent closing when clicking in the panel bg + // MouseArea { + // anchors.fill: parent + // } + + // ColumnLayout { + // anchors.fill: parent + // anchors.margins: 16 + // spacing: 10 + + // // Tabs centered inside the window + // RowLayout { + // Layout.fillWidth: true + // Layout.alignment: Qt.AlignHCenter + // spacing: 0 + + // Tabs { + // id: ioTabs + + // tabsModel: [{ + // "label": "Output", + // "icon": "volume_up" + // }, { + // "label": "Input", + // "icon": "mic" + // }] + // currentIndex: tabIndex + // onTabChanged: { + // tabIndex = currentIndex; + // } + // } + + // } + + // // Add vertical space between tabs and entries + // Item { + // height: 36 + // Layout.fillWidth: true + // } + + // // Output Devices + // Flickable { + // id: sinkList + + // visible: tabIndex === 0 + // contentHeight: sinkColumn.height + // clip: true + // interactive: contentHeight > height + // width: parent.width + // height: 220 + + // ColumnLayout { + // id: sinkColumn + + // width: sinkList.width + // spacing: 6 + + // Repeater { + // model: ioSelector.sinkNodes() + + // Rectangle { + // width: parent.width + // height: 36 + // color: "transparent" + // radius: 6 + + // RowLayout { + // anchors.fill: parent + // anchors.margins: 6 + // spacing: 8 + + // Text { + // text: "volume_up" + // font.family: "Material Symbols Outlined" + // font.pixelSize: 16 * Theme.scale(screen) + // color: (Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary + // Layout.alignment: Qt.AlignVCenter + // } + + // ColumnLayout { + // Layout.fillWidth: true + // spacing: 1 + // Layout.maximumWidth: sinkList.width - 120 // Reserve space for the Set button + + // Text { + // text: modelData.nickname || modelData.description || modelData.name + // font.bold: true + // font.pixelSize: 12 * Theme.scale(screen) + // color: (Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary + // elide: Text.ElideRight + // maximumLineCount: 1 + // Layout.fillWidth: true + // } + + // Text { + // text: modelData.description !== modelData.nickname ? modelData.description : "" + // font.pixelSize: 10 * Theme.scale(screen) + // color: Theme.textSecondary + // elide: Text.ElideRight + // maximumLineCount: 1 + // Layout.fillWidth: true + // } + + // } + + // Rectangle { + // visible: Pipewire.preferredDefaultAudioSink !== modelData + // width: 60 + // height: 20 + // radius: 4 + // color: Theme.accentPrimary + // border.color: Theme.accentPrimary + // border.width: 1 + // Layout.alignment: Qt.AlignVCenter + + // Text { + // anchors.centerIn: parent + // text: "Set" + // color: Theme.onAccent + // font.pixelSize: 10 * Theme.scale(screen) + // font.bold: true + // } + + // MouseArea { + // anchors.fill: parent + // cursorShape: Qt.PointingHandCursor + // onClicked: Pipewire.preferredDefaultAudioSink = modelData + // } + + // } + + // Text { + // text: "(Current)" + // visible: Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id + // color: Theme.accentPrimary + // font.pixelSize: 10 * Theme.scale(screen) + // Layout.alignment: Qt.AlignVCenter + // } + + // } + + // } + + // } + + // } + + // ScrollBar.vertical: ScrollBar { + // } + + // } + + // // Input Devices + // Flickable { + // id: sourceList + + // visible: tabIndex === 1 + // contentHeight: sourceColumn.height + // clip: true + // interactive: contentHeight > height + // width: parent.width + // height: 220 + + // ColumnLayout { + // id: sourceColumn + + // width: sourceList.width + // spacing: 6 + + // Repeater { + // model: ioSelector.sourceNodes() + + // Rectangle { + // width: parent.width + // height: 36 + // color: "transparent" + // radius: 6 + + // RowLayout { + // anchors.fill: parent + // anchors.margins: 6 + // spacing: 8 + + // Text { + // text: "mic" + // font.family: "Material Symbols Outlined" + // font.pixelSize: 16 * Theme.scale(screen) + // color: (Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary + // Layout.alignment: Qt.AlignVCenter + // } + + // ColumnLayout { + // Layout.fillWidth: true + // spacing: 1 + // Layout.maximumWidth: sourceList.width - 120 // Reserve space for the Set button + + // Text { + // text: modelData.nickname || modelData.description || modelData.name + // font.bold: true + // font.pixelSize: 12 * Theme.scale(screen) + // color: (Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary + // elide: Text.ElideRight + // maximumLineCount: 1 + // Layout.fillWidth: true + // } + + // Text { + // text: modelData.description !== modelData.nickname ? modelData.description : "" + // font.pixelSize: 10 * Theme.scale(screen) + // color: Theme.textSecondary + // elide: Text.ElideRight + // maximumLineCount: 1 + // Layout.fillWidth: true + // } + + // } + + // Rectangle { + // visible: Pipewire.preferredDefaultAudioSource !== modelData + // width: 60 + // height: 20 + // radius: 4 + // color: Theme.accentPrimary + // border.color: Theme.accentPrimary + // border.width: 1 + // Layout.alignment: Qt.AlignVCenter + + // Text { + // anchors.centerIn: parent + // text: "Set" + // color: Theme.onAccent + // font.pixelSize: 10 * Theme.scale(screen) + // font.bold: true + // } + + // MouseArea { + // anchors.fill: parent + // cursorShape: Qt.PointingHandCursor + // onClicked: Pipewire.preferredDefaultAudioSource = modelData + // } + + // } + + // Text { + // text: "(Current)" + // visible: Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id + // color: Theme.accentPrimary + // font.pixelSize: 10 * Theme.scale(screen) + // Layout.alignment: Qt.AlignVCenter + // } + + // } + + // } + + // } + + // } + + // ScrollBar.vertical: ScrollBar { + // } + + // } + + // } + + // } + + // Connections { + // function onReadyChanged() { + // if (Pipewire.ready && Pipewire.nodes && Pipewire.nodes.values) { + // for (var i = 0; i < Pipewire.nodes.values.length; ++i) { + // var n = Pipewire.nodes.values[i]; + // } + // } + // } + + // function onDefaultAudioSinkChanged() { + // } + + // function onDefaultAudioSourceChanged() { + // } + + // target: Pipewire + // } +} diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index c9c8365..f0f1b89 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -38,6 +38,7 @@ Variants { layer.enabled: true } + // Left Row { id: leftSection @@ -53,6 +54,7 @@ Variants { } } + // Center Row { id: centerSection @@ -64,6 +66,7 @@ Variants { Workspace {} } + // Right Row { id: rightSection @@ -77,6 +80,10 @@ Variants { anchors.verticalCenter: parent.verticalCenter } + Volume { + anchors.verticalCenter: parent.verticalCenter + } + Clock { anchors.verticalCenter: parent.verticalCenter } diff --git a/Modules/Bar/Volume.qml b/Modules/Bar/Volume.qml new file mode 100644 index 0000000..f00e09c --- /dev/null +++ b/Modules/Bar/Volume.qml @@ -0,0 +1,130 @@ +import QtQuick +import Quickshell +import qs.Services +import qs.Modules.Audio +import qs.Widgets + +Item { + id: volumeDisplay + property var shell + property int volume: 0 + property bool firstChange: true + + width: pillIndicator.width + height: pillIndicator.height + + function getVolumeColor() { + if (volume <= 100) + return Colors.accentPrimary + // Calculate interpolation factor (0 at 100%, 1 at 200%) + var factor = (volume - 100) / 100 + // Blend between accent and warning colors + return Qt.rgba(Colors.accentPrimary.r + (Colors.warning.r - Colors.accentPrimary.r) * factor, + Colors.accentPrimary.g + (Colors.warning.g - Colors.accentPrimary.g) * factor, + Colors.accentPrimary.b + (Colors.warning.b - Colors.accentPrimary.b) * factor, 1) + } + + function getIconColor() { + if (volume <= 100) + return Colors.textPrimary + return getVolumeColor() // Only use warning blend when >100% + } + + NPill { + id: pillIndicator + icon: shell && shell.defaultAudioSink && shell.defaultAudioSink.audio + && shell.defaultAudioSink.audio.muted ? "volume_off" : (volume === 0 ? "volume_off" : (volume < 30 ? "volume_down" : "volume_up")) + text: volume + "%" + + pillColor: Colors.surfaceVariant + iconCircleColor: getVolumeColor() + iconTextColor: Colors.backgroundPrimary + textColor: Colors.textPrimary + collapsedIconColor: getIconColor() + autoHide: true + + // StyledTooltip { + // id: volumeTooltip + // text: "Volume: " + volume + "%\nLeft click for advanced settings.\nScroll up/down to change volume." + // positionAbove: false + // tooltipVisible: !ioSelector.visible && volumeDisplay.containsMouse + // targetItem: pillIndicator + // delay: 1500 + // } + + // MouseArea { + // anchors.fill: parent + // hoverEnabled: true + // cursorShape: Qt.PointingHandCursor + // onClicked: { + // if (ioSelector.visible) { + // ioSelector.dismiss(); + // } else { + // ioSelector.show(); + // } + // } + // } + } + + Connections { + target: shell ?? null + function onVolumeChanged() { + if (shell) { + const clampedVolume = Math.max(0, Math.min(200, shell.volume)) + if (clampedVolume !== volume) { + volume = clampedVolume + pillIndicator.text = volume + "%" + pillIndicator.icon = shell.defaultAudioSink && shell.defaultAudioSink.audio + && shell.defaultAudioSink.audio.muted ? "volume_off" : (volume === 0 ? "volume_off" : (volume + < 30 ? "volume_down" : "volume_up")) + + if (firstChange) { + firstChange = false + } else { + pillIndicator.show() + } + } + } + } + } + + Component.onCompleted: { + if (shell && shell.volume !== undefined) { + volume = Math.max(0, Math.min(200, shell.volume)) + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + propagateComposedEvents: true + onEntered: { + volumeDisplay.containsMouse = true + pillIndicator.autoHide = false + pillIndicator.showDelayed() + } + onExited: { + volumeDisplay.containsMouse = false + pillIndicator.autoHide = true + pillIndicator.hide() + } + cursorShape: Qt.PointingHandCursor + onWheel: wheel => { + if (!shell) + return + let step = 5 + if (wheel.angleDelta.y > 0) { + shell.updateVolume(Math.min(200, shell.volume + step)) + } else if (wheel.angleDelta.y < 0) { + shell.updateVolume(Math.max(0, shell.volume - step)) + } + } + } + + // AudioDeviceSelector { + // id: ioSelector + // onPanelClosed: ioSelector.dismiss() + // } + property bool containsMouse: false +} diff --git a/Services/Scaling.qml b/Services/Scaling.qml index 912cee5..479b16c 100644 --- a/Services/Scaling.qml +++ b/Services/Scaling.qml @@ -57,6 +57,6 @@ Singleton { } // 3) Safe default - return 1.0 + return 1.4 } } diff --git a/Services/Style.qml b/Services/Style.qml index 44c2031..d737c72 100644 --- a/Services/Style.qml +++ b/Services/Style.qml @@ -62,6 +62,7 @@ Singleton { property int marginXL: 20 // Opacity + property real opacityNone: 0.0 property real opacityLight: 0.25 property real opacityMedium: 0.5 property real opacityHeavy: 0.75 diff --git a/Widgets/NPill.qml b/Widgets/NPill.qml index a809bfd..892489c 100644 --- a/Widgets/NPill.qml +++ b/Widgets/NPill.qml @@ -25,7 +25,7 @@ Item { // Exposed width logic readonly property int pillHeight: Style.baseWidgetSize * sizeMultiplier * scaling readonly property int iconSize: Style.baseWidgetSize * sizeMultiplier * scaling - readonly property int pillPaddingHorizontal: 14 * scaling + readonly property int pillPaddingHorizontal: Style.marginMedium * scaling readonly property int pillOverlap: iconSize * 0.5 readonly property int maxPillWidth: Math.max(1, textItem.implicitWidth + pillPaddingHorizontal * 2 + pillOverlap) @@ -41,7 +41,7 @@ Item { width: showPill ? maxPillWidth : 1 height: pillHeight x: (iconCircle.x + iconCircle.width / 2) - width - opacity: showPill ? 1 : 0 + opacity: showPill ? Style.opacityFull : Style.opacityNone color: pillColor topLeftRadius: pillHeight * 0.5 bottomLeftRadius: pillHeight * 0.5 @@ -68,7 +68,7 @@ Item { Behavior on opacity { enabled: showAnim.running || hideAnim.running NumberAnimation { - duration: 250 + duration: Style.animationNormal easing.type: Easing.OutCubic } } @@ -78,14 +78,14 @@ Item { id: iconCircle width: iconSize height: iconSize - radius: width / 2 + radius: width * 0.5 color: showPill ? iconCircleColor : "transparent" anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right Behavior on color { ColorAnimation { - duration: 200 + duration: Style.animationNormal easing.type: Easing.InOutQuad } } @@ -134,8 +134,9 @@ Item { duration: 2500 } ScriptAction { - script: if (shouldAnimateHide) + script: if (shouldAnimateHide) { hideAnim.start() + } } } From b2d3f401c46968dbbe4e03f74cb477b83ae13b8c Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 10 Aug 2025 22:02:48 +0200 Subject: [PATCH 071/394] Add notification, Use font.pointSize --- Bin/test_notifications.sh | 11 ++ Modules/Notification/Notification.qml | 192 ++++++++++++++++++++++++++ Modules/SidePanel/MediaCard.qml | 12 +- Modules/SidePanel/ProfileCard.qml | 8 +- Modules/SidePanel/SidePanel.qml | 2 +- Modules/SidePanel/WeatherCard.qml | 10 +- Services/NotificationService.qml | 139 +++++++++++++++++++ shell.qml | 5 + 8 files changed, 361 insertions(+), 18 deletions(-) create mode 100755 Bin/test_notifications.sh create mode 100644 Modules/Notification/Notification.qml create mode 100644 Services/NotificationService.qml diff --git a/Bin/test_notifications.sh b/Bin/test_notifications.sh new file mode 100755 index 0000000..4101024 --- /dev/null +++ b/Bin/test_notifications.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +echo "Sending 8 test notifications..." + +# Send 8 notifications with numbers +for i in {1..8}; do + notify-send "Notification $i" "This is test notification number $i of 8" + sleep 1 +done + +echo "All notifications sent!" \ No newline at end of file diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml new file mode 100644 index 0000000..6ced941 --- /dev/null +++ b/Modules/Notification/Notification.qml @@ -0,0 +1,192 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Quickshell.Wayland +import Quickshell.Services.Notifications +import qs.Services +import qs.Widgets + +// Simple notification popup - displays multiple notifications +PanelWindow { + id: root + + readonly property real scaling: Scaling.scale(screen) + + color: "transparent" + visible: notificationService.notificationModel.count > 0 + anchors.top: true + anchors.right: true + margins.top: (Style.barHeight + 10) * scaling + margins.right: 10 * scaling + implicitWidth: 360 * scaling + implicitHeight: Math.min(notificationStack.implicitHeight, (notificationService.maxVisible * 120) * scaling) + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.exclusionMode: ExclusionMode.Ignore + + // Use the notification service + property var notificationService: NotificationService { } + + // Access the notification model from the service + property ListModel notificationModel: notificationService.notificationModel + + // Track notifications being removed for animation + property var removingNotifications: ({}) + + // Connect to animation signal from service + Component.onCompleted: { + notificationService.animateAndRemove.connect(function(notification, index) { + // Find the delegate and trigger its animation + if (notificationStack.children && notificationStack.children[index]) { + let delegate = notificationStack.children[index] + if (delegate && delegate.animateOut) { + delegate.animateOut() + } + } + }) + } + + + + // Main notification container + Column { + id: notificationStack + anchors.top: parent.top + anchors.right: parent.right + spacing: 8 * scaling + width: 360 * scaling + visible: true + + + + // Multiple notifications display + Repeater { + model: notificationModel + delegate: Rectangle { + width: 360 * scaling + height: Math.max(80 * scaling, contentColumn.implicitHeight + (Style.marginMedium * 2 * scaling)) + clip: true + color: Colors.backgroundPrimary + radius: Style.radiusMedium * scaling + border.color: Colors.backgroundTertiary + border.width: Math.min(1, Style.borderThin * scaling) + + // Animation properties + property real scaleValue: 0.8 + property real opacityValue: 0.0 + property bool isRemoving: false + + // Scale and fade-in animation + scale: scaleValue + opacity: opacityValue + + // Animate in when the item is created + Component.onCompleted: { + scaleValue = 1.0 + opacityValue = 1.0 + } + + // Animate out when being removed + function animateOut() { + isRemoving = true + scaleValue = 0.8 + opacityValue = 0.0 + } + + // Timer for delayed removal after animation + Timer { + id: removalTimer + interval: Style.animationSlow + repeat: false + onTriggered: { + notificationService.forceRemoveNotification(model.rawNotification) + } + } + + // Check if this notification is being removed + onIsRemovingChanged: { + if (isRemoving) { + // Remove from model after animation completes + removalTimer.start() + } + } + + // Animation behaviors + Behavior on scale { + NumberAnimation { + duration: Style.animationSlow + easing.type: Easing.OutBack + } + } + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutQuad + } + } + + + + Column { + id: contentColumn + anchors.fill: parent + anchors.margins: Style.marginMedium * scaling + spacing: Style.marginSmall * scaling + + RowLayout { + spacing: Style.marginSmall * scaling + NText { + text: (model.appName || model.desktopEntry) || "Unknown App" + color: Colors.accentSecondary + font.pointSize: Style.fontSizeSmall + } + Rectangle { + width: 6 * scaling; height: 6 * scaling; radius: 3 * scaling + color: (model.urgency === NotificationUrgency.Critical) ? Colors.error : + (model.urgency === NotificationUrgency.Low) ? Colors.textSecondary : Colors.accentPrimary + Layout.alignment: Qt.AlignVCenter + } + Item { Layout.fillWidth: true } + NText { + text: notificationService.formatTimestamp(model.timestamp) + color: Colors.textSecondary + font.pointSize: Style.fontSizeSmall + } + } + + NText { + text: model.summary || "No summary" + font.pointSize: Style.fontSizeLarge + font.bold: true + color: Colors.textPrimary + wrapMode: Text.Wrap + width: 300 * scaling + maximumLineCount: 3 + elide: Text.ElideRight + } + + NText { + text: model.body || "" + font.pointSize: Style.fontSizeSmall + color: Colors.textSecondary + wrapMode: Text.Wrap + width: 300 * scaling + maximumLineCount: 5 + elide: Text.ElideRight + } + } + + NIconButton { + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: Style.marginSmall * scaling + icon: "close" + onClicked: function() { + animateOut() + } + } + } + } + } +} diff --git a/Modules/SidePanel/MediaCard.qml b/Modules/SidePanel/MediaCard.qml index 6e574f9..ecdddbd 100644 --- a/Modules/SidePanel/MediaCard.qml +++ b/Modules/SidePanel/MediaCard.qml @@ -19,12 +19,10 @@ NBox { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - anchors.margins: Style.marginXL * scaling - spacing: Style.marginSmall * scaling + anchors.margins: Style.marginMedium * scaling + spacing: Style.marginMedium * scaling - Item { - height: 36 * scaling - } + Item { height: Style.marginLarge * scaling } Text { text: "music_note" @@ -40,8 +38,6 @@ NBox { anchors.horizontalCenter: parent.horizontalCenter } - Item { - height: 36 * scaling - } + Item { height: Style.marginLarge * scaling } } } diff --git a/Modules/SidePanel/ProfileCard.qml b/Modules/SidePanel/ProfileCard.qml index cd75e56..7488e9d 100644 --- a/Modules/SidePanel/ProfileCard.qml +++ b/Modules/SidePanel/ProfileCard.qml @@ -25,8 +25,8 @@ NBox { Item { id: avatarBox - width: 40 * scaling - height: 40 * scaling + width: Style.baseWidgetSize * 1.25 * scaling + height: Style.baseWidgetSize * 1.25 * scaling Image { id: avatarImage @@ -69,11 +69,11 @@ NBox { } NIconButton { icon: "settings" - sizeMultiplier: 0.8 + sizeMultiplier: 0.9 } NIconButton { icon: "power_settings_new" - sizeMultiplier: 0.8 + sizeMultiplier: 0.9 } } } diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml index 030b47f..9d32486 100644 --- a/Modules/SidePanel/SidePanel.qml +++ b/Modules/SidePanel/SidePanel.qml @@ -35,7 +35,7 @@ NLoader { readonly property real scaling: Scaling.scale(screen) // Single source of truth for spacing between cards (both axes) - property real cardSpacing: Style.marginLarge * scaling + property real cardSpacing: Style.spacingLarge * scaling // X coordinate from the bar to align this panel under property real anchorX: root.anchorX // Ensure this panel attaches to the intended screen diff --git a/Modules/SidePanel/WeatherCard.qml b/Modules/SidePanel/WeatherCard.qml index a9881fc..2d2e164 100644 --- a/Modules/SidePanel/WeatherCard.qml +++ b/Modules/SidePanel/WeatherCard.qml @@ -18,8 +18,8 @@ NBox { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - anchors.margins: Style.marginLarge * scaling - spacing: Style.marginSmall * scaling + anchors.margins: Style.marginMedium * scaling + spacing: Style.marginMedium * scaling RowLayout { spacing: Style.marginSmall * scaling @@ -46,9 +46,9 @@ NBox { color: Colors.backgroundTertiary } - RowLayout { - Layout.fillWidth: true - spacing: Style.marginLarge * scaling + RowLayout { + Layout.fillWidth: true + spacing: Style.marginMedium * scaling Repeater { model: 5 delegate: ColumnLayout { diff --git a/Services/NotificationService.qml b/Services/NotificationService.qml new file mode 100644 index 0000000..52b572d --- /dev/null +++ b/Services/NotificationService.qml @@ -0,0 +1,139 @@ +import QtQuick +import qs.Services +import Quickshell.Services.Notifications + + +QtObject { + id: root + + // Notification server instance + property NotificationServer server: NotificationServer { + id: notificationServer + + // Server capabilities + keepOnReload: false + imageSupported: true + actionsSupported: true + actionIconsSupported: true + bodyMarkupSupported: true + bodySupported: true + persistenceSupported: true + inlineReplySupported: true + bodyHyperlinksSupported: true + bodyImagesSupported: true + + // Signal when notification is received + onNotification: function(notification) { + + // Track the notification + notification.tracked = true + + // Connect to closed signal for cleanup + notification.closed.connect(function() { + root.removeNotification(notification) + }) + + // Add to our model + root.addNotification(notification) + } + } + + // List model to hold notifications + property ListModel notificationModel: ListModel { } + + // Maximum visible notifications + property int maxVisible: 5 + + // Auto-hide timer + property Timer hideTimer: Timer { + interval: 5000 // 5 seconds + repeat: true + running: notificationModel.count > 0 + + onTriggered: { + if (notificationModel.count === 0) { + return + } + + // Always remove the oldest notification (last in the list) + let oldestNotification = notificationModel.get(notificationModel.count - 1).rawNotification + if (oldestNotification && !oldestNotification.transient) { + // Trigger animation signal instead of direct dismiss + animateAndRemove(oldestNotification, notificationModel.count - 1) + } + } + } + + // Function to add notification to model + function addNotification(notification) { + notificationModel.insert(0, { + rawNotification: notification, + summary: notification.summary, + body: notification.body, + appName: notification.appName, + urgency: notification.urgency, + timestamp: new Date() + }) + + // Remove oldest notifications if we exceed maxVisible + while (notificationModel.count > maxVisible) { + let oldestNotification = notificationModel.get(notificationModel.count - 1).rawNotification + if (oldestNotification) { + oldestNotification.dismiss() + } + notificationModel.remove(notificationModel.count - 1) + } + } + + // Signal to trigger animation before removal + signal animateAndRemove(var notification, int index) + + // Function to remove notification from model + function removeNotification(notification) { + for (let i = 0; i < notificationModel.count; i++) { + if (notificationModel.get(i).rawNotification === notification) { + // Emit signal to trigger animation first + animateAndRemove(notification, i) + break + } + } + } + + // Function to actually remove notification after animation + function forceRemoveNotification(notification) { + for (let i = 0; i < notificationModel.count; i++) { + if (notificationModel.get(i).rawNotification === notification) { + notificationModel.remove(i) + break + } + } + } + + // Function to format timestamp + function formatTimestamp(timestamp) { + if (!timestamp) return "" + + const now = new Date() + const diff = now - timestamp + + // Less than 1 minute + if (diff < 60000) { + return "now" + } + // Less than 1 hour + else if (diff < 3600000) { + const minutes = Math.floor(diff / 60000) + return `${minutes}m ago` + } + // Less than 24 hours + else if (diff < 86400000) { + const hours = Math.floor(diff / 3600000) + return `${hours}h ago` + } + // More than 24 hours + else { + const days = Math.floor(diff / 86400000) + return `${days}d ago` + } + } +} \ No newline at end of file diff --git a/shell.qml b/shell.qml index 8d4748b..66ae026 100644 --- a/shell.qml +++ b/shell.qml @@ -9,6 +9,7 @@ import qs.Modules.Bar import qs.Modules.DemoPanel import qs.Modules.Background import qs.Modules.SidePanel +import qs.Modules.Notification import qs.Services ShellRoot { @@ -26,4 +27,8 @@ ShellRoot { SidePanel { id: sidePanel } + + Notification { + id: notification + } } From 9418d94f6ddd2b7e1ee837e81d4a0c43dff8c581 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 16:10:35 -0400 Subject: [PATCH 072/394] commited 1.4 scaling by mistake --- Services/Scaling.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Services/Scaling.qml b/Services/Scaling.qml index 479b16c..912cee5 100644 --- a/Services/Scaling.qml +++ b/Services/Scaling.qml @@ -57,6 +57,6 @@ Singleton { } // 3) Safe default - return 1.4 + return 1.0 } } From eda4bc8a6a82a32423526ec3eda2c64a7a3523c0 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 17:04:51 -0400 Subject: [PATCH 073/394] qml format --- Modules/Notification/Notification.qml | 53 +++--- Modules/SidePanel/MediaCard.qml | 8 +- Modules/SidePanel/WeatherCard.qml | 6 +- Services/NotificationService.qml | 253 +++++++++++++------------- 4 files changed, 159 insertions(+), 161 deletions(-) diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index 6ced941..2e59bef 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -25,17 +25,17 @@ PanelWindow { WlrLayershell.exclusionMode: ExclusionMode.Ignore // Use the notification service - property var notificationService: NotificationService { } - + property var notificationService: NotificationService {} + // Access the notification model from the service property ListModel notificationModel: notificationService.notificationModel - + // Track notifications being removed for animation property var removingNotifications: ({}) - + // Connect to animation signal from service Component.onCompleted: { - notificationService.animateAndRemove.connect(function(notification, index) { + notificationService.animateAndRemove.connect(function (notification, index) { // Find the delegate and trigger its animation if (notificationStack.children && notificationStack.children[index]) { let delegate = notificationStack.children[index] @@ -46,8 +46,6 @@ PanelWindow { }) } - - // Main notification container Column { id: notificationStack @@ -57,8 +55,6 @@ PanelWindow { width: 360 * scaling visible: true - - // Multiple notifications display Repeater { model: notificationModel @@ -70,29 +66,29 @@ PanelWindow { radius: Style.radiusMedium * scaling border.color: Colors.backgroundTertiary border.width: Math.min(1, Style.borderThin * scaling) - + // Animation properties property real scaleValue: 0.8 property real opacityValue: 0.0 property bool isRemoving: false - + // Scale and fade-in animation scale: scaleValue opacity: opacityValue - + // Animate in when the item is created Component.onCompleted: { scaleValue = 1.0 opacityValue = 1.0 } - + // Animate out when being removed function animateOut() { isRemoving = true scaleValue = 0.8 opacityValue = 0.0 } - + // Timer for delayed removal after animation Timer { id: removalTimer @@ -102,7 +98,7 @@ PanelWindow { notificationService.forceRemoveNotification(model.rawNotification) } } - + // Check if this notification is being removed onIsRemovingChanged: { if (isRemoving) { @@ -110,7 +106,7 @@ PanelWindow { removalTimer.start() } } - + // Animation behaviors Behavior on scale { NumberAnimation { @@ -118,22 +114,20 @@ PanelWindow { easing.type: Easing.OutBack } } - + Behavior on opacity { NumberAnimation { duration: Style.animationNormal easing.type: Easing.OutQuad } } - - Column { id: contentColumn anchors.fill: parent anchors.margins: Style.marginMedium * scaling spacing: Style.marginSmall * scaling - + RowLayout { spacing: Style.marginSmall * scaling NText { @@ -142,19 +136,22 @@ PanelWindow { font.pointSize: Style.fontSizeSmall } Rectangle { - width: 6 * scaling; height: 6 * scaling; radius: 3 * scaling - color: (model.urgency === NotificationUrgency.Critical) ? Colors.error : - (model.urgency === NotificationUrgency.Low) ? Colors.textSecondary : Colors.accentPrimary + width: 6 * scaling + height: 6 * scaling + radius: 3 * scaling + color: (model.urgency === NotificationUrgency.Critical) ? Colors.error : (model.urgency === NotificationUrgency.Low) ? Colors.textSecondary : Colors.accentPrimary Layout.alignment: Qt.AlignVCenter } - Item { Layout.fillWidth: true } + Item { + Layout.fillWidth: true + } NText { text: notificationService.formatTimestamp(model.timestamp) color: Colors.textSecondary font.pointSize: Style.fontSizeSmall } } - + NText { text: model.summary || "No summary" font.pointSize: Style.fontSizeLarge @@ -165,7 +162,7 @@ PanelWindow { maximumLineCount: 3 elide: Text.ElideRight } - + NText { text: model.body || "" font.pointSize: Style.fontSizeSmall @@ -176,13 +173,13 @@ PanelWindow { elide: Text.ElideRight } } - + NIconButton { anchors.top: parent.top anchors.right: parent.right anchors.margins: Style.marginSmall * scaling icon: "close" - onClicked: function() { + onClicked: function () { animateOut() } } diff --git a/Modules/SidePanel/MediaCard.qml b/Modules/SidePanel/MediaCard.qml index ecdddbd..6142b4c 100644 --- a/Modules/SidePanel/MediaCard.qml +++ b/Modules/SidePanel/MediaCard.qml @@ -22,7 +22,9 @@ NBox { anchors.margins: Style.marginMedium * scaling spacing: Style.marginMedium * scaling - Item { height: Style.marginLarge * scaling } + Item { + height: Style.marginLarge * scaling + } Text { text: "music_note" @@ -38,6 +40,8 @@ NBox { anchors.horizontalCenter: parent.horizontalCenter } - Item { height: Style.marginLarge * scaling } + Item { + height: Style.marginLarge * scaling + } } } diff --git a/Modules/SidePanel/WeatherCard.qml b/Modules/SidePanel/WeatherCard.qml index 2d2e164..875805c 100644 --- a/Modules/SidePanel/WeatherCard.qml +++ b/Modules/SidePanel/WeatherCard.qml @@ -46,9 +46,9 @@ NBox { color: Colors.backgroundTertiary } - RowLayout { - Layout.fillWidth: true - spacing: Style.marginMedium * scaling + RowLayout { + Layout.fillWidth: true + spacing: Style.marginMedium * scaling Repeater { model: 5 delegate: ColumnLayout { diff --git a/Services/NotificationService.qml b/Services/NotificationService.qml index 52b572d..086fecf 100644 --- a/Services/NotificationService.qml +++ b/Services/NotificationService.qml @@ -2,138 +2,135 @@ import QtQuick import qs.Services import Quickshell.Services.Notifications - QtObject { - id: root - - // Notification server instance - property NotificationServer server: NotificationServer { - id: notificationServer - - // Server capabilities - keepOnReload: false - imageSupported: true - actionsSupported: true - actionIconsSupported: true - bodyMarkupSupported: true - bodySupported: true - persistenceSupported: true - inlineReplySupported: true - bodyHyperlinksSupported: true - bodyImagesSupported: true - - // Signal when notification is received - onNotification: function(notification) { - - // Track the notification - notification.tracked = true - - // Connect to closed signal for cleanup - notification.closed.connect(function() { - root.removeNotification(notification) - }) - - // Add to our model - root.addNotification(notification) - } + id: root + + // Notification server instance + property NotificationServer server: NotificationServer { + id: notificationServer + + // Server capabilities + keepOnReload: false + imageSupported: true + actionsSupported: true + actionIconsSupported: true + bodyMarkupSupported: true + bodySupported: true + persistenceSupported: true + inlineReplySupported: true + bodyHyperlinksSupported: true + bodyImagesSupported: true + + // Signal when notification is received + onNotification: function (notification) { + + // Track the notification + notification.tracked = true + + // Connect to closed signal for cleanup + notification.closed.connect(function () { + root.removeNotification(notification) + }) + + // Add to our model + root.addNotification(notification) } - - // List model to hold notifications - property ListModel notificationModel: ListModel { } - - // Maximum visible notifications - property int maxVisible: 5 - - // Auto-hide timer - property Timer hideTimer: Timer { - interval: 5000 // 5 seconds - repeat: true - running: notificationModel.count > 0 - - onTriggered: { - if (notificationModel.count === 0) { - return - } - - // Always remove the oldest notification (last in the list) - let oldestNotification = notificationModel.get(notificationModel.count - 1).rawNotification - if (oldestNotification && !oldestNotification.transient) { - // Trigger animation signal instead of direct dismiss - animateAndRemove(oldestNotification, notificationModel.count - 1) - } - } + } + + // List model to hold notifications + property ListModel notificationModel: ListModel {} + + // Maximum visible notifications + property int maxVisible: 5 + + // Auto-hide timer + property Timer hideTimer: Timer { + interval: 5000 // 5 seconds + repeat: true + running: notificationModel.count > 0 + + onTriggered: { + if (notificationModel.count === 0) { + return + } + + // Always remove the oldest notification (last in the list) + let oldestNotification = notificationModel.get(notificationModel.count - 1).rawNotification + if (oldestNotification && !oldestNotification.transient) { + // Trigger animation signal instead of direct dismiss + animateAndRemove(oldestNotification, notificationModel.count - 1) + } } - - // Function to add notification to model - function addNotification(notification) { - notificationModel.insert(0, { - rawNotification: notification, - summary: notification.summary, - body: notification.body, - appName: notification.appName, - urgency: notification.urgency, - timestamp: new Date() - }) - - // Remove oldest notifications if we exceed maxVisible - while (notificationModel.count > maxVisible) { - let oldestNotification = notificationModel.get(notificationModel.count - 1).rawNotification - if (oldestNotification) { - oldestNotification.dismiss() - } - notificationModel.remove(notificationModel.count - 1) - } + } + + // Function to add notification to model + function addNotification(notification) { + notificationModel.insert(0, { + "rawNotification": notification, + "summary": notification.summary, + "body": notification.body, + "appName": notification.appName, + "urgency": notification.urgency, + "timestamp": new Date() + }) + + // Remove oldest notifications if we exceed maxVisible + while (notificationModel.count > maxVisible) { + let oldestNotification = notificationModel.get(notificationModel.count - 1).rawNotification + if (oldestNotification) { + oldestNotification.dismiss() + } + notificationModel.remove(notificationModel.count - 1) } - - // Signal to trigger animation before removal - signal animateAndRemove(var notification, int index) - - // Function to remove notification from model - function removeNotification(notification) { - for (let i = 0; i < notificationModel.count; i++) { - if (notificationModel.get(i).rawNotification === notification) { - // Emit signal to trigger animation first - animateAndRemove(notification, i) - break - } - } + } + + // Signal to trigger animation before removal + signal animateAndRemove(var notification, int index) + + // Function to remove notification from model + function removeNotification(notification) { + for (var i = 0; i < notificationModel.count; i++) { + if (notificationModel.get(i).rawNotification === notification) { + // Emit signal to trigger animation first + animateAndRemove(notification, i) + break + } } - - // Function to actually remove notification after animation - function forceRemoveNotification(notification) { - for (let i = 0; i < notificationModel.count; i++) { - if (notificationModel.get(i).rawNotification === notification) { - notificationModel.remove(i) - break - } - } + } + + // Function to actually remove notification after animation + function forceRemoveNotification(notification) { + for (var i = 0; i < notificationModel.count; i++) { + if (notificationModel.get(i).rawNotification === notification) { + notificationModel.remove(i) + break + } } - - // Function to format timestamp - function formatTimestamp(timestamp) { - if (!timestamp) return "" - - const now = new Date() - const diff = now - timestamp - - // Less than 1 minute - if (diff < 60000) { - return "now" - } - // Less than 1 hour - else if (diff < 3600000) { - const minutes = Math.floor(diff / 60000) - return `${minutes}m ago` - } - // Less than 24 hours - else if (diff < 86400000) { - const hours = Math.floor(diff / 3600000) - return `${hours}h ago` - } - // More than 24 hours - else { - const days = Math.floor(diff / 86400000) - return `${days}d ago` - } + } + + // Function to format timestamp + function formatTimestamp(timestamp) { + if (!timestamp) + return "" + + const now = new Date() + const diff = now - timestamp + + // Less than 1 minute + if (diff < 60000) { + return "now" + } // Less than 1 hour + else if (diff < 3600000) { + const minutes = Math.floor(diff / 60000) + return `${minutes}m ago` + } // Less than 24 hours + else if (diff < 86400000) { + const hours = Math.floor(diff / 3600000) + return `${hours}h ago` + } // More than 24 hours + else { + const days = Math.floor(diff / 86400000) + return `${days}d ago` } -} \ No newline at end of file + } +} From 56777fc9fd2987c92a4cca496b654285d6c7d9f3 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 17:09:59 -0400 Subject: [PATCH 074/394] NIconButton: hide tooltip when clicking the icon --- Widgets/NIconButton.qml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml index 40a996f..21b3644 100644 --- a/Widgets/NIconButton.qml +++ b/Widgets/NIconButton.qml @@ -65,6 +65,9 @@ Rectangle { } onClicked: { root.onClicked() + if (tooltipText) { + tooltip.hide() + } } } } From 5c7268aaeea7faea95096ea2d888272e408407fb Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 18:28:36 -0400 Subject: [PATCH 075/394] Very minimal volume widget and PWAudioService --- Modules/Audio/Cava.qml | 77 +++++++++++ Modules/Bar/Volume.qml | 176 +++++++++++-------------- Services/MediaPlayer.qml | 263 +++++++++++++++++++------------------ Services/PipeWireAudio.qml | 30 +++++ Widgets/NIconButton.qml | 2 +- Widgets/NPill.qml | 9 ++ shell.qml | 3 +- 7 files changed, 326 insertions(+), 234 deletions(-) create mode 100644 Modules/Audio/Cava.qml create mode 100644 Services/PipeWireAudio.qml diff --git a/Modules/Audio/Cava.qml b/Modules/Audio/Cava.qml new file mode 100644 index 0000000..d0c53b4 --- /dev/null +++ b/Modules/Audio/Cava.qml @@ -0,0 +1,77 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Services + +Scope { + id: root + property int count: 44 + property int noiseReduction: 60 + property string channels: "mono" + property string monoOption: "average" + + property var config: ({ + "general": { + "bars": count, + "framerate": 30, + "autosens": 1 + }, + "smoothing": { + "monstercat": 1, + "gravity": 1000000, + "noise_reduction": noiseReduction + }, + "output": { + "method": "raw", + "bit_format": 8, + "channels": channels, + "mono_option": monoOption + } + }) + + property var values: Array(count).fill(0) + + Process { + id: process + property int index: 0 + stdinEnabled: true + running: MediaPlayer.isPlaying + command: ["cava", "-p", "/dev/stdin"] + onExited: { + stdinEnabled = true + index = 0 + values = Array(count).fill(0) + } + onStarted: { + for (const k in config) { + if (typeof config[k] !== "object") { + write(k + "=" + config[k] + "\n") + continue + } + write("[" + k + "]\n") + const obj = config[k] + for (const k2 in obj) { + write(k2 + "=" + obj[k2] + "\n") + } + } + stdinEnabled = false + } + stdout: SplitParser { + splitMarker: "" + onRead: data => { + const newValues = Array(count).fill(0) + for (var i = 0; i < values.length; i++) { + newValues[i] = values[i] + } + if (process.index + data.length > count) { + process.index = 0 + } + for (var i = 0; i < data.length; i += 1) { + newValues[process.index] = Math.min(data.charCodeAt(i), 128) / 128 + process.index = (process.index + 1) % count + } + values = newValues + } + } + } +} diff --git a/Modules/Bar/Volume.qml b/Modules/Bar/Volume.qml index f00e09c..b65130a 100644 --- a/Modules/Bar/Volume.qml +++ b/Modules/Bar/Volume.qml @@ -6,125 +6,99 @@ import qs.Widgets Item { id: volumeDisplay - property var shell - property int volume: 0 - property bool firstChange: true width: pillIndicator.width height: pillIndicator.height - function getVolumeColor() { - if (volume <= 100) - return Colors.accentPrimary - // Calculate interpolation factor (0 at 100%, 1 at 200%) - var factor = (volume - 100) / 100 - // Blend between accent and warning colors - return Qt.rgba(Colors.accentPrimary.r + (Colors.warning.r - Colors.accentPrimary.r) * factor, - Colors.accentPrimary.g + (Colors.warning.g - Colors.accentPrimary.g) * factor, - Colors.accentPrimary.b + (Colors.warning.b - Colors.accentPrimary.b) * factor, 1) + function getIcon() { + if (PipeWireAudio.muted) { + return "volume_off" + } + return PipeWireAudio.volume === 0 ? "volume_off" : (PipeWireAudio.volume < 0.33 ? "volume_down" : "volume_up") } function getIconColor() { - if (volume <= 100) + if (PipeWireAudio.volume <= 1.0) { return Colors.textPrimary - return getVolumeColor() // Only use warning blend when >100% + } + + // Indicate that the volume is over 100% + // Calculate interpolation factor (0 at 100%, 1.0 at 200%) + let factor = (PipeWireAudio.volume - 1) + + // Blend between accent and warning colors + return Qt.rgba(Colors.textPrimary.r + (Colors.warning.r - Colors.textPrimary.r) * factor, + Colors.textPrimary.g + (Colors.warning.g - Colors.textPrimary.g) * factor, + Colors.textPrimary.b + (Colors.warning.b - Colors.textPrimary.b) * factor, 1) } NPill { id: pillIndicator - icon: shell && shell.defaultAudioSink && shell.defaultAudioSink.audio - && shell.defaultAudioSink.audio.muted ? "volume_off" : (volume === 0 ? "volume_off" : (volume < 30 ? "volume_down" : "volume_up")) - text: volume + "%" + icon: getIcon() + text: Math.round(PipeWireAudio.volume * 100) + "%" + tooltipText: "Volume: " + Math.round( + PipeWireAudio.volume * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume." + onClicked: function () { + console.log("onClicked") + //if (ioSelector.visible) { + // ioSelector.dismiss(); + // } else { + // ioSelector.show(); + // } + } - pillColor: Colors.surfaceVariant - iconCircleColor: getVolumeColor() - iconTextColor: Colors.backgroundPrimary - textColor: Colors.textPrimary - collapsedIconColor: getIconColor() - autoHide: true - - // StyledTooltip { - // id: volumeTooltip - // text: "Volume: " + volume + "%\nLeft click for advanced settings.\nScroll up/down to change volume." - // positionAbove: false - // tooltipVisible: !ioSelector.visible && volumeDisplay.containsMouse - // targetItem: pillIndicator - // delay: 1500 - // } - - // MouseArea { - // anchors.fill: parent - // hoverEnabled: true - // cursorShape: Qt.PointingHandCursor - // onClicked: { - // if (ioSelector.visible) { - // ioSelector.dismiss(); - // } else { - // ioSelector.show(); - // } - // } - // } + // pillColor: Colors.surfaceVariant + // iconCircleColor: Colors.// getVolumeColor() + // iconTextColor: Colors.backgroundPrimary + // textColor: Colors.textPrimary + // collapsedIconColor: getIconColor() + // autoHide: true } - Connections { - target: shell ?? null - function onVolumeChanged() { - if (shell) { - const clampedVolume = Math.max(0, Math.min(200, shell.volume)) - if (clampedVolume !== volume) { - volume = clampedVolume - pillIndicator.text = volume + "%" - pillIndicator.icon = shell.defaultAudioSink && shell.defaultAudioSink.audio - && shell.defaultAudioSink.audio.muted ? "volume_off" : (volume === 0 ? "volume_off" : (volume - < 30 ? "volume_down" : "volume_up")) - - if (firstChange) { - firstChange = false - } else { - pillIndicator.show() - } - } - } - } + AudioDeviceSelector { + id: ioSelector + // onPanelClosed: ioSelector.dismiss() } - Component.onCompleted: { - if (shell && shell.volume !== undefined) { - volume = Math.max(0, Math.min(200, shell.volume)) - } - } + // Connections { + // target: PipeWireAudio + // function onVolumeChanged() { + // console.log("onVolumeChanged") + // } - MouseArea { - anchors.fill: parent - hoverEnabled: true - acceptedButtons: Qt.NoButton - propagateComposedEvents: true - onEntered: { - volumeDisplay.containsMouse = true - pillIndicator.autoHide = false - pillIndicator.showDelayed() - } - onExited: { - volumeDisplay.containsMouse = false - pillIndicator.autoHide = true - pillIndicator.hide() - } - cursorShape: Qt.PointingHandCursor - onWheel: wheel => { - if (!shell) - return - let step = 5 - if (wheel.angleDelta.y > 0) { - shell.updateVolume(Math.min(200, shell.volume + step)) - } else if (wheel.angleDelta.y < 0) { - shell.updateVolume(Math.max(0, shell.volume - step)) - } - } - } + // function onSinkChanged() { + // console.log("onSinkChanged") + // } - // AudioDeviceSelector { - // id: ioSelector - // onPanelClosed: ioSelector.dismiss() + // } + + // MouseArea { + // anchors.fill: parent + // hoverEnabled: true + // acceptedButtons: Qt.NoButton + // propagateComposedEvents: true + // onEntered: { + // volumeDisplay.containsMouse = true + // pillIndicator.autoHide = false + // pillIndicator.showDelayed() + // } + // onExited: { + // volumeDisplay.containsMouse = false + // pillIndicator.autoHide = true + // pillIndicator.hide() + // } + // cursorShape: Qt.PointingHandCursor + // onWheel: wheel => { + // if (!shell) + // return + // let step = 5 + // if (wheel.angleDelta.y > 0) { + // shell.updateVolume(Math.min(200, shell.volume + step)) + // } else if (wheel.angleDelta.y < 0) { + // shell.updateVolume(Math.max(0, shell.volume - step)) + // } + // } // } - property bool containsMouse: false + + // property bool containsMouse: false } diff --git a/Services/MediaPlayer.qml b/Services/MediaPlayer.qml index 806142c..cae8464 100644 --- a/Services/MediaPlayer.qml +++ b/Services/MediaPlayer.qml @@ -1,161 +1,162 @@ pragma Singleton +import QtQuick import Quickshell import Quickshell.Services.Mpris import qs.Services +import qs.Modules.Audio Singleton { id: root - // property var currentPlayer: null - // property real currentPosition: 0 - // property int selectedPlayerIndex: 0 - // property bool isPlaying: currentPlayer ? currentPlayer.isPlaying : false - // property string trackTitle: currentPlayer ? (currentPlayer.trackTitle || "Unknown Track") : "" - // property string trackArtist: currentPlayer ? (currentPlayer.trackArtist || "Unknown Artist") : "" - // property string trackAlbum: currentPlayer ? (currentPlayer.trackAlbum || "Unknown Album") : "" - // property string trackArtUrl: currentPlayer ? (currentPlayer.trackArtUrl || "") : "" - // property real trackLength: currentPlayer ? currentPlayer.length : 0 - // property bool canPlay: currentPlayer ? currentPlayer.canPlay : false - // property bool canPause: currentPlayer ? currentPlayer.canPause : false - // property bool canGoNext: currentPlayer ? currentPlayer.canGoNext : false - // property bool canGoPrevious: currentPlayer ? currentPlayer.canGoPrevious : false - // property bool canSeek: currentPlayer ? currentPlayer.canSeek : false - // property bool hasPlayer: getAvailablePlayers().length > 0 + property var currentPlayer: null + property real currentPosition: 0 + property int selectedPlayerIndex: 0 + property bool isPlaying: currentPlayer ? currentPlayer.isPlaying : false + property string trackTitle: currentPlayer ? (currentPlayer.trackTitle || "Unknown Track") : "" + property string trackArtist: currentPlayer ? (currentPlayer.trackArtist || "Unknown Artist") : "" + property string trackAlbum: currentPlayer ? (currentPlayer.trackAlbum || "Unknown Album") : "" + property string trackArtUrl: currentPlayer ? (currentPlayer.trackArtUrl || "") : "" + property real trackLength: currentPlayer ? currentPlayer.length : 0 + property bool canPlay: currentPlayer ? currentPlayer.canPlay : false + property bool canPause: currentPlayer ? currentPlayer.canPause : false + property bool canGoNext: currentPlayer ? currentPlayer.canGoNext : false + property bool canGoPrevious: currentPlayer ? currentPlayer.canGoPrevious : false + property bool canSeek: currentPlayer ? currentPlayer.canSeek : false + property bool hasPlayer: getAvailablePlayers().length > 0 - // Item { - // Component.onCompleted: { - // updateCurrentPlayer() - // } - // } + // Expose cava values + property alias cavaValues: cava.values - // function getAvailablePlayers() { - // if (!Mpris.players || !Mpris.players.values) { - // return [] - // } + Item { + Component.onCompleted: { + updateCurrentPlayer() + } + } - // let allPlayers = Mpris.players.values - // let controllablePlayers = [] + function getAvailablePlayers() { + if (!Mpris.players || !Mpris.players.values) { + return [] + } - // for (var i = 0; i < allPlayers.length; i++) { - // let player = allPlayers[i] - // if (player && player.canControl) { - // controllablePlayers.push(player) - // } - // } + let allPlayers = Mpris.players.values + let controllablePlayers = [] - // return controllablePlayers - // } + for (var i = 0; i < allPlayers.length; i++) { + let player = allPlayers[i] + if (player && player.canControl) { + controllablePlayers.push(player) + } + } - // function findActivePlayer() { - // let availablePlayers = getAvailablePlayers() - // if (availablePlayers.length === 0) { - // return null - // } + return controllablePlayers + } - // if (selectedPlayerIndex < availablePlayers.length) { - // return availablePlayers[selectedPlayerIndex] - // } else { - // selectedPlayerIndex = 0 - // return availablePlayers[0] - // } - // } + function findActivePlayer() { + let availablePlayers = getAvailablePlayers() + if (availablePlayers.length === 0) { + return null + } - // // Switch to the most recently active player - // function updateCurrentPlayer() { - // let newPlayer = findActivePlayer() - // if (newPlayer !== currentPlayer) { - // currentPlayer = newPlayer - // currentPosition = currentPlayer ? currentPlayer.position : 0 - // } - // } + if (selectedPlayerIndex < availablePlayers.length) { + return availablePlayers[selectedPlayerIndex] + } else { + selectedPlayerIndex = 0 + return availablePlayers[0] + } + } - // function playPause() { - // if (currentPlayer) { - // if (currentPlayer.isPlaying) { - // currentPlayer.pause() - // } else { - // currentPlayer.play() - // } - // } - // } + // Switch to the most recently active player + function updateCurrentPlayer() { + let newPlayer = findActivePlayer() + if (newPlayer !== currentPlayer) { + currentPlayer = newPlayer + currentPosition = currentPlayer ? currentPlayer.position : 0 + } + } - // function play() { - // if (currentPlayer && currentPlayer.canPlay) { - // currentPlayer.play() - // } - // } + function playPause() { + if (currentPlayer) { + if (currentPlayer.isPlaying) { + currentPlayer.pause() + } else { + currentPlayer.play() + } + } + } - // function pause() { - // if (currentPlayer && currentPlayer.canPause) { - // currentPlayer.pause() - // } - // } + function play() { + if (currentPlayer && currentPlayer.canPlay) { + currentPlayer.play() + } + } - // function next() { - // if (currentPlayer && currentPlayer.canGoNext) { - // currentPlayer.next() - // } - // } + function pause() { + if (currentPlayer && currentPlayer.canPause) { + currentPlayer.pause() + } + } - // function previous() { - // if (currentPlayer && currentPlayer.canGoPrevious) { - // currentPlayer.previous() - // } - // } + function next() { + if (currentPlayer && currentPlayer.canGoNext) { + currentPlayer.next() + } + } - // function seek(position) { - // if (currentPlayer && currentPlayer.canSeek) { - // currentPlayer.position = position - // currentPosition = position - // } - // } + function previous() { + if (currentPlayer && currentPlayer.canGoPrevious) { + currentPlayer.previous() + } + } - // // Seek to position based on ratio (0.0 to 1.0) - // function seekByRatio(ratio) { - // if (currentPlayer && currentPlayer.canSeek && currentPlayer.length > 0) { - // let seekPosition = ratio * currentPlayer.length - // currentPlayer.position = seekPosition - // currentPosition = seekPosition - // } - // } + function seek(position) { + if (currentPlayer && currentPlayer.canSeek) { + currentPlayer.position = position + currentPosition = position + } + } - // // Update progress bar every second while playing - // Timer { - // id: positionTimer - // interval: 1000 - // running: currentPlayer && currentPlayer.isPlaying && currentPlayer.length > 0 - // && currentPlayer.playbackState === MprisPlaybackState.Playing - // repeat: true - // onTriggered: { - // if (currentPlayer && currentPlayer.isPlaying && currentPlayer.playbackState === MprisPlaybackState.Playing) { - // currentPosition = currentPlayer.position - // } else { - // running = false - // } - // } - // } + // Seek to position based on ratio (0.0 to 1.0) + function seekByRatio(ratio) { + if (currentPlayer && currentPlayer.canSeek && currentPlayer.length > 0) { + let seekPosition = ratio * currentPlayer.length + currentPlayer.position = seekPosition + currentPosition = seekPosition + } + } - // // Reset position when switching to inactive player - // onCurrentPlayerChanged: { - // if (!currentPlayer || !currentPlayer.isPlaying || currentPlayer.playbackState !== MprisPlaybackState.Playing) { - // currentPosition = 0 - // } - // } + // Update progress bar every second while playing + Timer { + id: positionTimer + interval: 1000 + running: currentPlayer && currentPlayer.isPlaying && currentPlayer.length > 0 + && currentPlayer.playbackState === MprisPlaybackState.Playing + repeat: true + onTriggered: { + if (currentPlayer && currentPlayer.isPlaying && currentPlayer.playbackState === MprisPlaybackState.Playing) { + currentPosition = currentPlayer.position + } else { + running = false + } + } + } - // // Update current player when available players change - // Connections { - // target: Mpris.players - // function onValuesChanged() { - // updateCurrentPlayer() - // } - // } + // Reset position when switching to inactive player + onCurrentPlayerChanged: { + if (!currentPlayer || !currentPlayer.isPlaying || currentPlayer.playbackState !== MprisPlaybackState.Playing) { + currentPosition = 0 + } + } - // Cava { - // id: cava - // count: 44 - // } + // Update current player when available players change + Connections { + target: Mpris.players + function onValuesChanged() { + updateCurrentPlayer() + } + } - // // Expose cava values - // property alias cavaValues: cava.values + Cava { + id: cava + } } diff --git a/Services/PipeWireAudio.qml b/Services/PipeWireAudio.qml new file mode 100644 index 0000000..8ef7104 --- /dev/null +++ b/Services/PipeWireAudio.qml @@ -0,0 +1,30 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Services.Pipewire + +Singleton { + id: root + + // Ensure the volume is readonly from outside + readonly property alias volume: root._volume + property real _volume: 0 + + readonly property alias muted: root._muted + property bool _muted: false + + PwObjectTracker { + objects: [Pipewire.defaultAudioSink] + } + + Connections { + target: Pipewire.defaultAudioSink?.audio ? Pipewire.defaultAudioSink?.audio : null + + function onVolumeChanged() { + root._volume = (Pipewire.defaultAudioSink?.audio.volume ?? 0) + console.log("onVolumeChanged: " + volume) + } + } +} diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml index 21b3644..175e5e9 100644 --- a/Widgets/NIconButton.qml +++ b/Widgets/NIconButton.qml @@ -64,10 +64,10 @@ Rectangle { root.onExited() } onClicked: { - root.onClicked() if (tooltipText) { tooltip.hide() } + root.onClicked() } } } diff --git a/Widgets/NPill.qml b/Widgets/NPill.qml index 892489c..9e98abc 100644 --- a/Widgets/NPill.qml +++ b/Widgets/NPill.qml @@ -18,6 +18,10 @@ Item { property real sizeMultiplier: 0.8 property bool autoHide: false + property var onEntered: function () {} + property var onExited: function () {} + property var onClicked: function () {} + // Internal state property bool showPill: false property bool shouldAnimateHide: false @@ -190,10 +194,15 @@ Item { onEntered: { showDelayed() tooltip.show() + root.onEntered() } onExited: { hide() tooltip.hide() + root.onExited() + } + onClicked: { + root.onClicked() } } diff --git a/shell.qml b/shell.qml index 66ae026..a1544a5 100644 --- a/shell.qml +++ b/shell.qml @@ -4,6 +4,7 @@ import QtQuick import Quickshell import Quickshell.Io import Quickshell.Widgets +import Quickshell.Services.Pipewire import qs.Widgets import qs.Modules.Bar import qs.Modules.DemoPanel @@ -13,7 +14,7 @@ import qs.Modules.Notification import qs.Services ShellRoot { - id: root + id: shellRoot Background {} Overview {} From 5e4530a403a5405c2e311a930a9335a417e3b84b Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 18:48:04 -0400 Subject: [PATCH 076/394] Audio WIP --- Modules/Bar/Volume.qml | 45 +++++++++++++---------- Services/{PipeWireAudio.qml => Audio.qml} | 7 +++- 2 files changed, 32 insertions(+), 20 deletions(-) rename Services/{PipeWireAudio.qml => Audio.qml} (74%) diff --git a/Modules/Bar/Volume.qml b/Modules/Bar/Volume.qml index b65130a..c68e7a6 100644 --- a/Modules/Bar/Volume.qml +++ b/Modules/Bar/Volume.qml @@ -1,5 +1,6 @@ import QtQuick import Quickshell +import Quickshell.Services.Pipewire import qs.Services import qs.Modules.Audio import qs.Widgets @@ -11,33 +12,40 @@ Item { height: pillIndicator.height function getIcon() { - if (PipeWireAudio.muted) { + if (Audio.muted) { return "volume_off" } - return PipeWireAudio.volume === 0 ? "volume_off" : (PipeWireAudio.volume < 0.33 ? "volume_down" : "volume_up") + return Audio.volume === 0 ? "volume_off" : (Audio.volume < 0.33 ? "volume_down" : "volume_up") } function getIconColor() { - if (PipeWireAudio.volume <= 1.0) { - return Colors.textPrimary + return (Audio.volume <= 1.0) ? Colors.textPrimary : getVolumeColor(); + } + + function getVolumeColor() { + if (Audio.volume <= 1.0) { + return Colors.accentPrimary } // Indicate that the volume is over 100% // Calculate interpolation factor (0 at 100%, 1.0 at 200%) - let factor = (PipeWireAudio.volume - 1) + let factor = (Audio.volume - 1.0) // Blend between accent and warning colors - return Qt.rgba(Colors.textPrimary.r + (Colors.warning.r - Colors.textPrimary.r) * factor, - Colors.textPrimary.g + (Colors.warning.g - Colors.textPrimary.g) * factor, - Colors.textPrimary.b + (Colors.warning.b - Colors.textPrimary.b) * factor, 1) + return Qt.rgba(Colors.accentPrimary.r + (Colors.error.r - Colors.accentPrimary.r) * factor, + Colors.accentPrimary.g + (Colors.error.g - Colors.accentPrimary.g) * factor, + Colors.accentPrimary.b + (Colors.error.b - Colors.accentPrimary.b) * factor, 1) } NPill { id: pillIndicator icon: getIcon() - text: Math.round(PipeWireAudio.volume * 100) + "%" + iconCircleColor: getVolumeColor() + collapsedIconColor: getIconColor() + autoHide: true + text: Math.round(Audio.volume * 100) + "%" tooltipText: "Volume: " + Math.round( - PipeWireAudio.volume * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume." + Audio.volume * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume." onClicked: function () { console.log("onClicked") //if (ioSelector.visible) { @@ -46,13 +54,14 @@ Item { // ioSelector.show(); // } } + } - // pillColor: Colors.surfaceVariant - // iconCircleColor: Colors.// getVolumeColor() - // iconTextColor: Colors.backgroundPrimary - // textColor: Colors.textPrimary - // collapsedIconColor: getIconColor() - // autoHide: true + Connections { + target: Pipewire.defaultAudioSink?.audio ? Pipewire.defaultAudioSink?.audio : null + + function onVolumeChanged() { + console.log("[Bar:Volume] onVolumeChanged") + } } AudioDeviceSelector { @@ -61,7 +70,7 @@ Item { } // Connections { - // target: PipeWireAudio + // target: Audio // function onVolumeChanged() { // console.log("onVolumeChanged") // } @@ -99,6 +108,4 @@ Item { // } // } // } - - // property bool containsMouse: false } diff --git a/Services/PipeWireAudio.qml b/Services/Audio.qml similarity index 74% rename from Services/PipeWireAudio.qml rename to Services/Audio.qml index 8ef7104..1420a46 100644 --- a/Services/PipeWireAudio.qml +++ b/Services/Audio.qml @@ -24,7 +24,12 @@ Singleton { function onVolumeChanged() { root._volume = (Pipewire.defaultAudioSink?.audio.volume ?? 0) - console.log("onVolumeChanged: " + volume) + console.log("[Audio] onVolumeChanged: " + volume) + } + + function onMutedChanged() { + root._muted = (Pipewire.defaultAudioSink?.audio.muted ?? true) + console.log("[Audio] onMuteChanged " + muted) } } } From 4a22910897430bc6ee627f1cca6846deeae2c4c0 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 18:48:15 -0400 Subject: [PATCH 077/394] NTooltip: bigger vertical margin --- Widgets/NTooltip.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Widgets/NTooltip.qml b/Widgets/NTooltip.qml index 076d1cf..8a709c7 100644 --- a/Widgets/NTooltip.qml +++ b/Widgets/NTooltip.qml @@ -38,7 +38,7 @@ Window { function _showNow() { // Compute new size everytime we show the tooltip width = Math.max(50 * scaling, tooltipText.implicitWidth + Style.marginLarge * 2 * scaling) - height = Math.max(50 * scaling, tooltipText.implicitHeight + Style.marginSmall * 2 * scaling) + height = Math.max(50 * scaling, tooltipText.implicitHeight + Style.marginMedium * 2 * scaling) if (!target) { return From c05365c94961a2a1451670b0a33e66bae5412087 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 19:12:06 -0400 Subject: [PATCH 078/394] NPill: added support for mouse wheel up/down --- Widgets/NPill.qml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Widgets/NPill.qml b/Widgets/NPill.qml index 9e98abc..9a040f2 100644 --- a/Widgets/NPill.qml +++ b/Widgets/NPill.qml @@ -21,6 +21,7 @@ Item { property var onEntered: function () {} property var onExited: function () {} property var onClicked: function () {} + property var onWheel: function (delta) {} // Internal state property bool showPill: false @@ -204,6 +205,9 @@ Item { onClicked: { root.onClicked() } + onWheel: wheel => { + root.onWheel(wheel.angleDelta.y) + } } function show() { From 42073b289e5b4ff415e4d4b4e8104d795ccbbf28 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 19:12:44 -0400 Subject: [PATCH 079/394] Bar-Volume: everything working just missing the AudioDeviceSelector --- Modules/Bar/Volume.qml | 82 +++++++++++++++--------------------------- Services/Audio.qml | 33 +++++++++++++---- 2 files changed, 56 insertions(+), 59 deletions(-) diff --git a/Modules/Bar/Volume.qml b/Modules/Bar/Volume.qml index c68e7a6..996ec03 100644 --- a/Modules/Bar/Volume.qml +++ b/Modules/Bar/Volume.qml @@ -6,10 +6,13 @@ import qs.Modules.Audio import qs.Widgets Item { - id: volumeDisplay + id: root - width: pillIndicator.width - height: pillIndicator.height + width: pill.width + height: pill.height + + // Used to avoid opening the pill on Quickshell startup + property bool firstVolumeReceived: false function getIcon() { if (Audio.muted) { @@ -19,7 +22,7 @@ Item { } function getIconColor() { - return (Audio.volume <= 1.0) ? Colors.textPrimary : getVolumeColor(); + return (Audio.volume <= 1.0) ? Colors.textPrimary : getVolumeColor() } function getVolumeColor() { @@ -38,7 +41,7 @@ Item { } NPill { - id: pillIndicator + id: pill icon: getIcon() iconCircleColor: getVolumeColor() collapsedIconColor: getIconColor() @@ -48,19 +51,32 @@ Item { Audio.volume * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume." onClicked: function () { console.log("onClicked") - //if (ioSelector.visible) { - // ioSelector.dismiss(); - // } else { - // ioSelector.show(); - // } + // if (ioSelector.visible) { + // ioSelector.dismiss() + // } else { + // ioSelector.show() + // } + } + onWheel: function (angle) { + if (angle > 0) { + Audio.volumeIncrement() + } else if (angle < 0) { + Audio.volumeDecrement() + } } } + // Connection used to open the pill when volume changes Connections { - target: Pipewire.defaultAudioSink?.audio ? Pipewire.defaultAudioSink?.audio : null - + target: Audio.sink?.audio ? Audio.sink?.audio : null function onVolumeChanged() { - console.log("[Bar:Volume] onVolumeChanged") + // console.log("[Bar:Volume] onVolumeChanged") + if (!firstVolumeReceived) { + // Ignore the first volume change + firstVolumeReceived = true + } else { + pill.show() + } } } @@ -68,44 +84,4 @@ Item { id: ioSelector // onPanelClosed: ioSelector.dismiss() } - - // Connections { - // target: Audio - // function onVolumeChanged() { - // console.log("onVolumeChanged") - // } - - // function onSinkChanged() { - // console.log("onSinkChanged") - // } - - // } - - // MouseArea { - // anchors.fill: parent - // hoverEnabled: true - // acceptedButtons: Qt.NoButton - // propagateComposedEvents: true - // onEntered: { - // volumeDisplay.containsMouse = true - // pillIndicator.autoHide = false - // pillIndicator.showDelayed() - // } - // onExited: { - // volumeDisplay.containsMouse = false - // pillIndicator.autoHide = true - // pillIndicator.hide() - // } - // cursorShape: Qt.PointingHandCursor - // onWheel: wheel => { - // if (!shell) - // return - // let step = 5 - // if (wheel.angleDelta.y > 0) { - // shell.updateVolume(Math.min(200, shell.volume + step)) - // } else if (wheel.angleDelta.y < 0) { - // shell.updateVolume(Math.max(0, shell.volume - step)) - // } - // } - // } } diff --git a/Services/Audio.qml b/Services/Audio.qml index 1420a46..3c8bf02 100644 --- a/Services/Audio.qml +++ b/Services/Audio.qml @@ -8,27 +8,48 @@ import Quickshell.Services.Pipewire Singleton { id: root - // Ensure the volume is readonly from outside + readonly property PwNode sink: Pipewire.defaultAudioSink + readonly property PwNode source: Pipewire.defaultAudioSource + + // Volume [0..1] is readonly from outside readonly property alias volume: root._volume - property real _volume: 0 + property real _volume: sink?.audio?.volume ?? 0 readonly property alias muted: root._muted - property bool _muted: false + property bool _muted: !!sink?.audio?.muted + + readonly property real step: 0.05 + + function volumeIncrement() { + volumeSet(volume + step) + } + + function volumeDecrement() { + volumeSet(volume - step) + } + + function volumeSet(newVolume) { + // Clamp volume to 200% + if (sink?.ready && sink?.audio) { + sink.audio.muted = false + sink.audio.volume = Math.max(0, Math.min(2, newVolume)) + } + } PwObjectTracker { objects: [Pipewire.defaultAudioSink] } Connections { - target: Pipewire.defaultAudioSink?.audio ? Pipewire.defaultAudioSink?.audio : null + target: sink?.audio ? sink?.audio : null function onVolumeChanged() { - root._volume = (Pipewire.defaultAudioSink?.audio.volume ?? 0) + root._volume = (sink?.audio.volume ?? 0) console.log("[Audio] onVolumeChanged: " + volume) } function onMutedChanged() { - root._muted = (Pipewire.defaultAudioSink?.audio.muted ?? true) + root._muted = (sink?.audio.muted ?? true) console.log("[Audio] onMuteChanged " + muted) } } From 620afc7d0341fe0b0c919ae8ceb421eb4b6d5ea7 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 19:21:48 -0400 Subject: [PATCH 080/394] NLoader: renamed property panel => content so it looks more versatile --- Modules/Background/ScreenCorners.qml | 2 +- Modules/DemoPanel/DemoPanel.qml | 2 +- Modules/SidePanel/SidePanel.qml | 2 +- Widgets/NLoader.qml | 13 ++++++++----- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Modules/Background/ScreenCorners.qml b/Modules/Background/ScreenCorners.qml index 1557f19..be93b46 100644 --- a/Modules/Background/ScreenCorners.qml +++ b/Modules/Background/ScreenCorners.qml @@ -8,7 +8,7 @@ import qs.Widgets NLoader { isLoaded: Settings.data.general.showScreenCorners - panel: Variants { + content: Variants { model: Quickshell.screens PanelWindow { diff --git a/Modules/DemoPanel/DemoPanel.qml b/Modules/DemoPanel/DemoPanel.qml index 167e849..97c51d7 100644 --- a/Modules/DemoPanel/DemoPanel.qml +++ b/Modules/DemoPanel/DemoPanel.qml @@ -13,7 +13,7 @@ import qs.Widgets NLoader { id: root - panel: Component { + content: Component { NPanel { id: demoPanel diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml index 9d32486..de1a5c1 100644 --- a/Modules/SidePanel/SidePanel.qml +++ b/Modules/SidePanel/SidePanel.qml @@ -29,7 +29,7 @@ NLoader { } } - panel: Component { + content: Component { NPanel { id: sidePanel diff --git a/Widgets/NLoader.qml b/Widgets/NLoader.qml index b0e5323..2dfd104 100644 --- a/Widgets/NLoader.qml +++ b/Widgets/NLoader.qml @@ -1,19 +1,22 @@ import QtQuick +// Example usage: +// NLoader { +// content: Component { +// NPanel { + Loader { id: loader // Boolean control to load/unload the item property bool isLoaded: false - // Provide the component to load. - // Example usage: - // content: Component { NPanel { /* ... */ } } - property Component panel + // Provide the component to be loaded. + property Component content active: isLoaded asynchronous: true - sourceComponent: panel + sourceComponent: content onActiveChanged: { if (active && item && item.show) From 8d70b3d737b45cfdba40d6f8b7ed3cc4b20decbe Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 19:25:22 -0400 Subject: [PATCH 081/394] AudioDeviceSelector: In a loader but fake content --- Modules/Audio/AudioDeviceSelector.qml | 838 +++++++++++++++----------- Modules/Bar/Volume.qml | 49 +- Services/Audio.qml | 2 +- Widgets/NLoader.qml | 1 - 4 files changed, 500 insertions(+), 390 deletions(-) diff --git a/Modules/Audio/AudioDeviceSelector.qml b/Modules/Audio/AudioDeviceSelector.qml index 7cd5de6..109d88d 100644 --- a/Modules/Audio/AudioDeviceSelector.qml +++ b/Modules/Audio/AudioDeviceSelector.qml @@ -1,369 +1,485 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Quickshell import Quickshell.Services.Pipewire import qs.Services import qs.Widgets -NPanel { - id: ioSelector - - // property int tabIndex: 0 - // property Item anchorItem: null - - // signal panelClosed() - - // function sinkNodes() { - // let nodes = Pipewire.nodes && Pipewire.nodes.values ? Pipewire.nodes.values.filter(function(n) { - // return n.isSink && n.audio && n.isStream === false; - // }) : []; - // if (Pipewire.defaultAudioSink) - // nodes = nodes.slice().sort(function(a, b) { - // if (a.id === Pipewire.defaultAudioSink.id) - // return -1; - - // if (b.id === Pipewire.defaultAudioSink.id) - // return 1; - - // return 0; - // }); - - // return nodes; - // } - - // function sourceNodes() { - // let nodes = Pipewire.nodes && Pipewire.nodes.values ? Pipewire.nodes.values.filter(function(n) { - // return !n.isSink && n.audio && n.isStream === false; - // }) : []; - // if (Pipewire.defaultAudioSource) - // nodes = nodes.slice().sort(function(a, b) { - // if (a.id === Pipewire.defaultAudioSource.id) - // return -1; - - // if (b.id === Pipewire.defaultAudioSource.id) - // return 1; - - // return 0; - // }); - - // return nodes; - // } - - // Component.onCompleted: { - // if (Pipewire.nodes && Pipewire.nodes.values) { - // for (var i = 0; i < Pipewire.nodes.values.length; ++i) { - // var n = Pipewire.nodes.values[i]; - // } - // } - // } - // Component.onDestruction: { - // } - // onVisibleChanged: { - // if (!visible) - // panelClosed(); - - // } - - // // Bind all Pipewire nodes so their properties are valid - // PwObjectTracker { - // id: nodeTracker - - // objects: Pipewire.nodes - // } - - // Rectangle { - // color: Theme.backgroundPrimary - // radius: 20 - // width: 340 - // height: 340 - // anchors.top: parent.top - // anchors.right: parent.right - // anchors.topMargin: 4 - // anchors.rightMargin: 4 - - // // Prevent closing when clicking in the panel bg - // MouseArea { - // anchors.fill: parent - // } - - // ColumnLayout { - // anchors.fill: parent - // anchors.margins: 16 - // spacing: 10 - - // // Tabs centered inside the window - // RowLayout { - // Layout.fillWidth: true - // Layout.alignment: Qt.AlignHCenter - // spacing: 0 - - // Tabs { - // id: ioTabs - - // tabsModel: [{ - // "label": "Output", - // "icon": "volume_up" - // }, { - // "label": "Input", - // "icon": "mic" - // }] - // currentIndex: tabIndex - // onTabChanged: { - // tabIndex = currentIndex; - // } - // } - - // } - - // // Add vertical space between tabs and entries - // Item { - // height: 36 - // Layout.fillWidth: true - // } - - // // Output Devices - // Flickable { - // id: sinkList - - // visible: tabIndex === 0 - // contentHeight: sinkColumn.height - // clip: true - // interactive: contentHeight > height - // width: parent.width - // height: 220 - - // ColumnLayout { - // id: sinkColumn - - // width: sinkList.width - // spacing: 6 - - // Repeater { - // model: ioSelector.sinkNodes() - - // Rectangle { - // width: parent.width - // height: 36 - // color: "transparent" - // radius: 6 - - // RowLayout { - // anchors.fill: parent - // anchors.margins: 6 - // spacing: 8 - - // Text { - // text: "volume_up" - // font.family: "Material Symbols Outlined" - // font.pixelSize: 16 * Theme.scale(screen) - // color: (Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary - // Layout.alignment: Qt.AlignVCenter - // } - - // ColumnLayout { - // Layout.fillWidth: true - // spacing: 1 - // Layout.maximumWidth: sinkList.width - 120 // Reserve space for the Set button - - // Text { - // text: modelData.nickname || modelData.description || modelData.name - // font.bold: true - // font.pixelSize: 12 * Theme.scale(screen) - // color: (Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary - // elide: Text.ElideRight - // maximumLineCount: 1 - // Layout.fillWidth: true - // } - - // Text { - // text: modelData.description !== modelData.nickname ? modelData.description : "" - // font.pixelSize: 10 * Theme.scale(screen) - // color: Theme.textSecondary - // elide: Text.ElideRight - // maximumLineCount: 1 - // Layout.fillWidth: true - // } - - // } - - // Rectangle { - // visible: Pipewire.preferredDefaultAudioSink !== modelData - // width: 60 - // height: 20 - // radius: 4 - // color: Theme.accentPrimary - // border.color: Theme.accentPrimary - // border.width: 1 - // Layout.alignment: Qt.AlignVCenter - - // Text { - // anchors.centerIn: parent - // text: "Set" - // color: Theme.onAccent - // font.pixelSize: 10 * Theme.scale(screen) - // font.bold: true - // } - - // MouseArea { - // anchors.fill: parent - // cursorShape: Qt.PointingHandCursor - // onClicked: Pipewire.preferredDefaultAudioSink = modelData - // } - - // } - - // Text { - // text: "(Current)" - // visible: Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id - // color: Theme.accentPrimary - // font.pixelSize: 10 * Theme.scale(screen) - // Layout.alignment: Qt.AlignVCenter - // } - - // } - - // } - - // } - - // } - - // ScrollBar.vertical: ScrollBar { - // } - - // } - - // // Input Devices - // Flickable { - // id: sourceList - - // visible: tabIndex === 1 - // contentHeight: sourceColumn.height - // clip: true - // interactive: contentHeight > height - // width: parent.width - // height: 220 - - // ColumnLayout { - // id: sourceColumn - - // width: sourceList.width - // spacing: 6 - - // Repeater { - // model: ioSelector.sourceNodes() - - // Rectangle { - // width: parent.width - // height: 36 - // color: "transparent" - // radius: 6 - - // RowLayout { - // anchors.fill: parent - // anchors.margins: 6 - // spacing: 8 - - // Text { - // text: "mic" - // font.family: "Material Symbols Outlined" - // font.pixelSize: 16 * Theme.scale(screen) - // color: (Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary - // Layout.alignment: Qt.AlignVCenter - // } - - // ColumnLayout { - // Layout.fillWidth: true - // spacing: 1 - // Layout.maximumWidth: sourceList.width - 120 // Reserve space for the Set button - - // Text { - // text: modelData.nickname || modelData.description || modelData.name - // font.bold: true - // font.pixelSize: 12 * Theme.scale(screen) - // color: (Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary - // elide: Text.ElideRight - // maximumLineCount: 1 - // Layout.fillWidth: true - // } - - // Text { - // text: modelData.description !== modelData.nickname ? modelData.description : "" - // font.pixelSize: 10 * Theme.scale(screen) - // color: Theme.textSecondary - // elide: Text.ElideRight - // maximumLineCount: 1 - // Layout.fillWidth: true - // } - - // } - - // Rectangle { - // visible: Pipewire.preferredDefaultAudioSource !== modelData - // width: 60 - // height: 20 - // radius: 4 - // color: Theme.accentPrimary - // border.color: Theme.accentPrimary - // border.width: 1 - // Layout.alignment: Qt.AlignVCenter - - // Text { - // anchors.centerIn: parent - // text: "Set" - // color: Theme.onAccent - // font.pixelSize: 10 * Theme.scale(screen) - // font.bold: true - // } - - // MouseArea { - // anchors.fill: parent - // cursorShape: Qt.PointingHandCursor - // onClicked: Pipewire.preferredDefaultAudioSource = modelData - // } - - // } - - // Text { - // text: "(Current)" - // visible: Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id - // color: Theme.accentPrimary - // font.pixelSize: 10 * Theme.scale(screen) - // Layout.alignment: Qt.AlignVCenter - // } - - // } - - // } - - // } - - // } - - // ScrollBar.vertical: ScrollBar { - // } - - // } - - // } - - // } - - // Connections { - // function onReadyChanged() { - // if (Pipewire.ready && Pipewire.nodes && Pipewire.nodes.values) { - // for (var i = 0; i < Pipewire.nodes.values.length; ++i) { - // var n = Pipewire.nodes.values[i]; - // } - // } - // } - - // function onDefaultAudioSinkChanged() { - // } - - // function onDefaultAudioSourceChanged() { - // } - - // target: Pipewire - // } +NLoader { + id: root + + content: Component { + NPanel { + id: demoPanel + + readonly property real scaling: Scaling.scale(screen) + + // Ensure panel shows itself once created + Component.onCompleted: show() + + Rectangle { + color: Colors.backgroundPrimary + radius: Style.radiusMedium * scaling + border.color: Colors.backgroundTertiary + border.width: Math.min(1, Style.borderMedium * scaling) + width: 500 * scaling + height: 400 + anchors.centerIn: parent + + // Prevent closing when clicking in the panel bg + MouseArea { + anchors.fill: parent + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginXL * scaling + spacing: Style.marginSmall * scaling + + // NIconButton + ColumnLayout { + spacing: 16 * scaling + NText { + text: "NIconButton" + color: Colors.accentSecondary + } + + NIconButton { + id: myIconButton + icon: "refresh" + } + + NDivider { + Layout.fillWidth: true + } + } + + // NToggle + ColumnLayout { + spacing: Style.marginLarge * scaling + NText { + text: "NToggle" + color: Colors.accentSecondary + } + + NToggle { + label: "Label" + description: "Description" + onToggled: function (value) { + console.log("NToggle: " + value) + } + } + + NDivider { + Layout.fillWidth: true + } + } + + // NSlider + ColumnLayout { + spacing: 16 * scaling + NText { + text: "Scaling" + color: Colors.accentSecondary + } + RowLayout { + spacing: Style.marginSmall * scaling + NText { + text: `${Math.round(Scaling.overrideScale * 100)}%` + Layout.alignment: Qt.AlignVCenter + } + NSlider { + id: scaleSlider + from: 0.6 + to: 1.8 + stepSize: 0.01 + value: Scaling.overrideScale + onMoved: function () { + Scaling.overrideScale = value + } + onPressedChanged: function () { + Scaling.overrideEnabled = true + } + } + NIconButton { + icon: "restart_alt" + sizeMultiplier: 0.7 + onClicked: function () { + Scaling.overrideEnabled = false + Scaling.overrideScale = 1.0 + } + } + } + NDivider { + Layout.fillWidth: true + } + } + } + } + } + } } +// NPanel { +// id: ioSelector + +// property int tabIndex: 0 +// property Item anchorItem: null + +// signal panelClosed() + +// function sinkNodes() { +// let nodes = Pipewire.nodes && Pipewire.nodes.values ? Pipewire.nodes.values.filter(function(n) { +// return n.isSink && n.audio && n.isStream === false; +// }) : []; +// if (Pipewire.defaultAudioSink) +// nodes = nodes.slice().sort(function(a, b) { +// if (a.id === Pipewire.defaultAudioSink.id) +// return -1; + +// if (b.id === Pipewire.defaultAudioSink.id) +// return 1; + +// return 0; +// }); + +// return nodes; +// } + +// function sourceNodes() { +// let nodes = Pipewire.nodes && Pipewire.nodes.values ? Pipewire.nodes.values.filter(function(n) { +// return !n.isSink && n.audio && n.isStream === false; +// }) : []; +// if (Pipewire.defaultAudioSource) +// nodes = nodes.slice().sort(function(a, b) { +// if (a.id === Pipewire.defaultAudioSource.id) +// return -1; + +// if (b.id === Pipewire.defaultAudioSource.id) +// return 1; + +// return 0; +// }); + +// return nodes; +// } + +// Component.onCompleted: { +// if (Pipewire.nodes && Pipewire.nodes.values) { +// for (var i = 0; i < Pipewire.nodes.values.length; ++i) { +// var n = Pipewire.nodes.values[i]; +// } +// } +// } +// Component.onDestruction: { +// } +// onVisibleChanged: { +// if (!visible) +// panelClosed(); + +// } + +// // Bind all Pipewire nodes so their properties are valid +// PwObjectTracker { +// id: nodeTracker + +// objects: Pipewire.nodes +// } + +// Rectangle { +// color: Theme.backgroundPrimary +// radius: 20 +// width: 340 +// height: 340 +// anchors.top: parent.top +// anchors.right: parent.right +// anchors.topMargin: 4 +// anchors.rightMargin: 4 + +// // Prevent closing when clicking in the panel bg +// MouseArea { +// anchors.fill: parent +// } + +// ColumnLayout { +// anchors.fill: parent +// anchors.margins: 16 +// spacing: 10 + +// // Tabs centered inside the window +// RowLayout { +// Layout.fillWidth: true +// Layout.alignment: Qt.AlignHCenter +// spacing: 0 + +// Tabs { +// id: ioTabs + +// tabsModel: [{ +// "label": "Output", +// "icon": "volume_up" +// }, { +// "label": "Input", +// "icon": "mic" +// }] +// currentIndex: tabIndex +// onTabChanged: { +// tabIndex = currentIndex; +// } +// } + +// } + +// // Add vertical space between tabs and entries +// Item { +// height: 36 +// Layout.fillWidth: true +// } + +// // Output Devices +// Flickable { +// id: sinkList + +// visible: tabIndex === 0 +// contentHeight: sinkColumn.height +// clip: true +// interactive: contentHeight > height +// width: parent.width +// height: 220 + +// ColumnLayout { +// id: sinkColumn + +// width: sinkList.width +// spacing: 6 + +// Repeater { +// model: ioSelector.sinkNodes() + +// Rectangle { +// width: parent.width +// height: 36 +// color: "transparent" +// radius: 6 + +// RowLayout { +// anchors.fill: parent +// anchors.margins: 6 +// spacing: 8 + +// Text { +// text: "volume_up" +// font.family: "Material Symbols Outlined" +// font.pixelSize: 16 * Theme.scale(screen) +// color: (Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary +// Layout.alignment: Qt.AlignVCenter +// } + +// ColumnLayout { +// Layout.fillWidth: true +// spacing: 1 +// Layout.maximumWidth: sinkList.width - 120 // Reserve space for the Set button + +// Text { +// text: modelData.nickname || modelData.description || modelData.name +// font.bold: true +// font.pixelSize: 12 * Theme.scale(screen) +// color: (Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary +// elide: Text.ElideRight +// maximumLineCount: 1 +// Layout.fillWidth: true +// } + +// Text { +// text: modelData.description !== modelData.nickname ? modelData.description : "" +// font.pixelSize: 10 * Theme.scale(screen) +// color: Theme.textSecondary +// elide: Text.ElideRight +// maximumLineCount: 1 +// Layout.fillWidth: true +// } + +// } + +// Rectangle { +// visible: Pipewire.preferredDefaultAudioSink !== modelData +// width: 60 +// height: 20 +// radius: 4 +// color: Theme.accentPrimary +// border.color: Theme.accentPrimary +// border.width: 1 +// Layout.alignment: Qt.AlignVCenter + +// Text { +// anchors.centerIn: parent +// text: "Set" +// color: Theme.onAccent +// font.pixelSize: 10 * Theme.scale(screen) +// font.bold: true +// } + +// MouseArea { +// anchors.fill: parent +// cursorShape: Qt.PointingHandCursor +// onClicked: Pipewire.preferredDefaultAudioSink = modelData +// } + +// } + +// Text { +// text: "(Current)" +// visible: Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id +// color: Theme.accentPrimary +// font.pixelSize: 10 * Theme.scale(screen) +// Layout.alignment: Qt.AlignVCenter +// } + +// } + +// } + +// } + +// } + +// ScrollBar.vertical: ScrollBar { +// } + +// } + +// // Input Devices +// Flickable { +// id: sourceList + +// visible: tabIndex === 1 +// contentHeight: sourceColumn.height +// clip: true +// interactive: contentHeight > height +// width: parent.width +// height: 220 + +// ColumnLayout { +// id: sourceColumn + +// width: sourceList.width +// spacing: 6 + +// Repeater { +// model: ioSelector.sourceNodes() + +// Rectangle { +// width: parent.width +// height: 36 +// color: "transparent" +// radius: 6 + +// RowLayout { +// anchors.fill: parent +// anchors.margins: 6 +// spacing: 8 + +// Text { +// text: "mic" +// font.family: "Material Symbols Outlined" +// font.pixelSize: 16 * Theme.scale(screen) +// color: (Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary +// Layout.alignment: Qt.AlignVCenter +// } + +// ColumnLayout { +// Layout.fillWidth: true +// spacing: 1 +// Layout.maximumWidth: sourceList.width - 120 // Reserve space for the Set button + +// Text { +// text: modelData.nickname || modelData.description || modelData.name +// font.bold: true +// font.pixelSize: 12 * Theme.scale(screen) +// color: (Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary +// elide: Text.ElideRight +// maximumLineCount: 1 +// Layout.fillWidth: true +// } + +// Text { +// text: modelData.description !== modelData.nickname ? modelData.description : "" +// font.pixelSize: 10 * Theme.scale(screen) +// color: Theme.textSecondary +// elide: Text.ElideRight +// maximumLineCount: 1 +// Layout.fillWidth: true +// } + +// } + +// Rectangle { +// visible: Pipewire.preferredDefaultAudioSource !== modelData +// width: 60 +// height: 20 +// radius: 4 +// color: Theme.accentPrimary +// border.color: Theme.accentPrimary +// border.width: 1 +// Layout.alignment: Qt.AlignVCenter + +// Text { +// anchors.centerIn: parent +// text: "Set" +// color: Theme.onAccent +// font.pixelSize: 10 * Theme.scale(screen) +// font.bold: true +// } + +// MouseArea { +// anchors.fill: parent +// cursorShape: Qt.PointingHandCursor +// onClicked: Pipewire.preferredDefaultAudioSource = modelData +// } + +// } + +// Text { +// text: "(Current)" +// visible: Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id +// color: Theme.accentPrimary +// font.pixelSize: 10 * Theme.scale(screen) +// Layout.alignment: Qt.AlignVCenter +// } + +// } + +// } + +// } + +// } + +// ScrollBar.vertical: ScrollBar { +// } + +// } + +// } + +// } + +// Connections { +// function onReadyChanged() { +// if (Pipewire.ready && Pipewire.nodes && Pipewire.nodes.values) { +// for (var i = 0; i < Pipewire.nodes.values.length; ++i) { +// var n = Pipewire.nodes.values[i]; +// } +// } +// } + +// function onDefaultAudioSinkChanged() { +// } + +// function onDefaultAudioSourceChanged() { +// } + +// target: Pipewire +// } +// } + diff --git a/Modules/Bar/Volume.qml b/Modules/Bar/Volume.qml index 996ec03..d1f0992 100644 --- a/Modules/Bar/Volume.qml +++ b/Modules/Bar/Volume.qml @@ -40,32 +40,6 @@ Item { Colors.accentPrimary.b + (Colors.error.b - Colors.accentPrimary.b) * factor, 1) } - NPill { - id: pill - icon: getIcon() - iconCircleColor: getVolumeColor() - collapsedIconColor: getIconColor() - autoHide: true - text: Math.round(Audio.volume * 100) + "%" - tooltipText: "Volume: " + Math.round( - Audio.volume * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume." - onClicked: function () { - console.log("onClicked") - // if (ioSelector.visible) { - // ioSelector.dismiss() - // } else { - // ioSelector.show() - // } - } - onWheel: function (angle) { - if (angle > 0) { - Audio.volumeIncrement() - } else if (angle < 0) { - Audio.volumeDecrement() - } - } - } - // Connection used to open the pill when volume changes Connections { target: Audio.sink?.audio ? Audio.sink?.audio : null @@ -80,8 +54,29 @@ Item { } } + NPill { + id: pill + icon: getIcon() + iconCircleColor: getVolumeColor() + collapsedIconColor: getIconColor() + autoHide: true + text: Math.round(Audio.volume * 100) + "%" + tooltipText: "Volume: " + Math.round( + Audio.volume * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume." + + onWheel: function (angle) { + if (angle > 0) { + Audio.volumeIncrement() + } else if (angle < 0) { + Audio.volumeDecrement() + } + } + onClicked: function () { + ioSelector.isLoaded = !ioSelector.isLoaded + } + } + AudioDeviceSelector { id: ioSelector - // onPanelClosed: ioSelector.dismiss() } } diff --git a/Services/Audio.qml b/Services/Audio.qml index 3c8bf02..6ce01a0 100644 --- a/Services/Audio.qml +++ b/Services/Audio.qml @@ -37,7 +37,7 @@ Singleton { } PwObjectTracker { - objects: [Pipewire.defaultAudioSink] + objects: [Pipewire.defaultAudioSink, Pipewire.nodes] } Connections { diff --git a/Widgets/NLoader.qml b/Widgets/NLoader.qml index 2dfd104..133083b 100644 --- a/Widgets/NLoader.qml +++ b/Widgets/NLoader.qml @@ -4,7 +4,6 @@ import QtQuick // NLoader { // content: Component { // NPanel { - Loader { id: loader From 160962d7af274a6e1ca43a00e6d74a53da25934c Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 19:34:24 -0400 Subject: [PATCH 082/394] Workspace: fix scaling not applied --- Modules/Bar/Workspace.qml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Modules/Bar/Workspace.qml b/Modules/Bar/Workspace.qml index 4c03f68..10529e3 100644 --- a/Modules/Bar/Workspace.qml +++ b/Modules/Bar/Workspace.qml @@ -12,20 +12,19 @@ Item { property bool isDestroying: false property bool hovered: false - readonly property real scaling: Scaling.scale(screen) - - signal workspaceChanged(int workspaceId, color accentColor) + // Unified scale + readonly property real s: Scaling.scale(screen) property ListModel localWorkspaces: ListModel {} property real masterProgress: 0.0 property bool effectsActive: false property color effectColor: Colors.accentPrimary - // Unified scale - property real s: scale property int horizontalPadding: Math.round(16 * s) property int spacingBetweenPills: Math.round(8 * s) + signal workspaceChanged(int workspaceId, color accentColor) + width: { let total = 0 for (var i = 0; i < localWorkspaces.count; i++) { From c1452e3c11afb8241fe9050c3f8a8307fad4d3b0 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 19:35:16 -0400 Subject: [PATCH 083/394] AudioDeviceSelector: moved to Shell so we only get one in memory --- Modules/Audio/AudioDeviceSelector.qml | 4 +--- Modules/Bar/Volume.qml | 7 +------ Widgets/NLoader.qml | 6 ++++-- shell.qml | 5 +++++ 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Modules/Audio/AudioDeviceSelector.qml b/Modules/Audio/AudioDeviceSelector.qml index 109d88d..e170844 100644 --- a/Modules/Audio/AudioDeviceSelector.qml +++ b/Modules/Audio/AudioDeviceSelector.qml @@ -119,9 +119,7 @@ NLoader { } } } -} -// NPanel { -// id: ioSelector +} // NPanel {// id: ioSelector // property int tabIndex: 0 // property Item anchorItem: null diff --git a/Modules/Bar/Volume.qml b/Modules/Bar/Volume.qml index d1f0992..f8f69d7 100644 --- a/Modules/Bar/Volume.qml +++ b/Modules/Bar/Volume.qml @@ -2,7 +2,6 @@ import QtQuick import Quickshell import Quickshell.Services.Pipewire import qs.Services -import qs.Modules.Audio import qs.Widgets Item { @@ -72,11 +71,7 @@ Item { } } onClicked: function () { - ioSelector.isLoaded = !ioSelector.isLoaded + audioDeviceSelector.isLoaded = !audioDeviceSelector.isLoaded } } - - AudioDeviceSelector { - id: ioSelector - } } diff --git a/Widgets/NLoader.qml b/Widgets/NLoader.qml index 133083b..b3c5f52 100644 --- a/Widgets/NLoader.qml +++ b/Widgets/NLoader.qml @@ -18,13 +18,15 @@ Loader { sourceComponent: content onActiveChanged: { - if (active && item && item.show) + if (active && item && item.show) { item.show() + } } onItemChanged: { - if (active && item && item.show) + if (active && item && item.show) { item.show() + } } Connections { diff --git a/shell.qml b/shell.qml index a1544a5..05c3654 100644 --- a/shell.qml +++ b/shell.qml @@ -6,6 +6,7 @@ import Quickshell.Io import Quickshell.Widgets import Quickshell.Services.Pipewire import qs.Widgets +import qs.Modules.Audio import qs.Modules.Bar import qs.Modules.DemoPanel import qs.Modules.Background @@ -32,4 +33,8 @@ ShellRoot { Notification { id: notification } + + AudioDeviceSelector { + id: audioDeviceSelector + } } From 93fca936d8b7179e3b4e2b88257686c86b6ad22e Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 21:31:09 -0400 Subject: [PATCH 084/394] Calendar is no longer a Widget, moved to Modules/Calendar/Calendar.qml - Using a NLoader - Got a display bug with DayOfWeekRow! --- Modules/Bar/Clock.qml | 10 +-- Modules/Calendar/Calendar.qml | 141 ++++++++++++++++++++++++++++++++++ Widgets/NCalendar.qml | 132 ------------------------------- Widgets/NLoader.qml | 4 + shell.qml | 5 ++ 5 files changed, 153 insertions(+), 139 deletions(-) create mode 100644 Modules/Calendar/Calendar.qml delete mode 100644 Widgets/NCalendar.qml diff --git a/Modules/Bar/Clock.qml b/Modules/Bar/Clock.qml index 79fda94..39f572b 100644 --- a/Modules/Bar/Clock.qml +++ b/Modules/Bar/Clock.qml @@ -12,13 +12,8 @@ NClock { target: root } - NCalendar { - id: calendar - visible: false - } - onEntered: function () { - if (!calendar.visible) { + if (!calendar.isLoaded) { tooltip.show() } } @@ -26,7 +21,8 @@ NClock { tooltip.hide() } onClicked: function () { - calendar.visible = !calendar.visible tooltip.hide() + calendar.isLoaded = !calendar.isLoaded + } } diff --git a/Modules/Calendar/Calendar.qml b/Modules/Calendar/Calendar.qml new file mode 100644 index 0000000..6f1b9e3 --- /dev/null +++ b/Modules/Calendar/Calendar.qml @@ -0,0 +1,141 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import qs.Services +import qs.Widgets + +NLoader { + id: root + + content: Component { + NPanel { + id: calendarPanel + + readonly property real scaling: Scaling.scale(screen) + + Rectangle { + color: Colors.backgroundSecondary + radius: Style.radiusMedium * scaling + border.color: Colors.backgroundTertiary + border.width: Math.min(1, Style.borderMedium * scaling) + width: 340 * scaling + height: 320 // TBC + anchors.top: parent.top + anchors.right: parent.right + anchors.topMargin: Style.marginTiny * scaling + anchors.rightMargin: Style.marginTiny * scaling + + // Prevent closing when clicking in the panel bg + MouseArea { + anchors.fill: parent + } + + // Main Column + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginMedium * scaling + spacing: Style.marginMedium * scaling + + // Header: Month/Year with navigation + RowLayout { + Layout.fillWidth: true + spacing: Style.marginSmall * scaling + + NIconButton { + icon: "chevron_left" + onClicked: function () { + let newDate = new Date(grid.year, grid.month - 1, 1) + grid.year = newDate.getFullYear() + grid.month = newDate.getMonth() + } + } + + NText { + text: grid.title + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + font.pointSize: Style.fontSizeMedium * scaling + color: Colors.accentPrimary + } + + NIconButton { + icon: "chevron_right" + onClicked: function () { + let newDate = new Date(grid.year, grid.month + 1, 1) + grid.year = newDate.getFullYear() + grid.month = newDate.getMonth() + } + } + } + + NDivider { + Layout.fillWidth: true + } + + // Columns label (Sunday to Saturday) + DayOfWeekRow { + Layout.fillWidth: true + spacing: 0 + Layout.leftMargin: Style.marginSmall * scaling // Align with grid + Layout.rightMargin: Style.marginSmall * scaling + + delegate: NText { + text: shortName + color: Colors.accentSecondary + font.pointSize: Style.fontSizeMedium * scaling + horizontalAlignment: Text.AlignHCenter + width: Style.baseWidgetSize * scaling + } + } + + // Grids: days + MonthGrid { + id: grid + + Layout.fillWidth: true + Layout.leftMargin: Style.marginSmall * scaling + Layout.rightMargin: Style.marginSmall * scaling + spacing: 0 + month: Time.date.getMonth() + year: Time.date.getFullYear() + + // Optionally, update when the panel becomes visible + Connections { + target: calendarPanel + function onVisibleChanged() { + if (calendarPanel.visible) { + grid.month = Time.date.getMonth() + grid.year = Time.date.getFullYear() + } + } + } + + delegate: Rectangle { + width: Style.baseWidgetSize * scaling + height: Style.baseWidgetSize * scaling + radius: Style.radiusSmall * scaling + color: model.today ? Colors.accentPrimary : "transparent" + + NText { + anchors.centerIn: parent + text: model.day + color: model.today ? Colors.onAccent : Colors.textPrimary + opacity: model.month === grid.month ? Style.opacityHeavy : Style.opacityLight + font.pointSize: Style.fontSizeMedium * scaling + font.bold: model.today ? true : false + } + + Behavior on color { + ColorAnimation { + duration: 150 + } + } + } + } + } + } + } + } +} diff --git a/Widgets/NCalendar.qml b/Widgets/NCalendar.qml deleted file mode 100644 index c3ffda8..0000000 --- a/Widgets/NCalendar.qml +++ /dev/null @@ -1,132 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell -import Quickshell.Wayland -import qs.Services - -NPanel { - id: root - - readonly property real scaling: Scaling.scale(screen) - - Rectangle { - color: Colors.backgroundSecondary - radius: Style.radiusMedium * scaling - border.color: Colors.backgroundTertiary - border.width: Math.min(1, Style.borderMedium * scaling) - width: 340 * scaling - height: 320 // TBC - anchors.top: parent.top - anchors.right: parent.right - anchors.topMargin: Style.marginTiny * scaling - anchors.rightMargin: Style.marginTiny * scaling - - // Prevent closing when clicking in the panel bg - MouseArea { - anchors.fill: parent - } - - ColumnLayout { - anchors.fill: parent - anchors.margins: Style.marginMedium * scaling - spacing: Style.marginMedium * scaling - - // Month/Year header with navigation - RowLayout { - Layout.fillWidth: true - spacing: Style.marginSmall * scaling - - NIconButton { - icon: "chevron_left" - onClicked: function () { - let newDate = new Date(calendar.year, calendar.month - 1, 1) - calendar.year = newDate.getFullYear() - calendar.month = newDate.getMonth() - } - } - - NText { - text: calendar.title - Layout.fillWidth: true - horizontalAlignment: Text.AlignHCenter - font.pointSize: Style.fontSizeMedium * scaling - color: Colors.accentPrimary - } - - NIconButton { - icon: "chevron_right" - onClicked: function () { - let newDate = new Date(calendar.year, calendar.month + 1, 1) - calendar.year = newDate.getFullYear() - calendar.month = newDate.getMonth() - } - } - } - - NDivider { - Layout.fillWidth: true - } - - DayOfWeekRow { - Layout.fillWidth: true - spacing: 0 - Layout.leftMargin: Style.marginSmall * scaling // Align with grid - Layout.rightMargin: Style.marginSmall * scaling - - delegate: NText { - text: shortName - color: Colors.accentSecondary - font.pointSize: Style.fontSizeMedium * scaling - horizontalAlignment: Text.AlignHCenter - width: Style.baseWidgetSize * scaling - } - } - - MonthGrid { - id: calendar - - Layout.fillWidth: true - Layout.leftMargin: Style.marginSmall * scaling - Layout.rightMargin: Style.marginSmall * scaling - spacing: 0 - month: Time.date.getMonth() - year: Time.date.getFullYear() - - // Optionally, update when the panel becomes visible - Connections { - function onVisibleChanged() { - if (root.visible) { - calendar.month = Time.date.getMonth() - calendar.year = Time.date.getFullYear() - } - } - - target: root - } - - delegate: Rectangle { - width: Style.baseWidgetSize * scaling - height: Style.baseWidgetSize * scaling - radius: Style.radiusSmall * scaling - color: model.today ? Colors.accentPrimary : "transparent" - - NText { - anchors.centerIn: parent - text: model.day - color: model.today ? Colors.onAccent : Colors.textPrimary - opacity: model.month === calendar.month ? Style.opacityHeavy : Style.opacityLight - font.pointSize: Style.fontSizeMedium * scaling - font.bold: model.today ? true : false - } - - Behavior on color { - ColorAnimation { - duration: 150 - } - } - } - } - } - } -} diff --git a/Widgets/NLoader.qml b/Widgets/NLoader.qml index b3c5f52..c41e00f 100644 --- a/Widgets/NLoader.qml +++ b/Widgets/NLoader.qml @@ -17,6 +17,10 @@ Loader { asynchronous: true sourceComponent: content + // onLoaded: { + // console.log("NLoader onLoaded: " + item.toString()); + // } + onActiveChanged: { if (active && item && item.show) { item.show() diff --git a/shell.qml b/shell.qml index 05c3654..b3f16c9 100644 --- a/shell.qml +++ b/shell.qml @@ -8,6 +8,7 @@ import Quickshell.Services.Pipewire import qs.Widgets import qs.Modules.Audio import qs.Modules.Bar +import qs.Modules.Calendar import qs.Modules.DemoPanel import qs.Modules.Background import qs.Modules.SidePanel @@ -37,4 +38,8 @@ ShellRoot { AudioDeviceSelector { id: audioDeviceSelector } + + Calendar { + id: calendar + } } From 2325943c66cd96138b1d711021f775bfd889b320 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 21:36:52 -0400 Subject: [PATCH 085/394] Audio Service: avoid NaN --- Services/Audio.qml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Services/Audio.qml b/Services/Audio.qml index 6ce01a0..1a3c8fa 100644 --- a/Services/Audio.qml +++ b/Services/Audio.qml @@ -44,13 +44,17 @@ Singleton { target: sink?.audio ? sink?.audio : null function onVolumeChanged() { - root._volume = (sink?.audio.volume ?? 0) - console.log("[Audio] onVolumeChanged: " + volume) + var vol = (sink?.audio.volume ?? 0) + if (isNaN(vol)) { + vol = 0 + } + root._volume = vol + console.log("[Audio] onVolumeChanged: " + root._volume.toFixed(2)) } function onMutedChanged() { root._muted = (sink?.audio.muted ?? true) - console.log("[Audio] onMuteChanged " + muted) + console.log("[Audio] onMuteChanged " + root._muted) } } } From d6d9350bffdae68ad5e84fd957cbac79fb5178db Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 21:37:04 -0400 Subject: [PATCH 086/394] formatting --- Modules/Bar/Clock.qml | 1 - Widgets/NLoader.qml | 1 - 2 files changed, 2 deletions(-) diff --git a/Modules/Bar/Clock.qml b/Modules/Bar/Clock.qml index 39f572b..6e05d79 100644 --- a/Modules/Bar/Clock.qml +++ b/Modules/Bar/Clock.qml @@ -23,6 +23,5 @@ NClock { onClicked: function () { tooltip.hide() calendar.isLoaded = !calendar.isLoaded - } } diff --git a/Widgets/NLoader.qml b/Widgets/NLoader.qml index c41e00f..b7aa6b3 100644 --- a/Widgets/NLoader.qml +++ b/Widgets/NLoader.qml @@ -20,7 +20,6 @@ Loader { // onLoaded: { // console.log("NLoader onLoaded: " + item.toString()); // } - onActiveChanged: { if (active && item && item.show) { item.show() From 6ce7c7d55dfac0ee3b268cb6e33c6679caae2a02 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 21:59:33 -0400 Subject: [PATCH 087/394] Tray: basic tray, no dropdown menu yet --- Modules/Bar/Bar.qml | 4 ++ Modules/Bar/Tray.qml | 122 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 Modules/Bar/Tray.qml diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index f0f1b89..ba608fc 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -76,6 +76,10 @@ Variants { anchors.verticalCenter: bar.verticalCenter spacing: Style.marginSmall * scaling + Tray { + anchors.verticalCenter: parent.verticalCenter + } + Battery { anchors.verticalCenter: parent.verticalCenter } diff --git a/Modules/Bar/Tray.qml b/Modules/Bar/Tray.qml new file mode 100644 index 0000000..74dff62 --- /dev/null +++ b/Modules/Bar/Tray.qml @@ -0,0 +1,122 @@ +import QtQuick +import QtQuick.Effects +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Services.SystemTray +import Quickshell.Widgets +import qs.Services +import qs.Widgets + +Row { + readonly property real scaling: Scaling.scale(screen) + property bool containsMouse: false + property var systemTray: SystemTray + + spacing: 8 + Layout.alignment: Qt.AlignVCenter + + Repeater { + model: systemTray.items + delegate: Item { + width: 24 * scaling + height: 24 * scaling + + visible: modelData + property bool isHovered: trayMouseArea.containsMouse + + // No animations - static display + Rectangle { + anchors.centerIn: parent + width: 16 * scaling + height: 16 * scaling + radius: 6 + color: "transparent" + clip: true + + IconImage { + id: trayIcon + anchors.centerIn: parent + width: 16 * scaling + height: 16 * scaling + smooth: false + asynchronous: true + backer.fillMode: Image.PreserveAspectFit + source: { + let icon = modelData?.icon || "" + if (!icon) { + return "" + } + + // Process icon path + if (icon.includes("?path=")) { + const chunks = icon.split("?path=") + const name = chunks[0] + const path = chunks[1] + const fileName = name.substring(name.lastIndexOf("/") + 1) + return `file://${path}/${fileName}` + } + return icon + } + opacity: status === Image.Ready ? 1 : 0 + } + } + + MouseArea { + id: trayMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + onClicked: mouse => { + if (!modelData) + return + + if (mouse.button === Qt.LeftButton) { + // Close any open menu first + if (trayMenu && trayMenu.visible) { + trayMenu.hideMenu() + } + + if (!modelData.onlyMenu) { + modelData.activate() + } + } else if (mouse.button === Qt.MiddleButton) { + // Close any open menu first + if (trayMenu && trayMenu.visible) { + trayMenu.hideMenu() + } + + modelData.secondaryActivate && modelData.secondaryActivate() + } else if (mouse.button === Qt.RightButton) { + trayTooltip.tooltipVisible = false + // If menu is already visible, close it + if (trayMenu && trayMenu.visible) { + trayMenu.hideMenu() + return + } + + if (modelData.hasMenu && modelData.menu && trayMenu) { + // Anchor the menu to the tray icon item (parent) and position it below the icon + const menuX = (width / 2) - (trayMenu.width / 2) + const menuY = height + 20 * scaling + trayMenu.menu = modelData.menu + trayMenu.showAt(parent, menuX, menuY) + } else { + + console.log("Tray: no menu available for", modelData.id, "or trayMenu not set") + } + } + } + onEntered: trayTooltip.show() + onExited: trayTooltip.hide() + } + + NTooltip { + id: trayTooltip + target: trayIcon + text: modelData.tooltipTitle || modelData.name || modelData.id || "Tray Item" + } + } + } +} From c64d14319ea0bbf2e568437942074d993f4286b3 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 22:59:42 -0400 Subject: [PATCH 088/394] Typography++ NText is now fontWeightRegular Transformed a few Text into NText Checked boldness Improved DemoPanel scaling and look --- Modules/Audio/AudioDeviceSelector.qml | 98 +----- Modules/Bar/Bar.qml | 1 + Modules/Bar/Tray.qml | 137 ++++---- Modules/Bar/TrayMenu.qml | 480 ++++++++++++++++++++++++++ Modules/Calendar/Calendar.qml | 4 +- Modules/DemoPanel/DemoPanel.qml | 16 +- Modules/Notification/Notification.qml | 2 +- Modules/SidePanel/ProfileCard.qml | 1 + Modules/SidePanel/WeatherCard.qml | 7 +- Services/Style.qml | 1 + Widgets/NClock.qml | 1 + Widgets/NIconButton.qml | 2 +- Widgets/NPill.qml | 5 +- Widgets/NText.qml | 2 +- Widgets/NToggle.qml | 6 +- Widgets/NTooltip.qml | 1 - 16 files changed, 584 insertions(+), 180 deletions(-) create mode 100644 Modules/Bar/TrayMenu.qml diff --git a/Modules/Audio/AudioDeviceSelector.qml b/Modules/Audio/AudioDeviceSelector.qml index e170844..460b297 100644 --- a/Modules/Audio/AudioDeviceSelector.qml +++ b/Modules/Audio/AudioDeviceSelector.qml @@ -32,105 +32,13 @@ NLoader { anchors.fill: parent } - ColumnLayout { - anchors.fill: parent - anchors.margins: Style.marginXL * scaling - spacing: Style.marginSmall * scaling - - // NIconButton - ColumnLayout { - spacing: 16 * scaling - NText { - text: "NIconButton" - color: Colors.accentSecondary - } - - NIconButton { - id: myIconButton - icon: "refresh" - } - - NDivider { - Layout.fillWidth: true - } - } - - // NToggle - ColumnLayout { - spacing: Style.marginLarge * scaling - NText { - text: "NToggle" - color: Colors.accentSecondary - } - - NToggle { - label: "Label" - description: "Description" - onToggled: function (value) { - console.log("NToggle: " + value) - } - } - - NDivider { - Layout.fillWidth: true - } - } - - // NSlider - ColumnLayout { - spacing: 16 * scaling - NText { - text: "Scaling" - color: Colors.accentSecondary - } - RowLayout { - spacing: Style.marginSmall * scaling - NText { - text: `${Math.round(Scaling.overrideScale * 100)}%` - Layout.alignment: Qt.AlignVCenter - } - NSlider { - id: scaleSlider - from: 0.6 - to: 1.8 - stepSize: 0.01 - value: Scaling.overrideScale - onMoved: function () { - Scaling.overrideScale = value - } - onPressedChanged: function () { - Scaling.overrideEnabled = true - } - } - NIconButton { - icon: "restart_alt" - sizeMultiplier: 0.7 - onClicked: function () { - Scaling.overrideEnabled = false - Scaling.overrideScale = 1.0 - } - } - } - NDivider { - Layout.fillWidth: true - } - } + NText { + text: "Audio Device Selector" } } } } -} // NPanel {// id: ioSelector - -// property int tabIndex: 0 -// property Item anchorItem: null - -// signal panelClosed() - -// function sinkNodes() { -// let nodes = Pipewire.nodes && Pipewire.nodes.values ? Pipewire.nodes.values.filter(function(n) { -// return n.isSink && n.audio && n.isStream === false; -// }) : []; -// if (Pipewire.defaultAudioSink) +} // NPanel {// id: ioSelector// property int tabIndex: 0// property Item anchorItem: null// signal panelClosed()// function sinkNodes() {// let nodes = Pipewire.nodes && Pipewire.nodes.values ? Pipewire.nodes.values.filter(function(n) {// return n.isSink && n.audio && n.isStream === false;// }) : [];// if (Pipewire.defaultAudioSink) // nodes = nodes.slice().sort(function(a, b) { // if (a.id === Pipewire.defaultAudioSink.id) // return -1; diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index ba608fc..6dd0d8b 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -51,6 +51,7 @@ Variants { NText { text: screen.name anchors.verticalCenter: parent.verticalCenter + font.weight: Style.fontWeightBold } } diff --git a/Modules/Bar/Tray.qml b/Modules/Bar/Tray.qml index 74dff62..eafdc12 100644 --- a/Modules/Bar/Tray.qml +++ b/Modules/Bar/Tray.qml @@ -8,31 +8,26 @@ import Quickshell.Widgets import qs.Services import qs.Widgets -Row { +Item { readonly property real scaling: Scaling.scale(screen) - property bool containsMouse: false - property var systemTray: SystemTray + readonly property real itemSize: 24 * scaling - spacing: 8 - Layout.alignment: Qt.AlignVCenter + width: tray.width + height: itemSize - Repeater { - model: systemTray.items - delegate: Item { - width: 24 * scaling - height: 24 * scaling + Row { + id: tray - visible: modelData - property bool isHovered: trayMouseArea.containsMouse + spacing: Style.marginSmall * scaling + Layout.alignment: Qt.AlignVCenter - // No animations - static display - Rectangle { - anchors.centerIn: parent - width: 16 * scaling - height: 16 * scaling - radius: 6 - color: "transparent" - clip: true + Repeater { + id: repeater + model: SystemTray.items + delegate: Item { + width: itemSize + height: itemSize + visible: modelData IconImage { id: trayIcon @@ -50,6 +45,7 @@ Row { // Process icon path if (icon.includes("?path=")) { + // Seems qmlfmt does not support the following ES6 syntax: const[name, path] = icon.split const chunks = icon.split("?path=") const name = chunks[0] const path = chunks[1] @@ -60,63 +56,68 @@ Row { } opacity: status === Image.Ready ? 1 : 0 } - } - MouseArea { - id: trayMouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - onClicked: mouse => { - if (!modelData) - return - - if (mouse.button === Qt.LeftButton) { - // Close any open menu first - if (trayMenu && trayMenu.visible) { - trayMenu.hideMenu() - } - - if (!modelData.onlyMenu) { - modelData.activate() - } - } else if (mouse.button === Qt.MiddleButton) { - // Close any open menu first - if (trayMenu && trayMenu.visible) { - trayMenu.hideMenu() - } - - modelData.secondaryActivate && modelData.secondaryActivate() - } else if (mouse.button === Qt.RightButton) { - trayTooltip.tooltipVisible = false - // If menu is already visible, close it - if (trayMenu && trayMenu.visible) { - trayMenu.hideMenu() + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + onClicked: mouse => { + if (!modelData) { return } - if (modelData.hasMenu && modelData.menu && trayMenu) { - // Anchor the menu to the tray icon item (parent) and position it below the icon - const menuX = (width / 2) - (trayMenu.width / 2) - const menuY = height + 20 * scaling - trayMenu.menu = modelData.menu - trayMenu.showAt(parent, menuX, menuY) - } else { + if (mouse.button === Qt.LeftButton) { + // Close any open menu first + if (trayMenu && trayMenu.visible) { + trayMenu.hideMenu() + } - console.log("Tray: no menu available for", modelData.id, "or trayMenu not set") + if (!modelData.onlyMenu) { + modelData.activate() + } + } else if (mouse.button === Qt.MiddleButton) { + // Close any open menu first + if (trayMenu && trayMenu.visible) { + trayMenu.hideMenu() + } + + modelData.secondaryActivate && modelData.secondaryActivate() + } else if (mouse.button === Qt.RightButton) { + trayTooltip.hide() + // If menu is already visible, close it + if (trayMenu && trayMenu.visible) { + trayMenu.hideMenu() + return + } + + if (modelData.hasMenu && modelData.menu && trayMenu) { + // Anchor the menu to the tray icon item (parent) and position it below the icon + const menuX = (width / 2) - (trayMenu.width / 2) + const menuY = height + 20 * scaling + trayMenu.menu = modelData.menu + trayMenu.showAt(parent, menuX, menuY) + } else { + + console.log("Tray: no menu available for", modelData.id, "or trayMenu not set") + } } } - } - onEntered: trayTooltip.show() - onExited: trayTooltip.hide() - } + onEntered: trayTooltip.show() + onExited: trayTooltip.hide() + } - NTooltip { - id: trayTooltip - target: trayIcon - text: modelData.tooltipTitle || modelData.name || modelData.id || "Tray Item" + NTooltip { + id: trayTooltip + target: trayIcon + text: modelData.tooltipTitle || modelData.name || modelData.id || "Tray Item" + } } } } + + // Attached TrayMenu + TrayMenu { + id: trayMenu + } } diff --git a/Modules/Bar/TrayMenu.qml b/Modules/Bar/TrayMenu.qml new file mode 100644 index 0000000..ceb529a --- /dev/null +++ b/Modules/Bar/TrayMenu.qml @@ -0,0 +1,480 @@ +pragma ComponentBehavior + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import Quickshell +import qs.Services +import qs.Widgets + +PopupWindow { + id: trayMenu + + readonly property real scaling: Scaling.scale(screen) + property QsMenuHandle menu + property var anchorItem: null + property real anchorX + property real anchorY + + implicitWidth: 180 + implicitHeight: Math.max(40, listView.contentHeight + 12) + visible: false + color: "transparent" + + anchor.item: anchorItem ? anchorItem : null + anchor.rect.x: anchorX + anchor.rect.y: anchorY - 4 + + // Recursive function to destroy all open submenus in delegate tree, safely avoiding infinite recursion + function destroySubmenusRecursively(item) { + if (!item || !item.contentItem) + return + var children = item.contentItem.children + for (var i = 0; i < children.length; ++i) { + var child = children[i] + if (child.subMenu) { + child.subMenu.hideMenu() + child.subMenu.destroy() + child.subMenu = null + } + // Recursively destroy submenus only if the child has contentItem to prevent issues + if (child.contentItem) { + destroySubmenusRecursively(child) + } + } + } + + function showAt(item, x, y) { + if (!item) { + console.warn("CustomTrayMenu: anchorItem is undefined, won't show menu.") + return + } + anchorItem = item + anchorX = x + anchorY = y + visible = true + forceActiveFocus() + Qt.callLater(() => trayMenu.anchor.updateAnchor()) + } + + function hideMenu() { + visible = false + destroySubmenusRecursively(listView) + } + + Item { + anchors.fill: parent + Keys.onEscapePressed: trayMenu.hideMenu() + } + + QsMenuOpener { + id: opener + menu: trayMenu.menu + } + + Rectangle { + id: bg + anchors.fill: parent + color: Colors.backgroundSecondary + border.color: Colors.outline + border.width: 1 + radius: 12 + z: 0 + } + + ListView { + id: listView + anchors.fill: parent + anchors.margins: 6 + spacing: 2 + interactive: false + enabled: trayMenu.visible + clip: true + + model: ScriptModel { + values: opener.children ? [...opener.children.values] : [] + } + + delegate: Rectangle { + id: entry + required property var modelData + + width: listView.width + height: (modelData?.isSeparator) ? 8 : 32 + color: "transparent" + radius: 12 + + property var subMenu: null + + Rectangle { + anchors.centerIn: parent + width: parent.width - 20 + height: 1 + color: Qt.darker(Colors.backgroundPrimary, 1.4) + visible: modelData?.isSeparator ?? false + } + + Rectangle { + id: bg + anchors.fill: parent + color: mouseArea.containsMouse ? Colors.highlight : "transparent" + radius: 8 + visible: !(modelData?.isSeparator ?? false) + property color hoverTextColor: mouseArea.containsMouse ? Colors.onAccent : Colors.textPrimary + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 12 + anchors.rightMargin: 12 + spacing: 8 + + NText { + Layout.fillWidth: true + color: (modelData?.enabled ?? true) ? bg.hoverTextColor : Colors.textDisabled + text: modelData?.text ?? "" + font.pointSize: Colors.fontSizeSmall * scaling + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + Image { + Layout.preferredWidth: 16 + Layout.preferredHeight: 16 + source: modelData?.icon ?? "" + visible: (modelData?.icon ?? "") !== "" + fillMode: Image.PreserveAspectFit + } + + // Chevron right for optional submenu + Text { + text: modelData?.hasChildren ? "menu" : "" + font.family: "Material Symbols Outlined" + font.pointSize: Colors.fontSizeMedium * scaling + verticalAlignment: Text.AlignVCenter + visible: modelData?.hasChildren ?? false + color: Colors.textPrimary + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + enabled: (modelData?.enabled ?? true) && !(modelData?.isSeparator ?? false) && trayMenu.visible + + onClicked: { + if (modelData && !modelData.isSeparator) { + if (modelData.hasChildren) { + // Submenus open on hover; ignore click here + return + } + modelData.triggered() + trayMenu.hideMenu() + } + } + + onEntered: { + if (!trayMenu.visible) + return + + if (modelData?.hasChildren) { + // Close sibling submenus immediately + for (var i = 0; i < listView.contentItem.children.length; i++) { + const sibling = listView.contentItem.children[i] + if (sibling !== entry && sibling.subMenu) { + sibling.subMenu.hideMenu() + sibling.subMenu.destroy() + sibling.subMenu = null + } + } + if (entry.subMenu) { + entry.subMenu.hideMenu() + entry.subMenu.destroy() + entry.subMenu = null + } + var globalPos = entry.mapToGlobal(0, 0) + var submenuWidth = 180 + var gap = 12 + var openLeft = (globalPos.x + entry.width + submenuWidth > Screen.width) + var anchorX = openLeft ? -submenuWidth - gap : entry.width + gap + + entry.subMenu = subMenuComponent.createObject(trayMenu, { + "menu": modelData, + "anchorItem": entry, + "anchorX": anchorX, + "anchorY": 0 + }) + entry.subMenu.showAt(entry, anchorX, 0) + } else { + // Hovered item without submenu; close siblings + for (var i = 0; i < listView.contentItem.children.length; i++) { + const sibling = listView.contentItem.children[i] + if (sibling.subMenu) { + sibling.subMenu.hideMenu() + sibling.subMenu.destroy() + sibling.subMenu = null + } + } + if (entry.subMenu) { + entry.subMenu.hideMenu() + entry.subMenu.destroy() + entry.subMenu = null + } + } + } + + onExited: { + if (entry.subMenu && !entry.subMenu.containsMouse()) { + entry.subMenu.hideMenu() + entry.subMenu.destroy() + entry.subMenu = null + } + } + } + } + + // Simplified containsMouse without recursive calls to avoid stack overflow + function containsMouse() { + return mouseArea.containsMouse + } + + Component.onDestruction: { + if (subMenu) { + subMenu.destroy() + subMenu = null + } + } + } + } + + Component { + id: subMenuComponent + + PopupWindow { + id: subMenu + implicitWidth: 180 + implicitHeight: Math.max(40, listView.contentHeight + 12) + visible: false + color: "transparent" + + property QsMenuHandle menu + property var anchorItem: null + property real anchorX + property real anchorY + + anchor.item: anchorItem ? anchorItem : null + anchor.rect.x: anchorX + anchor.rect.y: anchorY + + function showAt(item, x, y) { + if (!item) { + console.warn("subMenuComponent: anchorItem is undefined, not showing menu.") + return + } + anchorItem = item + anchorX = x + anchorY = y + visible = true + Qt.callLater(() => subMenu.anchor.updateAnchor()) + } + + function hideMenu() { + visible = false + // Close all submenus recursively in this submenu + for (var i = 0; i < listView.contentItem.children.length; i++) { + const child = listView.contentItem.children[i] + if (child.subMenu) { + child.subMenu.hideMenu() + child.subMenu.destroy() + child.subMenu = null + } + } + } + + // Simplified containsMouse avoiding recursive calls + function containsMouse() { + return subMenu.containsMouse + } + + Item { + anchors.fill: parent + Keys.onEscapePressed: subMenu.hideMenu() + } + + QsMenuOpener { + id: opener + menu: subMenu.menu + } + + Rectangle { + id: bg + anchors.fill: parent + color: Colors.backgroundPrimary + border.color: Colors.outline + border.width: 1 + radius: 12 + z: 0 + } + + ListView { + id: listView + anchors.fill: parent + anchors.margins: 6 + spacing: 2 + interactive: false + enabled: subMenu.visible + clip: true + + model: ScriptModel { + values: opener.children ? [...opener.children.values] : [] + } + + delegate: Rectangle { + id: entry + required property var modelData + + width: listView.width + height: (modelData?.isSeparator) ? 8 : 32 + color: "transparent" + radius: 12 + + property var subMenu: null + + Rectangle { + anchors.centerIn: parent + width: parent.width - 20 + height: 1 + color: Qt.darker(Colors.surfaceVariant, 1.4) + visible: modelData?.isSeparator ?? false + } + + Rectangle { + id: bg + anchors.fill: parent + color: mouseArea.containsMouse ? Colors.highlight : "transparent" + radius: 8 + visible: !(modelData?.isSeparator ?? false) + property color hoverTextColor: mouseArea.containsMouse ? Colors.onAccent : Colors.textPrimary + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 12 + anchors.rightMargin: 12 + spacing: 8 + + NText { + Layout.fillWidth: true + color: (modelData?.enabled ?? true) ? bg.hoverTextColor : Colors.textDisabled + text: modelData?.text ?? "" + font.pointSize: Colors.fontSizeSmall * Colors.scale(screen) + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + Image { + Layout.preferredWidth: 16 + Layout.preferredHeight: 16 + source: modelData?.icon ?? "" + visible: (modelData?.icon ?? "") !== "" + fillMode: Image.PreserveAspectFit + } + + NText { + text: modelData?.hasChildren ? "\uE5CC" : "" + font.family: "Material Symbols Outlined" + font.pointSize: Colors.fontSizeMedium * scaling + verticalAlignment: Text.AlignVCenter + visible: modelData?.hasChildren ?? false + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + enabled: (modelData?.enabled ?? true) && !(modelData?.isSeparator ?? false) && subMenu.visible + + onClicked: { + if (modelData && !modelData.isSeparator) { + if (modelData.hasChildren) { + return + } + modelData.triggered() + trayMenu.hideMenu() + } + } + + onEntered: { + if (!subMenu.visible) + return + + if (modelData?.hasChildren) { + for (var i = 0; i < listView.contentItem.children.length; i++) { + const sibling = listView.contentItem.children[i] + if (sibling !== entry && sibling.subMenu) { + sibling.subMenu.hideMenu() + sibling.subMenu.destroy() + sibling.subMenu = null + } + } + if (entry.subMenu) { + entry.subMenu.hideMenu() + entry.subMenu.destroy() + entry.subMenu = null + } + var globalPos = entry.mapToGlobal(0, 0) + var submenuWidth = 180 + var gap = 12 + var openLeft = (globalPos.x + entry.width + submenuWidth > Screen.width) + var anchorX = openLeft ? -submenuWidth - gap : entry.width + gap + + entry.subMenu = subMenuComponent.createObject(subMenu, { + "menu": modelData, + "anchorItem": entry, + "anchorX": anchorX, + "anchorY": 0 + }) + entry.subMenu.showAt(entry, anchorX, 0) + } else { + for (var i = 0; i < listView.contentItem.children.length; i++) { + const sibling = listView.contentItem.children[i] + if (sibling.subMenu) { + sibling.subMenu.hideMenu() + sibling.subMenu.destroy() + sibling.subMenu = null + } + } + if (entry.subMenu) { + entry.subMenu.hideMenu() + entry.subMenu.destroy() + entry.subMenu = null + } + } + } + + onExited: { + if (entry.subMenu && !entry.subMenu.containsMouse()) { + entry.subMenu.hideMenu() + entry.subMenu.destroy() + entry.subMenu = null + } + } + } + } + + // Simplified & safe containsMouse avoiding recursion + function containsMouse() { + return mouseArea.containsMouse + } + + Component.onDestruction: { + if (subMenu) { + subMenu.destroy() + subMenu = null + } + } + } + } + } + } +} diff --git a/Modules/Calendar/Calendar.qml b/Modules/Calendar/Calendar.qml index 6f1b9e3..ca60424 100644 --- a/Modules/Calendar/Calendar.qml +++ b/Modules/Calendar/Calendar.qml @@ -57,6 +57,7 @@ NLoader { Layout.fillWidth: true horizontalAlignment: Text.AlignHCenter font.pointSize: Style.fontSizeMedium * scaling + font.weight: Style.fontWeightBold color: Colors.accentPrimary } @@ -85,6 +86,7 @@ NLoader { text: shortName color: Colors.accentSecondary font.pointSize: Style.fontSizeMedium * scaling + font.weight: Style.fontWeightBold horizontalAlignment: Text.AlignHCenter width: Style.baseWidgetSize * scaling } @@ -124,7 +126,7 @@ NLoader { color: model.today ? Colors.onAccent : Colors.textPrimary opacity: model.month === grid.month ? Style.opacityHeavy : Style.opacityLight font.pointSize: Style.fontSizeMedium * scaling - font.bold: model.today ? true : false + font.weight: model.today ? Style.fontWeightBold : Style.fontWeightRegular } Behavior on color { diff --git a/Modules/DemoPanel/DemoPanel.qml b/Modules/DemoPanel/DemoPanel.qml index 97c51d7..6e46687 100644 --- a/Modules/DemoPanel/DemoPanel.qml +++ b/Modules/DemoPanel/DemoPanel.qml @@ -23,12 +23,13 @@ NLoader { Component.onCompleted: show() Rectangle { + id: bgRect color: Colors.backgroundPrimary radius: Style.radiusMedium * scaling border.color: Colors.backgroundTertiary border.width: Math.min(1, Style.borderMedium * scaling) width: 500 * scaling - height: 400 + height: 400 * scaling anchors.centerIn: parent // Prevent closing when clicking in the panel bg @@ -47,11 +48,14 @@ NLoader { NText { text: "NIconButton" color: Colors.accentSecondary + font.weight: Style.fontWeightBold } NIconButton { id: myIconButton - icon: "refresh" + icon: "celebration" + sizeMultiplier: 1.0 + fontPointSize: Style.fontSizeXL * scaling } NDivider { @@ -65,6 +69,7 @@ NLoader { NText { text: "NToggle" color: Colors.accentSecondary + font.weight: Style.fontWeightBold } NToggle { @@ -86,6 +91,7 @@ NLoader { NText { text: "Scaling" color: Colors.accentSecondary + font.weight: Style.fontWeightBold } RowLayout { spacing: Style.marginSmall * scaling @@ -99,6 +105,7 @@ NLoader { to: 1.8 stepSize: 0.01 value: Scaling.overrideScale + implicitWidth: bgRect.width * 0.75 onMoved: function () { Scaling.overrideScale = value } @@ -107,8 +114,9 @@ NLoader { } } NIconButton { - icon: "restart_alt" - sizeMultiplier: 0.7 + icon: "refresh" + sizeMultiplier: 1.0 + fontPointSize: Style.fontSizeXL * scaling onClicked: function () { Scaling.overrideEnabled = false Scaling.overrideScale = 1.0 diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index 2e59bef..5f96d84 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -155,7 +155,7 @@ PanelWindow { NText { text: model.summary || "No summary" font.pointSize: Style.fontSizeLarge - font.bold: true + font.weight: Style.fontWeightBold color: Colors.textPrimary wrapMode: Text.Wrap width: 300 * scaling diff --git a/Modules/SidePanel/ProfileCard.qml b/Modules/SidePanel/ProfileCard.qml index 7488e9d..441e68f 100644 --- a/Modules/SidePanel/ProfileCard.qml +++ b/Modules/SidePanel/ProfileCard.qml @@ -54,6 +54,7 @@ NBox { spacing: 2 * scaling NText { text: Quickshell.env("USER") || "user" + font.weight: Style.fontWeightBold } NText { text: "System Uptime: —" diff --git a/Modules/SidePanel/WeatherCard.qml b/Modules/SidePanel/WeatherCard.qml index 875805c..5d52dae 100644 --- a/Modules/SidePanel/WeatherCard.qml +++ b/Modules/SidePanel/WeatherCard.qml @@ -26,7 +26,7 @@ NBox { Text { text: "sunny" font.family: "Material Symbols Outlined" - font.pointSize: Style.fontSizeXL * scaling + font.pointSize: Style.fontSizeXXL * 1.25 * scaling color: Colors.accentSecondary } ColumnLayout { @@ -36,6 +36,7 @@ NBox { NText { text: "26°C" font.pointSize: (Style.fontSizeXL + 6) * scaling + font.weight: Style.fontWeightBold } } } @@ -55,10 +56,12 @@ NBox { spacing: 2 * scaling NText { text: ["Sun", "Mon", "Tue", "Wed", "Thu"][index] + font.weight: Style.fontWeightBold } - Text { + NText { text: index % 2 === 0 ? "wb_sunny" : "cloud" font.family: "Material Symbols Outlined" + font.weight: Style.fontWeightBold color: Colors.textSecondary } NText { diff --git a/Services/Style.qml b/Services/Style.qml index d737c72..f3d1bcf 100644 --- a/Services/Style.qml +++ b/Services/Style.qml @@ -17,6 +17,7 @@ Singleton { property real fontSizeMedium: 11 property real fontSizeLarge: 13 property real fontSizeXL: 18 + property real fontSizeXXL: 24 // Font weight / Unsure if we keep em? property int fontWeightRegular: 400 diff --git a/Widgets/NClock.qml b/Widgets/NClock.qml index f5ca8f5..e1c4bd8 100644 --- a/Widgets/NClock.qml +++ b/Widgets/NClock.qml @@ -18,6 +18,7 @@ Rectangle { id: textItem text: Time.time anchors.centerIn: parent + font.weight: Style.fontWeightBold } MouseArea { diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml index 175e5e9..042438a 100644 --- a/Widgets/NIconButton.qml +++ b/Widgets/NIconButton.qml @@ -25,7 +25,7 @@ Rectangle { color: root.hovering ? Colors.accentPrimary : "transparent" - Text { + NText { anchors.centerIn: parent anchors.horizontalCenterOffset: 0 anchors.verticalCenterOffset: 0 diff --git a/Widgets/NPill.qml b/Widgets/NPill.qml index 9a040f2..ff819f3 100644 --- a/Widgets/NPill.qml +++ b/Widgets/NPill.qml @@ -52,13 +52,12 @@ Item { bottomLeftRadius: pillHeight * 0.5 anchors.verticalCenter: parent.verticalCenter - Text { + NText { id: textItem anchors.centerIn: parent text: root.text font.pointSize: Colors.fontSizeSmall * scaling - font.family: Settings.data.ui.fontFamily - font.weight: Font.Bold + font.weight: Style.fontWeightBold color: textColor visible: showPill } diff --git a/Widgets/NText.qml b/Widgets/NText.qml index 0b9b13a..0544aac 100644 --- a/Widgets/NText.qml +++ b/Widgets/NText.qml @@ -9,6 +9,6 @@ Text { font.family: Settings.data.ui.fontFamily font.pointSize: Style.fontSizeMedium * scaling - font.weight: Font.Bold + font.weight: Style.fontWeightRegular color: Colors.textPrimary } diff --git a/Widgets/NToggle.qml b/Widgets/NToggle.qml index 1c191f7..2da7606 100644 --- a/Widgets/NToggle.qml +++ b/Widgets/NToggle.qml @@ -19,14 +19,14 @@ RowLayout { spacing: 2 * scaling Layout.fillWidth: true - Text { + NText { text: label font.pointSize: Style.fontSizeMedium * scaling - font.bold: true + font.weight: Style.fontWeightBold color: Colors.textPrimary } - Text { + NText { text: description font.pointSize: Style.fontSizeSmall * scaling color: Colors.textSecondary diff --git a/Widgets/NTooltip.qml b/Widgets/NTooltip.qml index 8a709c7..99efe7f 100644 --- a/Widgets/NTooltip.qml +++ b/Widgets/NTooltip.qml @@ -112,7 +112,6 @@ Window { anchors.centerIn: parent text: root.text font.pointSize: Style.fontSizeMedium * scaling - font.weight: Style.fontWeightRegular horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter wrapMode: Text.Wrap From f3530532c9fbe84fc5e94e629954a1bd9a6023ed Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 23:39:03 -0400 Subject: [PATCH 089/394] TrayMenu adaptation to colors, spacing, scaling, etc... --- Modules/Bar/Tray.qml | 2 +- Modules/Bar/TrayMenu.qml | 97 +++++++++++++++++++--------------------- Services/Style.qml | 2 + 3 files changed, 50 insertions(+), 51 deletions(-) diff --git a/Modules/Bar/Tray.qml b/Modules/Bar/Tray.qml index eafdc12..5ae2c99 100644 --- a/Modules/Bar/Tray.qml +++ b/Modules/Bar/Tray.qml @@ -94,7 +94,7 @@ Item { if (modelData.hasMenu && modelData.menu && trayMenu) { // Anchor the menu to the tray icon item (parent) and position it below the icon const menuX = (width / 2) - (trayMenu.width / 2) - const menuY = height + 20 * scaling + const menuY = (Style.barHeight * scaling) trayMenu.menu = modelData.menu trayMenu.showAt(parent, menuX, menuY) } else { diff --git a/Modules/Bar/TrayMenu.qml b/Modules/Bar/TrayMenu.qml index ceb529a..37f8d51 100644 --- a/Modules/Bar/TrayMenu.qml +++ b/Modules/Bar/TrayMenu.qml @@ -1,8 +1,6 @@ -pragma ComponentBehavior - -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts import Quickshell import qs.Services import qs.Widgets @@ -16,8 +14,8 @@ PopupWindow { property real anchorX property real anchorY - implicitWidth: 180 - implicitHeight: Math.max(40, listView.contentHeight + 12) + implicitWidth: 180 * scaling + implicitHeight: Math.max(40 * scaling, listView.contentHeight + (Style.spacingMedium * 2 * scaling)) visible: false color: "transparent" @@ -77,16 +75,16 @@ PopupWindow { anchors.fill: parent color: Colors.backgroundSecondary border.color: Colors.outline - border.width: 1 - radius: 12 + border.width: Math.max(1, Style.borderThin * scaling) + radius: Style.radiusMedium * scaling z: 0 } ListView { id: listView anchors.fill: parent - anchors.margins: 6 - spacing: 2 + anchors.margins: Style.spacingMedium * scaling + spacing: 0 interactive: false enabled: trayMenu.visible clip: true @@ -100,17 +98,14 @@ PopupWindow { required property var modelData width: listView.width - height: (modelData?.isSeparator) ? 8 : 32 + height: (modelData?.isSeparator) ? 8 * scaling : 32 * scaling color: "transparent" - radius: 12 property var subMenu: null - Rectangle { + NDivider { anchors.centerIn: parent - width: parent.width - 20 - height: 1 - color: Qt.darker(Colors.backgroundPrimary, 1.4) + width: parent.width - (Style.marginMedium * scaling * 2) visible: modelData?.isSeparator ?? false } @@ -118,19 +113,19 @@ PopupWindow { id: bg anchors.fill: parent color: mouseArea.containsMouse ? Colors.highlight : "transparent" - radius: 8 + radius: Style.radiusSmall * scaling visible: !(modelData?.isSeparator ?? false) - property color hoverTextColor: mouseArea.containsMouse ? Colors.onAccent : Colors.textPrimary RowLayout { anchors.fill: parent - anchors.leftMargin: 12 - anchors.rightMargin: 12 - spacing: 8 + anchors.leftMargin: Style.marginMedium * scaling + anchors.rightMargin: Style.marginMedium * scaling + spacing: Style.marginSmall * scaling NText { Layout.fillWidth: true - color: (modelData?.enabled ?? true) ? bg.hoverTextColor : Colors.textDisabled + color: (modelData?.enabled + ?? true) ? (mouseArea.containsMouse ? Colors.onAccent : Colors.textPrimary) : Colors.textDisabled text: modelData?.text ?? "" font.pointSize: Colors.fontSizeSmall * scaling verticalAlignment: Text.AlignVCenter @@ -138,8 +133,8 @@ PopupWindow { } Image { - Layout.preferredWidth: 16 - Layout.preferredHeight: 16 + Layout.preferredWidth: 16 * scaling + Layout.preferredHeight: 16 * scaling source: modelData?.icon ?? "" visible: (modelData?.icon ?? "") !== "" fillMode: Image.PreserveAspectFit @@ -175,7 +170,7 @@ PopupWindow { onEntered: { if (!trayMenu.visible) - return + return if (modelData?.hasChildren) { // Close sibling submenus immediately @@ -193,8 +188,8 @@ PopupWindow { entry.subMenu = null } var globalPos = entry.mapToGlobal(0, 0) - var submenuWidth = 180 - var gap = 12 + var submenuWidth = 180 * scaling + var gap = 12 * scaling var openLeft = (globalPos.x + entry.width + submenuWidth > Screen.width) var anchorX = openLeft ? -submenuWidth - gap : entry.width + gap @@ -247,12 +242,15 @@ PopupWindow { } } + // ----------------------------------------- + // Sub Component + // ----------------------------------------- Component { id: subMenuComponent PopupWindow { id: subMenu - implicitWidth: 180 + implicitWidth: 180 * scaling implicitHeight: Math.max(40, listView.contentHeight + 12) visible: false color: "transparent" @@ -311,16 +309,16 @@ PopupWindow { anchors.fill: parent color: Colors.backgroundPrimary border.color: Colors.outline - border.width: 1 - radius: 12 + border.width: Math.max(1, Style.borderThin * scaling) + radius: Style.radiusMedium * scaling z: 0 } ListView { id: listView anchors.fill: parent - anchors.margins: 6 - spacing: 2 + anchors.margins: Style.spacingSmall * scaling + spacing: Style.spacingTiny * scaling interactive: false enabled: subMenu.visible clip: true @@ -334,17 +332,14 @@ PopupWindow { required property var modelData width: listView.width - height: (modelData?.isSeparator) ? 8 : 32 + height: (modelData?.isSeparator) ? 8 * scaling : 32 * scaling color: "transparent" - radius: 12 property var subMenu: null - Rectangle { + NDivider { anchors.centerIn: parent - width: parent.width - 20 - height: 1 - color: Qt.darker(Colors.surfaceVariant, 1.4) + width: parent.width - (Style.marginMedium * scaling * 2) visible: modelData?.isSeparator ?? false } @@ -352,33 +347,34 @@ PopupWindow { id: bg anchors.fill: parent color: mouseArea.containsMouse ? Colors.highlight : "transparent" - radius: 8 + radius: Style.radiusSmall * scaling visible: !(modelData?.isSeparator ?? false) property color hoverTextColor: mouseArea.containsMouse ? Colors.onAccent : Colors.textPrimary RowLayout { anchors.fill: parent - anchors.leftMargin: 12 - anchors.rightMargin: 12 - spacing: 8 + anchors.leftMargin: Style.spacingMedium * scaling + anchors.rightMargin: Style.spacingMedium * scaling + spacing: Style.spacingSmall * scaling NText { Layout.fillWidth: true color: (modelData?.enabled ?? true) ? bg.hoverTextColor : Colors.textDisabled text: modelData?.text ?? "" - font.pointSize: Colors.fontSizeSmall * Colors.scale(screen) + font.pointSize: Colors.fontSizeSmall * scaling verticalAlignment: Text.AlignVCenter elide: Text.ElideRight } Image { - Layout.preferredWidth: 16 - Layout.preferredHeight: 16 + Layout.preferredWidth: 16 * scaling + Layout.preferredHeight: 16 * scaling source: modelData?.icon ?? "" visible: (modelData?.icon ?? "") !== "" fillMode: Image.PreserveAspectFit } + // TBC a Square UTF-8? NText { text: modelData?.hasChildren ? "\uE5CC" : "" font.family: "Material Symbols Outlined" @@ -405,8 +401,9 @@ PopupWindow { } onEntered: { - if (!subMenu.visible) - return + if (!subMenu.visible) { + return + } if (modelData?.hasChildren) { for (var i = 0; i < listView.contentItem.children.length; i++) { @@ -423,8 +420,8 @@ PopupWindow { entry.subMenu = null } var globalPos = entry.mapToGlobal(0, 0) - var submenuWidth = 180 - var gap = 12 + var submenuWidth = 180 * scaling + var gap = 12 * scaling var openLeft = (globalPos.x + entry.width + submenuWidth > Screen.width) var anchorX = openLeft ? -submenuWidth - gap : entry.width + gap diff --git a/Services/Style.qml b/Services/Style.qml index f3d1bcf..5c63b38 100644 --- a/Services/Style.qml +++ b/Services/Style.qml @@ -28,6 +28,7 @@ Singleton { property int radiusLarge: 20 property int radiusMedium: 16 property int radiusSmall: 12 + property int radiusTiny: 8 // Border property int borderThin: 1 @@ -39,6 +40,7 @@ Singleton { property int spacingLarge: 16 property int spacingMedium: 12 property int spacingSmall: 8 + property int spacingTiny: 4 // Animation duration (ms) property int animationFast: 150 From 3bd339c122e47606ff2d45d4b7c9b25c4ef226c4 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 23:40:10 -0400 Subject: [PATCH 090/394] Slightly faster tooltipDelayLong --- Services/Style.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Services/Style.qml b/Services/Style.qml index 5c63b38..7bb9608 100644 --- a/Services/Style.qml +++ b/Services/Style.qml @@ -54,7 +54,7 @@ Singleton { // Delays property int tooltipDelay: 300 - property int tooltipDelayLong: 1500 + property int tooltipDelayLong: 1200 property int pillDelay: 500 // Margins and spacing From 5c57625ac90d2a4835021c431892f89bdca53950 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 23:43:43 -0400 Subject: [PATCH 091/394] Notification: backgroundSecondary --- Modules/Notification/Notification.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index 5f96d84..7ff94fa 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -62,7 +62,7 @@ PanelWindow { width: 360 * scaling height: Math.max(80 * scaling, contentColumn.implicitHeight + (Style.marginMedium * 2 * scaling)) clip: true - color: Colors.backgroundPrimary + color: Colors.backgroundSecondary radius: Style.radiusMedium * scaling border.color: Colors.backgroundTertiary border.width: Math.min(1, Style.borderThin * scaling) From ff2bbe60a43cc223859ef80488afca2503acee88 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 23:51:54 -0400 Subject: [PATCH 092/394] Deleting unused wallust templates --- Assets/Wallust/Templates/kitty.conf | 29 --- Assets/Wallust/Templates/niri.kdl | 292 ---------------------------- 2 files changed, 321 deletions(-) delete mode 100644 Assets/Wallust/Templates/kitty.conf delete mode 100644 Assets/Wallust/Templates/niri.kdl diff --git a/Assets/Wallust/Templates/kitty.conf b/Assets/Wallust/Templates/kitty.conf deleted file mode 100644 index 72fb6f4..0000000 --- a/Assets/Wallust/Templates/kitty.conf +++ /dev/null @@ -1,29 +0,0 @@ -# The kitty terminal template for wallust -# Add to wallust config: kitty = { src='kitty.conf', dst='~/.config/kitty/colors.conf'} -# And add to kitty config: include colors.conf - -cursor {{ cursor }} - -background {{ background }} -foreground {{ foreground }} - -color0 {{ color0 }} -color1 {{ color1 }} -color2 {{ color2 }} -color3 {{ color3 }} -color4 {{ color4 }} -color5 {{ color5 }} -color6 {{ color6 }} -color7 {{ color7 }} -color8 {{ color8 }} -color9 {{ color9 }} -color10 {{ color10 }} -color11 {{ color11 }} -color12 {{ color12 }} -color13 {{ color13 }} -color14 {{ color14 }} -color15 {{ color15 }} - -mark1_foreground {{ color6 | saturate(0.2) }} -mark2_foreground {{ color7 | saturate(0.2) }} -mark3_foreground {{ color6 | saturate(0.2) }} \ No newline at end of file diff --git a/Assets/Wallust/Templates/niri.kdl b/Assets/Wallust/Templates/niri.kdl deleted file mode 100644 index cd99b52..0000000 --- a/Assets/Wallust/Templates/niri.kdl +++ /dev/null @@ -1,292 +0,0 @@ -// Niri configuration for CachyOS -// For documentation and full reference, see: https://github.com/YaLTeR/niri/wiki - -// ────────────── Input Configuration ────────────── -// https://github.com/YaLTeR/niri/wiki/Configuration:-Input - -input { - keyboard { - xkb { - layout "de" // Use the German keyboard layout - } - numlock // Enable numlock on startup - } - - touchpad { - tap // Enable tap-to-click - natural-scroll // Enable natural (macOS-style) scrolling - } - - focus-follows-mouse // Automatically focus windows under the mouse pointer - workspace-auto-back-and-forth // Enable workspace back & forth switching -} - -// ────────────── Output Configuration ────────────── -// You can run `niri msg outputs` to get the correct name for your displays. -// You will have to remove "/-" and edit it before it takes effect. -// https://github.com/YaLTeR/niri/wiki/Configuration:-Outputs - - output "DP-1" { - mode "2560x1440@359.979" // Set resolution and refresh rate - scale 1 // No scaling (use 2 for HiDPI) -} - -// ────────────── Keybindings ────────────── -// https://github.com/YaLTeR/niri/wiki/Configuration:-Key-Bindings - -binds { - MOD+SHIFT+ESCAPE { show-hotkey-overlay; } - - // ─── Applications ─── - MOD+RETURN hotkey-overlay-title="Open Terminal: Kitty" { spawn "kitty"; } - MOD+CTRL+RETURN hotkey-overlay-title="Open App Launcher: QS" { spawn "qs" "ipc" "call" "globalIPC" "toggleLauncher"; } - MOD+B hotkey-overlay-title="Open Browser: firefox" { spawn "firefox"; } - MOD+ALT+L hotkey-overlay-title="Lock Screen: swaylock" { spawn "swaylock"; } - - // Please choose your own file manager - MOD+E hotkey-overlay-title="File Manager: Nautilus" { spawn "nautilus"; } - - // ─── Audio Controls ─── - XF86AUDIORAISEVOLUME allow-when-locked=true { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1+"; } - XF86AUDIOLOWERVOLUME allow-when-locked=true { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1-"; } - XF86AUDIOMUTE allow-when-locked=true { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SINK@" "toggle"; } - XF86AUDIOMICMUTE allow-when-locked=true { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SOURCE@" "toggle"; } - - // ─── Window Movement and Focus ─── - MOD+Q { close-window; } - - MOD+LEFT { focus-column-left; } - MOD+H { focus-column-left; } - MOD+RIGHT { focus-column-right; } - MOD+L { focus-column-right; } - MOD+UP { focus-window-up; } - MOD+K { focus-window-up; } - MOD+DOWN { focus-window-down; } - MOD+J { focus-window-down; } - - MOD+CTRL+LEFT { move-column-left; } - MOD+CTRL+H { move-column-left; } - MOD+CTRL+RIGHT { move-column-right; } - MOD+CTRL+L { move-column-right; } - MOD+CTRL+UP { move-window-up; } - MOD+CTRL+K { move-window-up; } - MOD+CTRL+DOWN { move-window-down; } - MOD+CTRL+J { move-window-down; } - - MOD+HOME { focus-column-first; } - MOD+END { focus-column-last; } - MOD+CTRL+HOME { move-column-to-first; } - MOD+CTRL+END { move-column-to-last; } - - MOD+SHIFT+LEFT { focus-monitor-left; } - MOD+SHIFT+RIGHT { focus-monitor-right; } - MOD+SHIFT+UP { focus-monitor-up; } - MOD+SHIFT+DOWN { focus-monitor-down; } - - MOD+SHIFT+CTRL+LEFT { move-column-to-monitor-left; } - MOD+SHIFT+CTRL+RIGHT { move-column-to-monitor-right; } - MOD+SHIFT+CTRL+UP { move-column-to-monitor-up; } - MOD+SHIFT+CTRL+DOWN { move-column-to-monitor-down; } - - // ─── Workspace Switching ─── - MOD+WHEELSCROLLDOWN cooldown-ms=150 { focus-workspace-down; } - MOD+WHEELSCROLLUP cooldown-ms=150 { focus-workspace-up; } - MOD+CTRL+WHEELSCROLLDOWN cooldown-ms=150 { move-column-to-workspace-down; } - MOD+CTRL+WHEELSCROLLUP cooldown-ms=150 { move-column-to-workspace-up; } - - MOD+WHEELSCROLLRIGHT { focus-column-right; } - MOD+WHEELSCROLLLEFT { focus-column-left; } - MOD+CTRL+WHEELSCROLLRIGHT { move-column-right; } - MOD+CTRL+WHEELSCROLLLEFT { move-column-left; } - - MOD+SHIFT+WHEELSCROLLDOWN { focus-column-right; } - MOD+SHIFT+WHEELSCROLLUP { focus-column-left; } - MOD+CTRL+SHIFT+WHEELSCROLLDOWN { move-column-right; } - MOD+CTRL+SHIFT+WHEELSCROLLUP { move-column-left; } - - MOD+1 { focus-workspace 1; } - MOD+2 { focus-workspace 2; } - MOD+3 { focus-workspace 3; } - MOD+4 { focus-workspace 4; } - MOD+5 { focus-workspace 5; } - MOD+6 { focus-workspace 6; } - MOD+7 { focus-workspace 7; } - MOD+8 { focus-workspace 8; } - MOD+9 { focus-workspace 9; } - - MOD+CTRL+1 { move-column-to-workspace 1; } - MOD+CTRL+2 { move-column-to-workspace 2; } - MOD+CTRL+3 { move-column-to-workspace 3; } - MOD+CTRL+4 { move-column-to-workspace 4; } - MOD+CTRL+5 { move-column-to-workspace 5; } - MOD+CTRL+6 { move-column-to-workspace 6; } - MOD+CTRL+7 { move-column-to-workspace 7; } - MOD+CTRL+8 { move-column-to-workspace 8; } - MOD+CTRL+9 { move-column-to-workspace 9; } - - MOD+TAB { focus-workspace-previous; } - - // ─── Layout Controls ─── - MOD+CTRL+F { expand-column-to-available-width; } - MOD+C { center-column; } - MOD+CTRL+C { center-visible-columns; } - MOD+MINUS { set-column-width "-10%"; } - MOD+EQUAL { set-column-width "+10%"; } - MOD+SHIFT+MINUS { set-window-height "-10%"; } - MOD+SHIFT+EQUAL { set-window-height "+10%"; } - - // ─── Modes ─── - MOD+T { toggle-window-floating; } - MOD+F { fullscreen-window; } - MOD+W { toggle-column-tabbed-display; } - - // ─── Screenshots ─── - CTRL+SHIFT+1 { screenshot; } - CTRL+SHIFT+2 { screenshot-screen; } - CTRL+SHIFT+3 { screenshot-window; } - - // ─── Emergency Escape Key ─── - // Use this when a fullscreen app blocks your keybinds. - // It disables any active keyboard shortcut inhibitor, restoring control. - MOD+ESCAPE allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; } - - // ─── Exit / Power ─── - CTRL+ALT+DELETE { quit; } // Also quits Niri - MOD+SHIFT+P { power-off-monitors; } // Turn off screens (useful for OLED or privacy) - MOD+O repeat=false { toggle-overview; } -} - -// ────────────── Startup Applications ────────────── -// https://github.com/YaLTeR/niri/wiki/Configuration:-Miscellaneous#spawn-at-startup - - spawn-at-startup "/usr/lib/polkit-kde-authentication-agent-1" "&" // Polkit - spawn-at-startup "xwayland-satellite" // XWayland support - spawn-at-startup "swww-daemon" // Wallpaper daemon - spawn-at-startup "swww img" "/usr/share/wallpapers/cachyos-wallpapers/Skyscraper.png" // Set wallpaper - spawn-at-startup "qs" // Launch Quickshell - spawn-at-startup "vesktop" // Launch Vesktop - - prefer-no-csd // Disable program decorations - screenshot-path null // Disable screenshot saving - -// ────────────── Layout Settings ────────────── -// https://github.com/YaLTeR/niri/wiki/Configuration:-Layout - - layout { - gaps 16 // Gap between windows - center-focused-column "never" // Don’t auto-center focused column - - preset-column-widths { - proportion 0.33333 - proportion 0.5 - proportion 0.66667 - } - - focus-ring { - width 3 - active-color "{{ color4 }}" - inactive-color "{{ color0 }}" - } - - shadow { - softness 30 - spread 5 - offset x=0 y=5 - color "#0007" - } - - background-color "transparent" - - struts {} - } - -// ────────────── Animation Settings ────────────── -// https://github.com/YaLTeR/niri/wiki/Configuration:-Animations - animations { - workspace-switch { - spring damping-ratio=1.0 stiffness=1000 epsilon=0.0001 - } - window-open { - duration-ms 200 - curve "ease-out-quad" - } - window-close { - duration-ms 200 - curve "ease-out-cubic" - } - horizontal-view-movement { - spring damping-ratio=1.0 stiffness=900 epsilon=0.0001 - } - window-movement { - spring damping-ratio=1.0 stiffness=800 epsilon=0.0001 - } - window-resize { - spring damping-ratio=1.0 stiffness=1000 epsilon=0.0001 - } - config-notification-open-close { - spring damping-ratio=0.6 stiffness=1200 epsilon=0.001 - } - screenshot-ui-open { - duration-ms 300 - curve "ease-out-quad" - } - overview-open-close { - spring damping-ratio=1.0 stiffness=900 epsilon=0.0001 - } - } - -// ────────────── Named Workspaces ────────────── -// https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules - - workspace "browser" - workspace "chat" - -// ────────────── Window Rules ────────────── -// https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules - - window-rule { - match at-startup=true app-id="vesktop" - open-on-workspace "chat" - open-maximized true - } - - window-rule { - match app-id="firefox" - open-on-workspace "browser" - open-maximized true - } - - window-rule { - match app-id=r#"firefox$"# title="^Picture-in-Picture$" - open-floating true // Always float Firefox PiP windows - } - - window-rule { - geometry-corner-radius 20 // Set every window radius to 20 - clip-to-geometry true - } - -// ────────────── Layer Rules ────────────── -// https://github.com/YaLTeR/niri/wiki/Configuration:-Layer-Rules - - layer-rule { - match namespace="^swww-daemon$" - place-within-backdrop true - } - -// ────────────── Environment Variables ────────────── -// https://github.com/YaLTeR/niri/wiki/Configuration:-Miscellaneous#environment - - environment { - DISPLAY ":1" - ELECTRON_OZONE_PLATFORM_HINT "auto" - QT_QPA_PLATFORM "wayland" - QT_WAYLAND_DISABLE_WINDOWDECORATION "1" - XDG_SESSION_TYPE "wayland" - XDG_CURRENT_DESKTOP "niri" - } - -// ────────────── Misc ────────────── -hotkey-overlay { - skip-at-startup -} \ No newline at end of file From c7a07127d8c2af18558e785aba7e241bcbde72b4 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 10 Aug 2025 23:52:31 -0400 Subject: [PATCH 093/394] Fixed a bunch of border.width who used to have inverted logic (math.min vs max) --- Modules/Audio/AudioDeviceSelector.qml | 6 ++---- Modules/Calendar/Calendar.qml | 2 +- Modules/DemoPanel/DemoPanel.qml | 2 +- Modules/Notification/Notification.qml | 4 ++-- Modules/SidePanel/SidePanel.qml | 2 +- Widgets/NBox.qml | 2 +- Widgets/NCard.qml | 2 +- Widgets/NCircleStat.qml | 4 ++-- Widgets/NSlider.qml | 2 +- Widgets/NToggle.qml | 4 ++-- Widgets/NTooltip.qml | 2 +- 11 files changed, 15 insertions(+), 17 deletions(-) diff --git a/Modules/Audio/AudioDeviceSelector.qml b/Modules/Audio/AudioDeviceSelector.qml index 460b297..ded1457 100644 --- a/Modules/Audio/AudioDeviceSelector.qml +++ b/Modules/Audio/AudioDeviceSelector.qml @@ -22,7 +22,7 @@ NLoader { color: Colors.backgroundPrimary radius: Style.radiusMedium * scaling border.color: Colors.backgroundTertiary - border.width: Math.min(1, Style.borderMedium * scaling) + border.width: Math.max(1, Style.borderMedium * scaling) width: 500 * scaling height: 400 anchors.centerIn: parent @@ -38,9 +38,7 @@ NLoader { } } } -} // NPanel {// id: ioSelector// property int tabIndex: 0// property Item anchorItem: null// signal panelClosed()// function sinkNodes() {// let nodes = Pipewire.nodes && Pipewire.nodes.values ? Pipewire.nodes.values.filter(function(n) {// return n.isSink && n.audio && n.isStream === false;// }) : [];// if (Pipewire.defaultAudioSink) -// nodes = nodes.slice().sort(function(a, b) { -// if (a.id === Pipewire.defaultAudioSink.id) +} // NPanel {// id: ioSelector// property int tabIndex: 0// property Item anchorItem: null// signal panelClosed()// function sinkNodes() {// let nodes = Pipewire.nodes && Pipewire.nodes.values ? Pipewire.nodes.values.filter(function(n) {// return n.isSink && n.audio && n.isStream === false;// }) : [];// if (Pipewire.defaultAudioSink)// nodes = nodes.slice().sort(function(a, b) {// if (a.id === Pipewire.defaultAudioSink.id) // return -1; // if (b.id === Pipewire.defaultAudioSink.id) diff --git a/Modules/Calendar/Calendar.qml b/Modules/Calendar/Calendar.qml index ca60424..6768593 100644 --- a/Modules/Calendar/Calendar.qml +++ b/Modules/Calendar/Calendar.qml @@ -19,7 +19,7 @@ NLoader { color: Colors.backgroundSecondary radius: Style.radiusMedium * scaling border.color: Colors.backgroundTertiary - border.width: Math.min(1, Style.borderMedium * scaling) + border.width: Math.max(1, Style.borderMedium * scaling) width: 340 * scaling height: 320 // TBC anchors.top: parent.top diff --git a/Modules/DemoPanel/DemoPanel.qml b/Modules/DemoPanel/DemoPanel.qml index 6e46687..b33d0dd 100644 --- a/Modules/DemoPanel/DemoPanel.qml +++ b/Modules/DemoPanel/DemoPanel.qml @@ -27,7 +27,7 @@ NLoader { color: Colors.backgroundPrimary radius: Style.radiusMedium * scaling border.color: Colors.backgroundTertiary - border.width: Math.min(1, Style.borderMedium * scaling) + border.width: Math.max(1, Style.borderMedium * scaling) width: 500 * scaling height: 400 * scaling anchors.centerIn: parent diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index 7ff94fa..9926689 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -64,8 +64,8 @@ PanelWindow { clip: true color: Colors.backgroundSecondary radius: Style.radiusMedium * scaling - border.color: Colors.backgroundTertiary - border.width: Math.min(1, Style.borderThin * scaling) + border.color: Colors.accentTertiary + border.width: Math.max(1, Style.borderThin * scaling) // Animation properties property real scaleValue: 0.8 diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml index de1a5c1..9e155b6 100644 --- a/Modules/SidePanel/SidePanel.qml +++ b/Modules/SidePanel/SidePanel.qml @@ -50,7 +50,7 @@ NLoader { color: Colors.backgroundPrimary radius: Style.radiusLarge * scaling border.color: Colors.backgroundTertiary - border.width: Math.min(1, Style.borderMedium * scaling) + border.width: Math.max(1, Style.borderMedium * scaling) layer.enabled: true width: 460 * scaling property real innerMargin: sidePanel.cardSpacing diff --git a/Widgets/NBox.qml b/Widgets/NBox.qml index aa5a28d..9281bf2 100644 --- a/Widgets/NBox.qml +++ b/Widgets/NBox.qml @@ -14,6 +14,6 @@ Rectangle { color: Colors.surfaceVariant radius: Style.radiusMedium * scaling border.color: Colors.backgroundTertiary - border.width: Math.min(1, Style.borderThin * scaling) + border.width: Math.max(1, Style.borderThin * scaling) clip: true } diff --git a/Widgets/NCard.qml b/Widgets/NCard.qml index d0ff71d..bc2bb40 100644 --- a/Widgets/NCard.qml +++ b/Widgets/NCard.qml @@ -13,5 +13,5 @@ Rectangle { color: Colors.backgroundSecondary radius: Style.radiusMedium * scaling border.color: Colors.backgroundTertiary - border.width: Math.min(1, Style.borderThin * scaling) + border.width: Math.max(1, Style.borderThin * scaling) } diff --git a/Widgets/NCircleStat.qml b/Widgets/NCircleStat.qml index 6ef5eaa..a0a740f 100644 --- a/Widgets/NCircleStat.qml +++ b/Widgets/NCircleStat.qml @@ -21,7 +21,7 @@ Rectangle { color: flat ? "transparent" : Colors.backgroundSecondary radius: Style.radiusSmall * scaling border.color: flat ? "transparent" : Colors.backgroundTertiary - border.width: flat ? 0 : Math.min(1, Style.borderThin * scaling) + border.width: flat ? 0 : Math.max(1, Style.borderThin * scaling) clip: true // Repaint gauge when the bound value changes @@ -90,7 +90,7 @@ Rectangle { radius: width / 2 color: Colors.backgroundPrimary border.color: Colors.accentPrimary - border.width: Math.min(1, Style.borderThin * scaling) + border.width: Math.max(1, Style.borderThin * scaling) anchors.right: parent.right anchors.bottom: parent.bottom anchors.rightMargin: 4 * scaling * contentScale diff --git a/Widgets/NSlider.qml b/Widgets/NSlider.qml index 60a8367..3afc394 100644 --- a/Widgets/NSlider.qml +++ b/Widgets/NSlider.qml @@ -83,7 +83,7 @@ Slider { radius: width * 0.5 color: root.pressed ? Colors.surfaceVariant : Colors.surface border.color: Colors.accentPrimary - border.width: Math.min(1, Style.borderThick * scaling) + border.width: Math.max(1, Style.borderThick * scaling) // Press feedback halo (using accent color, low opacity) Rectangle { anchors.centerIn: parent diff --git a/Widgets/NToggle.qml b/Widgets/NToggle.qml index 2da7606..3318bf8 100644 --- a/Widgets/NToggle.qml +++ b/Widgets/NToggle.qml @@ -43,7 +43,7 @@ RowLayout { radius: height * 0.5 color: value ? Colors.accentPrimary : Colors.surfaceVariant border.color: value ? Colors.accentPrimary : Colors.outline - border.width: Math.min(1, Style.borderMedium * scaling) + border.width: Math.max(1, Style.borderMedium * scaling) Rectangle { width: (Style.baseWidgetSize - 4) * scaling @@ -51,7 +51,7 @@ RowLayout { radius: height * 0.5 color: Colors.surface border.color: hovering ? Colors.textDisabled : Colors.outline - border.width: Math.min(1, Style.borderMedium * scaling) + border.width: Math.max(1, Style.borderMedium * scaling) y: 2 * scaling x: value ? switcher.width - width - 2 * scaling : 2 * scaling diff --git a/Widgets/NTooltip.qml b/Widgets/NTooltip.qml index 99efe7f..7d6bda2 100644 --- a/Widgets/NTooltip.qml +++ b/Widgets/NTooltip.qml @@ -102,7 +102,7 @@ Window { radius: Style.radiusMedium * scaling color: Colors.backgroundTertiary border.color: Colors.outline - border.width: Math.min(1, Style.borderThin * scaling) + border.width: Math.max(1, Style.borderThin * scaling) opacity: Style.opacityFull z: 1 } From b2fb868cd7f3e3ac6864eaa885546277e60baf10 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Mon, 11 Aug 2025 13:02:44 +0200 Subject: [PATCH 094/394] Small fixes for Notification --- Modules/Background/Background.qml | 1 + Modules/Notification/Notification.qml | 6 +++--- Services/NotificationService.qml | 6 +++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Modules/Background/Background.qml b/Modules/Background/Background.qml index 551c2d3..5993f04 100644 --- a/Modules/Background/Background.qml +++ b/Modules/Background/Background.qml @@ -9,6 +9,7 @@ Variants { delegate: PanelWindow { required property ShellScreen modelData property string wallpaperSource: Qt.resolvedUrl("../../Assets/Tests/wallpaper.png") + //property string wallpaperSource: Qt.resolvedUrl("/home/lysec/Pictures/wallpapers/wallhaven-6lqvql.jpg") visible: wallpaperSource !== "" color: "transparent" diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index 9926689..83fdfa8 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -17,8 +17,8 @@ PanelWindow { visible: notificationService.notificationModel.count > 0 anchors.top: true anchors.right: true - margins.top: (Style.barHeight + 10) * scaling - margins.right: 10 * scaling + margins.top: (Style.barHeight + Style.marginMedium) * scaling + margins.right: Style.marginMedium * scaling implicitWidth: 360 * scaling implicitHeight: Math.min(notificationStack.implicitHeight, (notificationService.maxVisible * 120) * scaling) WlrLayershell.layer: WlrLayer.Overlay @@ -64,7 +64,7 @@ PanelWindow { clip: true color: Colors.backgroundSecondary radius: Style.radiusMedium * scaling - border.color: Colors.accentTertiary + border.color: Colors.accentPrimary border.width: Math.max(1, Style.borderThin * scaling) // Animation properties diff --git a/Services/NotificationService.qml b/Services/NotificationService.qml index 086fecf..74533bc 100644 --- a/Services/NotificationService.qml +++ b/Services/NotificationService.qml @@ -45,7 +45,7 @@ QtObject { // Auto-hide timer property Timer hideTimer: Timer { - interval: 5000 // 5 seconds + interval: 8000 // 8 seconds - longer display time repeat: true running: notificationModel.count > 0 @@ -54,9 +54,9 @@ QtObject { return } - // Always remove the oldest notification (last in the list) + // Remove the oldest notification (last in the list) let oldestNotification = notificationModel.get(notificationModel.count - 1).rawNotification - if (oldestNotification && !oldestNotification.transient) { + if (oldestNotification) { // Trigger animation signal instead of direct dismiss animateAndRemove(oldestNotification, notificationModel.count - 1) } From c8aec206f20ba4c0f9fa6f19715d5672c1b0d91c Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Mon, 11 Aug 2025 13:53:53 +0200 Subject: [PATCH 095/394] Fix for Calendar days --- Modules/Calendar/Calendar.qml | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/Modules/Calendar/Calendar.qml b/Modules/Calendar/Calendar.qml index 6768593..47a47a8 100644 --- a/Modules/Calendar/Calendar.qml +++ b/Modules/Calendar/Calendar.qml @@ -75,20 +75,29 @@ NLoader { Layout.fillWidth: true } - // Columns label (Sunday to Saturday) - DayOfWeekRow { + // Columns label (Monday to Sunday) + RowLayout { Layout.fillWidth: true - spacing: 0 Layout.leftMargin: Style.marginSmall * scaling // Align with grid Layout.rightMargin: Style.marginSmall * scaling + spacing: 0 - delegate: NText { - text: shortName - color: Colors.accentSecondary - font.pointSize: Style.fontSizeMedium * scaling - font.weight: Style.fontWeightBold - horizontalAlignment: Text.AlignHCenter - width: Style.baseWidgetSize * scaling + Repeater { + model: 7 + + NText { + text: { + // Start with Monday (1) instead of Sunday (0) + let dayIndex = (index + 1) % 7 + return Qt.locale().dayName(dayIndex, Locale.ShortFormat) + } + color: Colors.accentSecondary + font.pointSize: Style.fontSizeMedium * scaling + font.weight: Style.fontWeightBold + horizontalAlignment: Text.AlignHCenter + Layout.fillWidth: true + Layout.preferredWidth: Style.baseWidgetSize * scaling + } } } @@ -102,6 +111,7 @@ NLoader { spacing: 0 month: Time.date.getMonth() year: Time.date.getFullYear() + locale: Qt.locale() // Use system locale // Optionally, update when the panel becomes visible Connections { @@ -140,4 +150,4 @@ NLoader { } } } -} +} \ No newline at end of file From ccd5fbba3af893616527241fb64c3a1a71cd34fb Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Mon, 11 Aug 2025 13:58:52 +0200 Subject: [PATCH 096/394] Possible calendar locale fix --- Modules/Calendar/Calendar.qml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Modules/Calendar/Calendar.qml b/Modules/Calendar/Calendar.qml index 47a47a8..ae404f8 100644 --- a/Modules/Calendar/Calendar.qml +++ b/Modules/Calendar/Calendar.qml @@ -21,7 +21,7 @@ NLoader { border.color: Colors.backgroundTertiary border.width: Math.max(1, Style.borderMedium * scaling) width: 340 * scaling - height: 320 // TBC + height: 380 * scaling // Scale the height and make it larger anchors.top: parent.top anchors.right: parent.right anchors.topMargin: Style.marginTiny * scaling @@ -75,7 +75,7 @@ NLoader { Layout.fillWidth: true } - // Columns label (Monday to Sunday) + // Columns label (respects locale's first day of week) RowLayout { Layout.fillWidth: true Layout.leftMargin: Style.marginSmall * scaling // Align with grid @@ -87,8 +87,9 @@ NLoader { NText { text: { - // Start with Monday (1) instead of Sunday (0) - let dayIndex = (index + 1) % 7 + // Use the locale's first day of week setting + let firstDay = Qt.locale().firstDayOfWeek + let dayIndex = (firstDay + index) % 7 return Qt.locale().dayName(dayIndex, Locale.ShortFormat) } color: Colors.accentSecondary @@ -125,8 +126,8 @@ NLoader { } delegate: Rectangle { - width: Style.baseWidgetSize * scaling - height: Style.baseWidgetSize * scaling + width: (Style.baseWidgetSize * scaling) + height: (Style.baseWidgetSize * scaling) radius: Style.radiusSmall * scaling color: model.today ? Colors.accentPrimary : "transparent" @@ -135,7 +136,7 @@ NLoader { text: model.day color: model.today ? Colors.onAccent : Colors.textPrimary opacity: model.month === grid.month ? Style.opacityHeavy : Style.opacityLight - font.pointSize: Style.fontSizeMedium * scaling + font.pointSize: (Style.fontSizeMedium * scaling) font.weight: model.today ? Style.fontWeightBold : Style.fontWeightRegular } From 6c2b1cdfb1fcf20a447c76859069599442e6271d Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Mon, 11 Aug 2025 14:11:17 +0200 Subject: [PATCH 097/394] Fix calendar spacing --- Modules/Calendar/Calendar.qml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Modules/Calendar/Calendar.qml b/Modules/Calendar/Calendar.qml index ae404f8..22e7d6c 100644 --- a/Modules/Calendar/Calendar.qml +++ b/Modules/Calendar/Calendar.qml @@ -21,7 +21,7 @@ NLoader { border.color: Colors.backgroundTertiary border.width: Math.max(1, Style.borderMedium * scaling) width: 340 * scaling - height: 380 * scaling // Scale the height and make it larger + height: 320 * scaling // Reduced height to eliminate bottom space anchors.top: parent.top anchors.right: parent.right anchors.topMargin: Style.marginTiny * scaling @@ -36,11 +36,13 @@ NLoader { ColumnLayout { anchors.fill: parent anchors.margins: Style.marginMedium * scaling - spacing: Style.marginMedium * scaling + spacing: Style.marginTiny * scaling // Header: Month/Year with navigation RowLayout { Layout.fillWidth: true + Layout.leftMargin: Style.marginMedium * scaling + Layout.rightMargin: Style.marginMedium * scaling spacing: Style.marginSmall * scaling NIconButton { @@ -71,8 +73,11 @@ NLoader { } } + // Divider between header and weekdays NDivider { Layout.fillWidth: true + Layout.topMargin: Style.marginSmall * scaling + Layout.bottomMargin: Style.marginMedium * scaling } // Columns label (respects locale's first day of week) @@ -107,6 +112,7 @@ NLoader { id: grid Layout.fillWidth: true + Layout.fillHeight: true // Take remaining space Layout.leftMargin: Style.marginSmall * scaling Layout.rightMargin: Style.marginSmall * scaling spacing: 0 From f29fd7b5551219a97926080988324e1af06d075f Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 08:18:57 -0400 Subject: [PATCH 098/394] TrayMenu: improved look + fixed error --- Modules/Bar/TrayMenu.qml | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Modules/Bar/TrayMenu.qml b/Modules/Bar/TrayMenu.qml index 37f8d51..4e3637f 100644 --- a/Modules/Bar/TrayMenu.qml +++ b/Modules/Bar/TrayMenu.qml @@ -98,7 +98,8 @@ PopupWindow { required property var modelData width: listView.width - height: (modelData?.isSeparator) ? 8 * scaling : 32 * scaling + height: (modelData?.isSeparator) ? 8 * scaling : Math.max(32 * scaling, + text.height + 8) color: "transparent" property var subMenu: null @@ -123,6 +124,7 @@ PopupWindow { spacing: Style.marginSmall * scaling NText { + id: text Layout.fillWidth: true color: (modelData?.enabled ?? true) ? (mouseArea.containsMouse ? Colors.onAccent : Colors.textPrimary) : Colors.textDisabled @@ -144,7 +146,7 @@ PopupWindow { Text { text: modelData?.hasChildren ? "menu" : "" font.family: "Material Symbols Outlined" - font.pointSize: Colors.fontSizeMedium * scaling + font.pointSize: Colors.fontSizeSmall * scaling verticalAlignment: Text.AlignVCenter visible: modelData?.hasChildren ?? false color: Colors.textPrimary @@ -307,7 +309,7 @@ PopupWindow { Rectangle { id: bg anchors.fill: parent - color: Colors.backgroundPrimary + color: Colors.backgroundSecondary border.color: Colors.outline border.width: Math.max(1, Style.borderThin * scaling) radius: Style.radiusMedium * scaling @@ -332,7 +334,8 @@ PopupWindow { required property var modelData width: listView.width - height: (modelData?.isSeparator) ? 8 * scaling : 32 * scaling + height: (modelData?.isSeparator) ? 8 * scaling : Math.max(32 * scaling, + subText.height + 8) color: "transparent" property var subMenu: null @@ -358,6 +361,7 @@ PopupWindow { spacing: Style.spacingSmall * scaling NText { + id: subText Layout.fillWidth: true color: (modelData?.enabled ?? true) ? bg.hoverTextColor : Colors.textDisabled text: modelData?.text ?? "" @@ -378,7 +382,7 @@ PopupWindow { NText { text: modelData?.hasChildren ? "\uE5CC" : "" font.family: "Material Symbols Outlined" - font.pointSize: Colors.fontSizeMedium * scaling + font.pointSize: Colors.fontSizeSmall * scaling verticalAlignment: Text.AlignVCenter visible: modelData?.hasChildren ?? false } @@ -388,7 +392,7 @@ PopupWindow { id: mouseArea anchors.fill: parent hoverEnabled: true - enabled: (modelData?.enabled ?? true) && !(modelData?.isSeparator ?? false) && subMenu.visible + enabled: (modelData?.enabled ?? true) && !(modelData?.isSeparator ?? false) onClicked: { if (modelData && !modelData.isSeparator) { @@ -401,7 +405,7 @@ PopupWindow { } onEntered: { - if (!subMenu.visible) { + if (subMenu && !subMenu.visible) { return } From a72afcafbbe46ed7f9bec91b667d2cfe341c8517 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 08:40:28 -0400 Subject: [PATCH 099/394] Notification: subtle gradient --- Modules/Notification/Notification.qml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index 83fdfa8..53f4222 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -62,10 +62,13 @@ PanelWindow { width: 360 * scaling height: Math.max(80 * scaling, contentColumn.implicitHeight + (Style.marginMedium * 2 * scaling)) clip: true - color: Colors.backgroundSecondary radius: Style.radiusMedium * scaling border.color: Colors.accentPrimary border.width: Math.max(1, Style.borderThin * scaling) + gradient: Gradient { + GradientStop { position: 0.0; color: Colors.backgroundTertiary } + GradientStop { position: 1.0; color: Colors.backgroundSecondary } + } // Animation properties property real scaleValue: 0.8 From 4fd97799413423e0e6f8ac48489e9511ed894282 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 08:40:55 -0400 Subject: [PATCH 100/394] formatting --- Modules/Audio/AudioDeviceSelector.qml | 3 +-- Modules/Background/Background.qml | 2 +- Modules/Bar/TrayMenu.qml | 6 ++---- Modules/Calendar/Calendar.qml | 8 ++++---- Modules/Notification/Notification.qml | 12 +++++++++--- 5 files changed, 17 insertions(+), 14 deletions(-) diff --git a/Modules/Audio/AudioDeviceSelector.qml b/Modules/Audio/AudioDeviceSelector.qml index ded1457..d8253c2 100644 --- a/Modules/Audio/AudioDeviceSelector.qml +++ b/Modules/Audio/AudioDeviceSelector.qml @@ -38,8 +38,7 @@ NLoader { } } } -} // NPanel {// id: ioSelector// property int tabIndex: 0// property Item anchorItem: null// signal panelClosed()// function sinkNodes() {// let nodes = Pipewire.nodes && Pipewire.nodes.values ? Pipewire.nodes.values.filter(function(n) {// return n.isSink && n.audio && n.isStream === false;// }) : [];// if (Pipewire.defaultAudioSink)// nodes = nodes.slice().sort(function(a, b) {// if (a.id === Pipewire.defaultAudioSink.id) -// return -1; +} // NPanel {// id: ioSelector// property int tabIndex: 0// property Item anchorItem: null// signal panelClosed()// function sinkNodes() {// let nodes = Pipewire.nodes && Pipewire.nodes.values ? Pipewire.nodes.values.filter(function(n) {// return n.isSink && n.audio && n.isStream === false;// }) : [];// if (Pipewire.defaultAudioSink)// nodes = nodes.slice().sort(function(a, b) {// if (a.id === Pipewire.defaultAudioSink.id)// return -1; // if (b.id === Pipewire.defaultAudioSink.id) // return 1; diff --git a/Modules/Background/Background.qml b/Modules/Background/Background.qml index 5993f04..e6216df 100644 --- a/Modules/Background/Background.qml +++ b/Modules/Background/Background.qml @@ -9,8 +9,8 @@ Variants { delegate: PanelWindow { required property ShellScreen modelData property string wallpaperSource: Qt.resolvedUrl("../../Assets/Tests/wallpaper.png") - //property string wallpaperSource: Qt.resolvedUrl("/home/lysec/Pictures/wallpapers/wallhaven-6lqvql.jpg") + //property string wallpaperSource: Qt.resolvedUrl("/home/lysec/Pictures/wallpapers/wallhaven-6lqvql.jpg") visible: wallpaperSource !== "" color: "transparent" screen: modelData diff --git a/Modules/Bar/TrayMenu.qml b/Modules/Bar/TrayMenu.qml index 4e3637f..953f075 100644 --- a/Modules/Bar/TrayMenu.qml +++ b/Modules/Bar/TrayMenu.qml @@ -98,8 +98,7 @@ PopupWindow { required property var modelData width: listView.width - height: (modelData?.isSeparator) ? 8 * scaling : Math.max(32 * scaling, - text.height + 8) + height: (modelData?.isSeparator) ? 8 * scaling : Math.max(32 * scaling, text.height + 8) color: "transparent" property var subMenu: null @@ -334,8 +333,7 @@ PopupWindow { required property var modelData width: listView.width - height: (modelData?.isSeparator) ? 8 * scaling : Math.max(32 * scaling, - subText.height + 8) + height: (modelData?.isSeparator) ? 8 * scaling : Math.max(32 * scaling, subText.height + 8) color: "transparent" property var subMenu: null diff --git a/Modules/Calendar/Calendar.qml b/Modules/Calendar/Calendar.qml index 22e7d6c..ad05d47 100644 --- a/Modules/Calendar/Calendar.qml +++ b/Modules/Calendar/Calendar.qml @@ -21,7 +21,7 @@ NLoader { border.color: Colors.backgroundTertiary border.width: Math.max(1, Style.borderMedium * scaling) width: 340 * scaling - height: 320 * scaling // Reduced height to eliminate bottom space + height: 320 * scaling // Reduced height to eliminate bottom space anchors.top: parent.top anchors.right: parent.right anchors.topMargin: Style.marginTiny * scaling @@ -89,7 +89,7 @@ NLoader { Repeater { model: 7 - + NText { text: { // Use the locale's first day of week setting @@ -112,7 +112,7 @@ NLoader { id: grid Layout.fillWidth: true - Layout.fillHeight: true // Take remaining space + Layout.fillHeight: true // Take remaining space Layout.leftMargin: Style.marginSmall * scaling Layout.rightMargin: Style.marginSmall * scaling spacing: 0 @@ -157,4 +157,4 @@ NLoader { } } } -} \ No newline at end of file +} diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index 53f4222..11bfa0f 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -66,9 +66,15 @@ PanelWindow { border.color: Colors.accentPrimary border.width: Math.max(1, Style.borderThin * scaling) gradient: Gradient { - GradientStop { position: 0.0; color: Colors.backgroundTertiary } - GradientStop { position: 1.0; color: Colors.backgroundSecondary } - } + GradientStop { + position: 0.0 + color: Colors.backgroundTertiary + } + GradientStop { + position: 1.0 + color: Colors.backgroundSecondary + } + } // Animation properties property real scaleValue: 0.8 From ee11c40ad3b1ca93a8b9adae86c3654891e93fe6 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 08:45:22 -0400 Subject: [PATCH 101/394] notification changed easing so it does not clip --- Modules/Notification/Notification.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index 11bfa0f..324387a 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -120,7 +120,8 @@ PanelWindow { Behavior on scale { NumberAnimation { duration: Style.animationSlow - easing.type: Easing.OutBack + easing.type: Easing.OutExpo + //easing.type: Easing.OutBack looks better but notification get clipped on all sides } } From b62bdcf985d13471c3b9bf33ca1e035ba41a722a Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 09:28:52 -0400 Subject: [PATCH 102/394] Bar: avoid an error when a monitor suddenly disappear (Turned off or KVM switched) --- Modules/Bar/Bar.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 6dd0d8b..ae9fc24 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -17,7 +17,7 @@ Variants { screen: modelData implicitHeight: Style.barHeight * scaling color: "transparent" - visible: Settings.data.bar.monitors.includes(modelData.name) || (Settings.data.bar.monitors.length === 0) + visible: modelData ? (Settings.data.bar.monitors.includes(modelData.name) || (Settings.data.bar.monitors.length === 0)) : false anchors { top: true From 5543b58c3ccc34d077ce4f95d9f6363b7a4bcbab Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 09:41:28 -0400 Subject: [PATCH 103/394] Formatting --- Modules/Bar/Bar.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index ae9fc24..c5cb0ed 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -17,7 +17,8 @@ Variants { screen: modelData implicitHeight: Style.barHeight * scaling color: "transparent" - visible: modelData ? (Settings.data.bar.monitors.includes(modelData.name) || (Settings.data.bar.monitors.length === 0)) : false + visible: modelData ? (Settings.data.bar.monitors.includes(modelData.name) + || (Settings.data.bar.monitors.length === 0)) : false anchors { top: true From dc621647d7aa9fa0409a9d9eacb7c96e45e1d8c6 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Mon, 11 Aug 2025 15:49:48 +0200 Subject: [PATCH 104/394] Early Progress of SettingsWindow --- Modules/DemoPanel/DemoPanel.qml | 4 - Modules/Settings/SettingsWindow.qml | 179 +++++++++++++++++++++++ Modules/Settings/Tabs/About.qml | 18 +++ Modules/Settings/Tabs/Bar.qml | 19 +++ Modules/Settings/Tabs/Display.qml | 18 +++ Modules/Settings/Tabs/General.qml | 89 +++++++++++ Modules/Settings/Tabs/Misc.qml | 18 +++ Modules/Settings/Tabs/Network.qml | 18 +++ Modules/Settings/Tabs/ScreenRecorder.qml | 18 +++ Modules/Settings/Tabs/TimeWeather.qml | 18 +++ Modules/Settings/Tabs/Wallpaper.qml | 18 +++ Modules/SidePanel/ProfileCard.qml | 19 +++ Services/Settings.qml | 1 + Widgets/NTextBox.qml | 66 +++++++++ 14 files changed, 499 insertions(+), 4 deletions(-) create mode 100644 Modules/Settings/SettingsWindow.qml create mode 100644 Modules/Settings/Tabs/About.qml create mode 100644 Modules/Settings/Tabs/Bar.qml create mode 100644 Modules/Settings/Tabs/Display.qml create mode 100644 Modules/Settings/Tabs/General.qml create mode 100644 Modules/Settings/Tabs/Misc.qml create mode 100644 Modules/Settings/Tabs/Network.qml create mode 100644 Modules/Settings/Tabs/ScreenRecorder.qml create mode 100644 Modules/Settings/Tabs/TimeWeather.qml create mode 100644 Modules/Settings/Tabs/Wallpaper.qml create mode 100644 Widgets/NTextBox.qml diff --git a/Modules/DemoPanel/DemoPanel.qml b/Modules/DemoPanel/DemoPanel.qml index b33d0dd..0877655 100644 --- a/Modules/DemoPanel/DemoPanel.qml +++ b/Modules/DemoPanel/DemoPanel.qml @@ -6,10 +6,6 @@ import Quickshell.Wayland import qs.Services import qs.Widgets - -/* - An experiment/demo panel to tweaks widgets -*/ NLoader { id: root diff --git a/Modules/Settings/SettingsWindow.qml b/Modules/Settings/SettingsWindow.qml new file mode 100644 index 0000000..389e239 --- /dev/null +++ b/Modules/Settings/SettingsWindow.qml @@ -0,0 +1,179 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import qs.Services +import qs.Widgets +import "Tabs" as Tabs + +NLoader { + id: root + + content: Component { + NPanel { + id: settingsPanel + + readonly property real scaling: Scaling.scale(screen) + // Single source of truth for tabs + // Each tab points to a QML file path. The content stack simply loads the file via Loader.source. + property var tabsModel: [ + { icon: "tune", label: "General", source: "Tabs/General.qml" }, + { icon: "web_asset", label: "Bar", source: "Tabs/Bar.qml" }, + { icon: "schedule", label: "Time & Weather", source: "Tabs/TimeWeather.qml" }, + { icon: "videocam", label: "Screen Recorder", source: "Tabs/ScreenRecorder.qml" }, + { icon: "wifi", label: "Network", source: "Tabs/Network.qml" }, + { icon: "monitor", label: "Display", source: "Tabs/Display.qml" }, + { icon: "image", label: "Wallpaper", source: "Tabs/Wallpaper.qml" }, + { icon: "more_horiz", label: "Misc", source: "Tabs/Misc.qml" }, + { icon: "info", label: "About", source: "Tabs/About.qml" } + ] + + // Always default to the first tab (General) when the panel becomes visible + onVisibleChanged: function () { + if (visible) { + Qt.callLater(function () { + if (typeof stack !== 'undefined' && stack) { + stack.currentIndex = 0 + } + }) + } + } + + // Ensure panel shows itself once created + Component.onCompleted: show() + + Rectangle { + id: bgRect + color: Colors.backgroundPrimary + radius: Style.radiusLarge * scaling + border.color: Colors.backgroundTertiary + border.width: Math.max(1, Style.borderMedium * scaling) + layer.enabled: true + width: 1040 * scaling + height: 640 * scaling + anchors.centerIn: parent + + // Prevent closing when clicking in the panel bg + MouseArea { anchors.fill: parent } + + + // Main two-pane layout + RowLayout { + anchors.fill: parent + anchors.margins: Style.marginLarge * scaling + spacing: Style.marginLarge * scaling + + // Sidebar + Rectangle { + id: sidebar + Layout.preferredWidth: 260 * scaling + Layout.fillHeight: true + radius: Style.radiusMedium * scaling + color: Colors.backgroundSecondary + border.color: Colors.outline + border.width: Math.max(1, Style.borderThin * scaling) + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginSmall * scaling + spacing: Style.marginSmall * scaling + + Repeater { + id: sections + model: settingsPanel.tabsModel + + delegate: Rectangle { + readonly property bool selected: index === stack.currentIndex + Layout.fillWidth: true + height: 44 * scaling + radius: Style.radiusSmall * scaling + color: selected ? Colors.highlight : "transparent" + border.color: Colors.outline + border.width: Math.max(1, Style.borderThin * scaling) + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Style.marginMedium * scaling + anchors.rightMargin: Style.marginMedium * scaling + spacing: Style.marginSmall * scaling + NText { + text: modelData.icon + font.family: "Material Symbols Outlined" + font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 } + color: selected ? Colors.onAccent : Colors.textSecondary + } + NText { text: modelData.label; color: selected ? Colors.onAccent : Colors.textPrimary; Layout.fillWidth: true } + } + MouseArea { anchors.fill: parent; onClicked: stack.currentIndex = index } + } + } + } + } + + // Content + Rectangle { + id: contentPane + Layout.fillWidth: true + Layout.fillHeight: true + radius: Style.radiusMedium * scaling + color: Colors.surface + border.color: Colors.outline + border.width: Math.max(1, Style.borderThin * scaling) + clip: true + + // Content layout: header + divider + pages + ColumnLayout { + id: contentLayout + anchors.fill: parent + anchors.margins: Style.marginLarge * scaling + spacing: Style.marginSmall * scaling + + // Header row + RowLayout { + id: headerRow + Layout.fillWidth: true + spacing: Style.marginSmall * scaling + NText { + text: settingsPanel.tabsModel[stack.currentIndex].label + font.weight: Style.fontWeightBold + color: Colors.textPrimary + Layout.fillWidth: true + } + NIconButton { + id: demoPanelToggle + icon: "close" + tooltipText: "Open demo panel" + Layout.alignment: Qt.AlignVCenter + onClicked: function () { settingsWindow.isLoaded = !settingsWindow.isLoaded } + } + } + + NDivider { Layout.fillWidth: true } + + // Stacked pages + StackLayout { + id: stack + Layout.fillWidth: true + Layout.fillHeight: true + currentIndex: 0 + Component.onCompleted: currentIndex = 0 + + // Pages generated from tabsModel + Repeater { + model: settingsPanel.tabsModel + delegate: Loader { + active: index === stack.currentIndex + visible: active + source: modelData.source + } + } + } + } + } + } + } + } + } +} + diff --git a/Modules/Settings/Tabs/About.qml b/Modules/Settings/Tabs/About.qml new file mode 100644 index 0000000..18142c3 --- /dev/null +++ b/Modules/Settings/Tabs/About.qml @@ -0,0 +1,18 @@ +import QtQuick +import QtQuick.Layouts +import qs.Services +import qs.Widgets + +Item { + property real scaling: 1 + anchors.fill: parent + + ColumnLayout { + anchors.fill: parent + spacing: Style.marginMedium * scaling + NText { text: "About"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + NText { text: "Coming soon"; color: Colors.textSecondary } + Item { Layout.fillHeight: true } + } +} + diff --git a/Modules/Settings/Tabs/Bar.qml b/Modules/Settings/Tabs/Bar.qml new file mode 100644 index 0000000..da8669f --- /dev/null +++ b/Modules/Settings/Tabs/Bar.qml @@ -0,0 +1,19 @@ +import QtQuick +import QtQuick.Layouts +import qs.Services +import qs.Widgets + +Item { + // Optional scaling prop to match other tabs + property real scaling: 1 + anchors.fill: parent + + ColumnLayout { + anchors.fill: parent + spacing: Style.marginMedium * scaling + NText { text: "Bar"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + NText { text: "Coming soon"; color: Colors.textSecondary } + Item { Layout.fillHeight: true } + } +} + diff --git a/Modules/Settings/Tabs/Display.qml b/Modules/Settings/Tabs/Display.qml new file mode 100644 index 0000000..ac2f8e1 --- /dev/null +++ b/Modules/Settings/Tabs/Display.qml @@ -0,0 +1,18 @@ +import QtQuick +import QtQuick.Layouts +import qs.Services +import qs.Widgets + +Item { + property real scaling: 1 + anchors.fill: parent + + ColumnLayout { + anchors.fill: parent + spacing: Style.marginMedium * scaling + NText { text: "Display"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + NText { text: "Coming soon"; color: Colors.textSecondary } + Item { Layout.fillHeight: true } + } +} + diff --git a/Modules/Settings/Tabs/General.qml b/Modules/Settings/Tabs/General.qml new file mode 100644 index 0000000..e0800db --- /dev/null +++ b/Modules/Settings/Tabs/General.qml @@ -0,0 +1,89 @@ +import QtQuick +import QtQuick.Layouts +import qs.Services +import qs.Widgets + +Item { + id: generalPage + + // Public API + // Scaling factor provided by the parent settings window + property real scaling: 1 + + anchors.fill: parent + implicitWidth: parent ? parent.width : 0 + implicitHeight: parent ? parent.height : 0 + + ColumnLayout { + anchors.fill: parent + anchors.margins: 0 + spacing: Style.marginMedium * scaling + + // Profile section + NText { text: "Profile"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginMedium * scaling + + // Avatar preview + Rectangle { + width: 40 * scaling + height: 40 * scaling + radius: 20 * scaling + color: Colors.surfaceVariant + border.color: Colors.outline + border.width: Math.max(1, Style.borderThin * scaling) + Image { + anchors.fill: parent + anchors.margins: 2 * scaling + source: Settings.data.general.avatarImage + fillMode: Image.PreserveAspectCrop + asynchronous: true + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 2 * scaling + NText { text: "Profile Image"; color: Colors.textPrimary; font.weight: Style.fontWeightBold } + NText { text: "Your profile picture displayed in various places throughout the shell"; color: Colors.textSecondary } + NTextBox { + text: Settings.data.general.avatarImage + placeholderText: "/home/user/.face" + Layout.fillWidth: true + onEditingFinished: Settings.data.general.avatarImage = text + } + } + } + + NDivider { Layout.fillWidth: true; Layout.topMargin: Style.marginSmall * scaling; Layout.bottomMargin: Style.marginSmall * scaling } + + // UI section + NText { text: "User Interface"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + + NToggle { + label: "Show Corners" + description: "Display rounded corners on the edge of the screen" + value: Settings.data.general.showScreenCorners + onToggled: function (v) { Settings.data.general.showScreenCorners = v } + } + + NToggle { + label: "Show Dock" + description: "Display a dock at the bottom of the screen for quick access to applications" + value: Settings.data.general.showDock + onToggled: function (v) { Settings.data.general.showDock = v } + } + + NToggle { + label: "Dim Desktop" + description: "Dim the desktop when panels or menus are open" + value: Settings.data.general.dimDesktop + onToggled: function (v) { Settings.data.general.dimDesktop = v } + } + + Item { Layout.fillHeight: true } + } +} + diff --git a/Modules/Settings/Tabs/Misc.qml b/Modules/Settings/Tabs/Misc.qml new file mode 100644 index 0000000..9d26a4c --- /dev/null +++ b/Modules/Settings/Tabs/Misc.qml @@ -0,0 +1,18 @@ +import QtQuick +import QtQuick.Layouts +import qs.Services +import qs.Widgets + +Item { + property real scaling: 1 + anchors.fill: parent + + ColumnLayout { + anchors.fill: parent + spacing: Style.marginMedium * scaling + NText { text: "Misc"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + NText { text: "Coming soon"; color: Colors.textSecondary } + Item { Layout.fillHeight: true } + } +} + diff --git a/Modules/Settings/Tabs/Network.qml b/Modules/Settings/Tabs/Network.qml new file mode 100644 index 0000000..408922d --- /dev/null +++ b/Modules/Settings/Tabs/Network.qml @@ -0,0 +1,18 @@ +import QtQuick +import QtQuick.Layouts +import qs.Services +import qs.Widgets + +Item { + property real scaling: 1 + anchors.fill: parent + + ColumnLayout { + anchors.fill: parent + spacing: Style.marginMedium * scaling + NText { text: "Network"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + NText { text: "Coming soon"; color: Colors.textSecondary } + Item { Layout.fillHeight: true } + } +} + diff --git a/Modules/Settings/Tabs/ScreenRecorder.qml b/Modules/Settings/Tabs/ScreenRecorder.qml new file mode 100644 index 0000000..ea70889 --- /dev/null +++ b/Modules/Settings/Tabs/ScreenRecorder.qml @@ -0,0 +1,18 @@ +import QtQuick +import QtQuick.Layouts +import qs.Services +import qs.Widgets + +Item { + property real scaling: 1 + anchors.fill: parent + + ColumnLayout { + anchors.fill: parent + spacing: Style.marginMedium * scaling + NText { text: "Screen Recorder"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + NText { text: "Coming soon"; color: Colors.textSecondary } + Item { Layout.fillHeight: true } + } +} + diff --git a/Modules/Settings/Tabs/TimeWeather.qml b/Modules/Settings/Tabs/TimeWeather.qml new file mode 100644 index 0000000..57bece5 --- /dev/null +++ b/Modules/Settings/Tabs/TimeWeather.qml @@ -0,0 +1,18 @@ +import QtQuick +import QtQuick.Layouts +import qs.Services +import qs.Widgets + +Item { + property real scaling: 1 + anchors.fill: parent + + ColumnLayout { + anchors.fill: parent + spacing: Style.marginMedium * scaling + NText { text: "Time & Weather"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + NText { text: "Coming soon"; color: Colors.textSecondary } + Item { Layout.fillHeight: true } + } +} + diff --git a/Modules/Settings/Tabs/Wallpaper.qml b/Modules/Settings/Tabs/Wallpaper.qml new file mode 100644 index 0000000..c495075 --- /dev/null +++ b/Modules/Settings/Tabs/Wallpaper.qml @@ -0,0 +1,18 @@ +import QtQuick +import QtQuick.Layouts +import qs.Services +import qs.Widgets + +Item { + property real scaling: 1 + anchors.fill: parent + + ColumnLayout { + anchors.fill: parent + spacing: Style.marginMedium * scaling + NText { text: "Wallpaper"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + NText { text: "Coming soon"; color: Colors.textSecondary } + Item { Layout.fillHeight: true } + } +} + diff --git a/Modules/SidePanel/ProfileCard.qml b/Modules/SidePanel/ProfileCard.qml index 441e68f..b715b7d 100644 --- a/Modules/SidePanel/ProfileCard.qml +++ b/Modules/SidePanel/ProfileCard.qml @@ -10,6 +10,8 @@ NBox { id: root readonly property real scaling: Scaling.scale(screen) + // Hold a single instance of the Settings window (root is NLoader) + property var settingsWindow: null Layout.fillWidth: true // Height driven by content @@ -71,6 +73,23 @@ NBox { NIconButton { icon: "settings" sizeMultiplier: 0.9 + onClicked: function () { + if (!root.settingsWindow) { + const comp = Qt.createComponent("../Settings/SettingsWindow.qml") + if (comp.status === Component.Ready) { + root.settingsWindow = comp.createObject(root) + } else { + comp.statusChanged.connect(function () { + if (comp.status === Component.Ready) { + root.settingsWindow = comp.createObject(root) + } + }) + } + } + if (root.settingsWindow) { + root.settingsWindow.isLoaded = !root.settingsWindow.isLoaded + } + } } NIconButton { icon: "power_settings_new" diff --git a/Services/Settings.qml b/Services/Settings.qml index 2c034f3..7b33d9a 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -65,6 +65,7 @@ Singleton { property string avatarImage: Quickshell.env("HOME") + "/.face" property bool dimDesktop: true property bool showScreenCorners: false + property bool showDock: false } // location diff --git a/Widgets/NTextBox.qml b/Widgets/NTextBox.qml new file mode 100644 index 0000000..85b89f7 --- /dev/null +++ b/Widgets/NTextBox.qml @@ -0,0 +1,66 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Services + +Item { + id: root + + readonly property real scaling: Scaling.scale(screen) + + // API + property alias text: input.text + property alias placeholderText: input.placeholderText + property bool readOnly: false + property bool enabled: true + property var onEditingFinished: function () {} + property var onTextChanged: function (value) {} + + // Sizing + implicitHeight: Style.baseWidgetSize * 1.25 * scaling + implicitWidth: 320 * scaling + + // Container + Rectangle { + id: frame + anchors.fill: parent + radius: Style.radiusMedium * scaling + color: Colors.surfaceVariant + border.color: Colors.outline + border.width: Math.max(1, Style.borderThin * scaling) + + // Focus ring + Rectangle { + anchors.fill: parent + radius: frame.radius + color: "transparent" + border.color: input.activeFocus ? Colors.accentPrimary : "transparent" + border.width: input.activeFocus ? Math.max(1, Style.borderMedium * scaling) : 0 + } + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Style.marginMedium * scaling + anchors.rightMargin: Style.marginMedium * scaling + spacing: Style.marginSmall * 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: Colors.textPrimary + placeholderTextColor: Colors.textSecondary + background: null + font.pointSize: Colors.fontSizeSmall * scaling + onEditingFinished: root.onEditingFinished() + onTextChanged: root.onTextChanged(text) + } + } + } +} + From 7434df4fc15757fb5d43d1d85497145e61658fc5 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Mon, 11 Aug 2025 15:52:32 +0200 Subject: [PATCH 105/394] Make typing possible in SettingsWindow --- Modules/Settings/SettingsWindow.qml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Modules/Settings/SettingsWindow.qml b/Modules/Settings/SettingsWindow.qml index 389e239..13788fa 100644 --- a/Modules/Settings/SettingsWindow.qml +++ b/Modules/Settings/SettingsWindow.qml @@ -14,6 +14,8 @@ NLoader { NPanel { id: settingsPanel + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + readonly property real scaling: Scaling.scale(screen) // Single source of truth for tabs // Each tab points to a QML file path. The content stack simply loads the file via Loader.source. From d233768f0618c65abcbdebff2382c7bfb3a06a07 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Mon, 11 Aug 2025 15:55:31 +0200 Subject: [PATCH 106/394] SettingsWindow: Fix tab index and close button tooltip --- Modules/Settings/SettingsWindow.qml | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/Modules/Settings/SettingsWindow.qml b/Modules/Settings/SettingsWindow.qml index 13788fa..a779353 100644 --- a/Modules/Settings/SettingsWindow.qml +++ b/Modules/Settings/SettingsWindow.qml @@ -17,6 +17,8 @@ NLoader { WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand readonly property real scaling: Scaling.scale(screen) + // Active tab index unified for sidebar, header, and content stack + property int currentTabIndex: 0 // Single source of truth for tabs // Each tab points to a QML file path. The content stack simply loads the file via Loader.source. property var tabsModel: [ @@ -33,13 +35,7 @@ NLoader { // Always default to the first tab (General) when the panel becomes visible onVisibleChanged: function () { - if (visible) { - Qt.callLater(function () { - if (typeof stack !== 'undefined' && stack) { - stack.currentIndex = 0 - } - }) - } + if (visible) currentTabIndex = 0 } // Ensure panel shows itself once created @@ -86,7 +82,7 @@ NLoader { model: settingsPanel.tabsModel delegate: Rectangle { - readonly property bool selected: index === stack.currentIndex + readonly property bool selected: index === settingsPanel.currentTabIndex Layout.fillWidth: true height: 44 * scaling radius: Style.radiusSmall * scaling @@ -107,7 +103,7 @@ NLoader { } NText { text: modelData.label; color: selected ? Colors.onAccent : Colors.textPrimary; Layout.fillWidth: true } } - MouseArea { anchors.fill: parent; onClicked: stack.currentIndex = index } + MouseArea { anchors.fill: parent; onClicked: settingsPanel.currentTabIndex = index } } } } @@ -137,7 +133,7 @@ NLoader { Layout.fillWidth: true spacing: Style.marginSmall * scaling NText { - text: settingsPanel.tabsModel[stack.currentIndex].label + text: settingsPanel.tabsModel[settingsPanel.currentTabIndex].label font.weight: Style.fontWeightBold color: Colors.textPrimary Layout.fillWidth: true @@ -145,7 +141,7 @@ NLoader { NIconButton { id: demoPanelToggle icon: "close" - tooltipText: "Open demo panel" + tooltipText: "Close settings panel" Layout.alignment: Qt.AlignVCenter onClicked: function () { settingsWindow.isLoaded = !settingsWindow.isLoaded } } @@ -158,14 +154,13 @@ NLoader { id: stack Layout.fillWidth: true Layout.fillHeight: true - currentIndex: 0 - Component.onCompleted: currentIndex = 0 + currentIndex: settingsPanel.currentTabIndex // Pages generated from tabsModel Repeater { model: settingsPanel.tabsModel delegate: Loader { - active: index === stack.currentIndex + active: index === settingsPanel.currentTabIndex visible: active source: modelData.source } From f476fd243acdb2663bd9018271b724a79f4fd789 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 10:00:54 -0400 Subject: [PATCH 107/394] Replacing hardcoded duration by their Style. equivalent --- Modules/Bar/Workspace.qml | 12 ++++++------ Widgets/NPanel.qml | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Modules/Bar/Workspace.qml b/Modules/Bar/Workspace.qml index 10529e3..22a8cc7 100644 --- a/Modules/Bar/Workspace.qml +++ b/Modules/Bar/Workspace.qml @@ -193,37 +193,37 @@ Item { // Material 3-inspired smooth animation for width, height, scale, color, opacity, and radius Behavior on width { NumberAnimation { - duration: 350 + duration: Style.animationNormal easing.type: Easing.OutBack } } Behavior on height { NumberAnimation { - duration: 350 + duration: Style.animationNormal easing.type: Easing.OutBack } } Behavior on scale { NumberAnimation { - duration: 300 + duration: Style.animationNormal easing.type: Easing.OutBack } } Behavior on color { ColorAnimation { - duration: 200 + duration: Style.animationFast easing.type: Easing.InOutCubic } } Behavior on opacity { NumberAnimation { - duration: 200 + duration: Style.animationFast easing.type: Easing.InOutCubic } } Behavior on radius { NumberAnimation { - duration: 350 + duration: Style.animationNormal easing.type: Easing.OutBack } } diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index e995273..f0613f5 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -50,7 +50,7 @@ PanelWindow { Behavior on color { ColorAnimation { - duration: 350 + duration: Style.animationNormal easing.type: Easing.InOutCubic } } From 503c7bf0d1e906989e9e8528e3bb96197da63921 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 10:01:59 -0400 Subject: [PATCH 108/394] More hardcoded values fix --- Modules/Bar/Workspace.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/Bar/Workspace.qml b/Modules/Bar/Workspace.qml index 22a8cc7..36797d1 100644 --- a/Modules/Bar/Workspace.qml +++ b/Modules/Bar/Workspace.qml @@ -231,13 +231,13 @@ Item { Behavior on width { NumberAnimation { - duration: 350 + duration: Style.animationNormal easing.type: Easing.OutBack } } Behavior on height { NumberAnimation { - duration: 350 + duration: Style.animationNormal easing.type: Easing.OutBack } } From ea22af05d1589329169a050422225607d1b3b93a Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Mon, 11 Aug 2025 16:07:04 +0200 Subject: [PATCH 109/394] Fix small warning with NTextBox --- Modules/Settings/SettingsWindow.qml | 3 +-- Modules/Settings/Tabs/About.qml | 3 +++ Modules/Settings/Tabs/Bar.qml | 4 ++++ Modules/Settings/Tabs/Display.qml | 3 +++ Modules/Settings/Tabs/General.qml | 4 ++++ Modules/Settings/Tabs/Misc.qml | 3 +++ Modules/Settings/Tabs/Network.qml | 3 +++ Modules/Settings/Tabs/ScreenRecorder.qml | 3 +++ Modules/Settings/Tabs/TimeWeather.qml | 3 +++ Modules/Settings/Tabs/Wallpaper.qml | 3 +++ Widgets/NTextBox.qml | 4 ++-- 11 files changed, 32 insertions(+), 4 deletions(-) diff --git a/Modules/Settings/SettingsWindow.qml b/Modules/Settings/SettingsWindow.qml index a779353..5f55b2a 100644 --- a/Modules/Settings/SettingsWindow.qml +++ b/Modules/Settings/SettingsWindow.qml @@ -19,8 +19,7 @@ NLoader { readonly property real scaling: Scaling.scale(screen) // Active tab index unified for sidebar, header, and content stack property int currentTabIndex: 0 - // Single source of truth for tabs - // Each tab points to a QML file path. The content stack simply loads the file via Loader.source. + // Single source of truth for tabs (explicit icon/label here) property var tabsModel: [ { icon: "tune", label: "General", source: "Tabs/General.qml" }, { icon: "web_asset", label: "Bar", source: "Tabs/Bar.qml" }, diff --git a/Modules/Settings/Tabs/About.qml b/Modules/Settings/Tabs/About.qml index 18142c3..676ec93 100644 --- a/Modules/Settings/Tabs/About.qml +++ b/Modules/Settings/Tabs/About.qml @@ -5,6 +5,9 @@ import qs.Widgets Item { property real scaling: 1 + readonly property string tabIcon: "info" + readonly property string tabLabel: "About" + readonly property int tabIndex: 8 anchors.fill: parent ColumnLayout { diff --git a/Modules/Settings/Tabs/Bar.qml b/Modules/Settings/Tabs/Bar.qml index da8669f..73b46e2 100644 --- a/Modules/Settings/Tabs/Bar.qml +++ b/Modules/Settings/Tabs/Bar.qml @@ -6,6 +6,10 @@ import qs.Widgets Item { // Optional scaling prop to match other tabs property real scaling: 1 + // Tab metadata + readonly property string tabIcon: "web_asset" + readonly property string tabLabel: "Bar" + readonly property int tabIndex: 1 anchors.fill: parent ColumnLayout { diff --git a/Modules/Settings/Tabs/Display.qml b/Modules/Settings/Tabs/Display.qml index ac2f8e1..d473471 100644 --- a/Modules/Settings/Tabs/Display.qml +++ b/Modules/Settings/Tabs/Display.qml @@ -5,6 +5,9 @@ import qs.Widgets Item { property real scaling: 1 + readonly property string tabIcon: "monitor" + readonly property string tabLabel: "Display" + readonly property int tabIndex: 5 anchors.fill: parent ColumnLayout { diff --git a/Modules/Settings/Tabs/General.qml b/Modules/Settings/Tabs/General.qml index e0800db..729fe09 100644 --- a/Modules/Settings/Tabs/General.qml +++ b/Modules/Settings/Tabs/General.qml @@ -9,6 +9,10 @@ Item { // Public API // Scaling factor provided by the parent settings window property real scaling: 1 + // Tab metadata + readonly property string tabIcon: "tune" + readonly property string tabLabel: "General" + readonly property int tabIndex: 0 anchors.fill: parent implicitWidth: parent ? parent.width : 0 diff --git a/Modules/Settings/Tabs/Misc.qml b/Modules/Settings/Tabs/Misc.qml index 9d26a4c..866c73f 100644 --- a/Modules/Settings/Tabs/Misc.qml +++ b/Modules/Settings/Tabs/Misc.qml @@ -5,6 +5,9 @@ import qs.Widgets Item { property real scaling: 1 + readonly property string tabIcon: "more_horiz" + readonly property string tabLabel: "Misc" + readonly property int tabIndex: 7 anchors.fill: parent ColumnLayout { diff --git a/Modules/Settings/Tabs/Network.qml b/Modules/Settings/Tabs/Network.qml index 408922d..e15423f 100644 --- a/Modules/Settings/Tabs/Network.qml +++ b/Modules/Settings/Tabs/Network.qml @@ -5,6 +5,9 @@ import qs.Widgets Item { property real scaling: 1 + readonly property string tabIcon: "wifi" + readonly property string tabLabel: "Network" + readonly property int tabIndex: 4 anchors.fill: parent ColumnLayout { diff --git a/Modules/Settings/Tabs/ScreenRecorder.qml b/Modules/Settings/Tabs/ScreenRecorder.qml index ea70889..f882f03 100644 --- a/Modules/Settings/Tabs/ScreenRecorder.qml +++ b/Modules/Settings/Tabs/ScreenRecorder.qml @@ -5,6 +5,9 @@ import qs.Widgets Item { property real scaling: 1 + readonly property string tabIcon: "videocam" + readonly property string tabLabel: "Screen Recorder" + readonly property int tabIndex: 3 anchors.fill: parent ColumnLayout { diff --git a/Modules/Settings/Tabs/TimeWeather.qml b/Modules/Settings/Tabs/TimeWeather.qml index 57bece5..2a425aa 100644 --- a/Modules/Settings/Tabs/TimeWeather.qml +++ b/Modules/Settings/Tabs/TimeWeather.qml @@ -5,6 +5,9 @@ import qs.Widgets Item { property real scaling: 1 + readonly property string tabIcon: "schedule" + readonly property string tabLabel: "Time & Weather" + readonly property int tabIndex: 2 anchors.fill: parent ColumnLayout { diff --git a/Modules/Settings/Tabs/Wallpaper.qml b/Modules/Settings/Tabs/Wallpaper.qml index c495075..8b880b8 100644 --- a/Modules/Settings/Tabs/Wallpaper.qml +++ b/Modules/Settings/Tabs/Wallpaper.qml @@ -5,6 +5,9 @@ import qs.Widgets Item { property real scaling: 1 + readonly property string tabIcon: "image" + readonly property string tabLabel: "Wallpaper" + readonly property int tabIndex: 6 anchors.fill: parent ColumnLayout { diff --git a/Widgets/NTextBox.qml b/Widgets/NTextBox.qml index 85b89f7..53ec72f 100644 --- a/Widgets/NTextBox.qml +++ b/Widgets/NTextBox.qml @@ -14,7 +14,6 @@ Item { property bool readOnly: false property bool enabled: true property var onEditingFinished: function () {} - property var onTextChanged: function (value) {} // Sizing implicitHeight: Style.baseWidgetSize * 1.25 * scaling @@ -58,7 +57,8 @@ Item { background: null font.pointSize: Colors.fontSizeSmall * scaling onEditingFinished: root.onEditingFinished() - onTextChanged: root.onTextChanged(text) + // 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. } } } From 690fa9674e117e3169fddfa9f72b6563ee83606b Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 10:44:16 -0400 Subject: [PATCH 110/394] TrayMenu wrapped in NPanel: clicking outside closes the menu (and panel) --- Modules/Bar/Tray.qml | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/Modules/Bar/Tray.qml b/Modules/Bar/Tray.qml index 5ae2c99..6b3334f 100644 --- a/Modules/Bar/Tray.qml +++ b/Modules/Bar/Tray.qml @@ -97,6 +97,7 @@ Item { const menuY = (Style.barHeight * scaling) trayMenu.menu = modelData.menu trayMenu.showAt(parent, menuX, menuY) + trayPanel.show() } else { console.log("Tray: no menu available for", modelData.id, "or trayMenu not set") @@ -116,8 +117,20 @@ Item { } } - // Attached TrayMenu - TrayMenu { - id: trayMenu + // Attached TrayMenu drop down + NPanel { + id: trayPanel + showOverlay: false // no colors overlay even if activated in settings + Connections { + target: trayPanel + ignoreUnknownSignals: true + function onDismissed() { + trayPanel.visible = false + trayMenu.hideMenu() + } + } + TrayMenu { + id: trayMenu + } } } From ddcb471a1ebc101b7317dc1625d133ea7a109502 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 10:50:12 -0400 Subject: [PATCH 111/394] Tray: added comments --- Modules/Bar/Tray.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/Modules/Bar/Tray.qml b/Modules/Bar/Tray.qml index 6b3334f..fcc67ce 100644 --- a/Modules/Bar/Tray.qml +++ b/Modules/Bar/Tray.qml @@ -118,6 +118,7 @@ Item { } // Attached TrayMenu drop down + // Wrapped in NPanel so we can detect click outside of the menu to close the TrayMenu NPanel { id: trayPanel showOverlay: false // no colors overlay even if activated in settings From b083d21c2f637d1ec32433b84387a171769d53f8 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 11:43:44 -0400 Subject: [PATCH 112/394] Formatting --- Modules/Bar/Tray.qml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Modules/Bar/Tray.qml b/Modules/Bar/Tray.qml index fcc67ce..5b2c806 100644 --- a/Modules/Bar/Tray.qml +++ b/Modules/Bar/Tray.qml @@ -123,13 +123,13 @@ Item { id: trayPanel showOverlay: false // no colors overlay even if activated in settings Connections { - target: trayPanel - ignoreUnknownSignals: true - function onDismissed() { - trayPanel.visible = false - trayMenu.hideMenu() - } + target: trayPanel + ignoreUnknownSignals: true + function onDismissed() { + trayPanel.visible = false + trayMenu.hideMenu() } + } TrayMenu { id: trayMenu } From bbffe39fe07d86fd1b00d97901f11616ec7a465c Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 11:43:58 -0400 Subject: [PATCH 113/394] Basic Wifi Icon --- Modules/Bar/Bar.qml | 8 + Modules/Bar/Wifi.qml | 403 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 411 insertions(+) create mode 100644 Modules/Bar/Wifi.qml diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index c5cb0ed..8cc968a 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -82,6 +82,14 @@ Variants { anchors.verticalCenter: parent.verticalCenter } + // TODO: Notification Icon + Wifi { + anchors.verticalCenter: parent.verticalCenter + } + + // Bluetooth { + // anchors.verticalCenter: parent.verticalCenter + // } Battery { anchors.verticalCenter: parent.verticalCenter } diff --git a/Modules/Bar/Wifi.qml b/Modules/Bar/Wifi.qml new file mode 100644 index 0000000..1482722 --- /dev/null +++ b/Modules/Bar/Wifi.qml @@ -0,0 +1,403 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland +import qs.Services +import qs.Widgets + +Item { + id: root + + readonly property real scaling: Scaling.scale(screen) + readonly property bool wifiEnabled: Settings.data.network.wifiEnabled + + property bool menuVisible: false + property string passwordPromptSsid: "" + property string passwordInput: "" + property bool showPasswordPrompt: false + + + width: wifiEnabled ? 22 : 0 + height: wifiEnabled ? 22 : 0 + + + Network { + id: network + } + + // WiFi icon/button + Item { + id: wifiIcon + width: 22 + height: 22 + visible: wifiEnabled + + property int currentSignal: { + let maxSignal = 0 + for (const net in network.networks) { + if (network.networks[net].connected && network.networks[net].signal > maxSignal) { + maxSignal = network.networks[net].signal + } + } + return maxSignal + } + + NIconButton { + id: wifiText + anchors.centerIn: parent + icon: { + let connected = false + for (const net in network.networks) { + if (network.networks[net].connected) { + connected = true + break + } + } + return connected ? network.signalIcon(parent.currentSignal) : "wifi_off" + } + fontFamily: mouseAreaWifi.containsMouse ? "Material Symbols Rounded" : "Material Symbols Outlined" + tooltipText: "WiFi Networks" + onClicked: function () { + if (!wifiMenuLoader.active) { + wifiMenuLoader.loading = true + } + if (wifiMenuLoader.item) { + wifiMenuLoader.item.visible = !wifiMenuLoader.item.visible + if (wifiMenuLoader.item.visible) { + network.onMenuOpened() + } else { + network.onMenuClosed() + } + } + } + } + + // MouseArea { + // id: mouseAreaWifi + // anchors.fill: parent + // hoverEnabled: true + // cursorShape: Qt.PointingHandCursor + // onClicked: { + // wifiTooltip.hide() + // if (!wifiMenuLoader.active) { + // wifiMenuLoader.loading = true + // } + // if (wifiMenuLoader.item) { + // wifiMenuLoader.item.visible = !wifiMenuLoader.item.visible + // if (wifiMenuLoader.item.visible) { + // network.onMenuOpened() + // } else { + // network.onMenuClosed() + // } + // } + // } + // onEntered: wifiTooltip.show() + // onExited: wifiTooltip.hide() + // } + } + + // NTooltip { + // id: wifiTooltip + // target: wifiIcon + + // positionAbove: false + // } + + // LazyLoader for WiFi menu + LazyLoader { + id: wifiMenuLoader + loading: false + component: NPanel { + id: wifiMenu + + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + + Rectangle { + color: Colors.backgroundSecondary + radius: Style.radiusMedium * scaling + border.color: Colors.backgroundTertiary + border.width: Math.max(1, Style.borderMedium * scaling) + width: 340 * scaling + height: 320 * scaling + anchors.top: parent.top + anchors.right: parent.right + anchors.topMargin: Style.marginTiny * scaling + anchors.rightMargin: Style.marginTiny * scaling + + ColumnLayout { + anchors.fill: parent + anchors.margins: 16 + spacing: 16 + + RowLayout { + Layout.fillWidth: true + spacing: 12 + + Text { + text: "wifi" + font.family: "Material Symbols Outlined" + font.pointSize: 24 * scaling + color: Colors.accentPrimary + } + + Text { + text: "WiFi Networks" + font.pointSize: 18 * scaling + font.bold: true + color: Colors.textPrimary + Layout.fillWidth: true + } + + NIconButton { + icon: "refresh" + onClicked: function () {network.refreshNetworks() } + } + + NIconButton { + icon: "close" + onClicked: function () { + wifiMenu.visible = false + network.onMenuClosed() + } + } + } + + Rectangle { + Layout.fillWidth: true + height: 1 + color: Colors.outline + opacity: 0.12 + } + + ListView { + id: networkList + Layout.fillWidth: true + Layout.fillHeight: true + model: Object.values(network.networks) + spacing: 8 + clip: true + + delegate: Item { + width: parent.width + height: modelData.ssid === passwordPromptSsid + && showPasswordPrompt ? 108 : 48 // 48 for network + 60 for password prompt + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 48 + radius: 8 + color: modelData.connected ? Qt.rgba( + Colors.accentPrimary.r, Colors.accentPrimary.g, Colors.accentPrimary.b, + 0.44) : (networkMouseArea.containsMouse ? Colors.highlight : "transparent") + + RowLayout { + anchors.fill: parent + anchors.margins: 8 + spacing: 8 + + Text { + text: network.signalIcon(modelData.signal) + font.family: "Material Symbols Outlined" + font.pointSize: 18 * scaling + color: networkMouseArea.containsMouse ? Colors.backgroundPrimary : (modelData.connected ? Colors.accentPrimary : Colors.textSecondary) + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + + Text { + text: modelData.ssid || "Unknown Network" + color: networkMouseArea.containsMouse ? Colors.backgroundPrimary : (modelData.connected ? Colors.accentPrimary : Colors.textPrimary) + font.pointSize: 14 * scaling + elide: Text.ElideRight + Layout.fillWidth: true + } + + Text { + text: modelData.security && modelData.security !== "--" ? modelData.security : "Open" + color: networkMouseArea.containsMouse ? Colors.backgroundPrimary : (modelData.connected ? Colors.accentPrimary : Colors.textSecondary) + font.pointSize: 11 * scaling + elide: Text.ElideRight + Layout.fillWidth: true + } + + Text { + visible: network.connectStatusSsid === modelData.ssid && network.connectStatus === "error" + && network.connectError.length > 0 + text: network.connectError + color: Colors.error + font.pointSize: 11 * scaling + elide: Text.ElideRight + Layout.fillWidth: true + } + } + + Item { + Layout.preferredWidth: 22 + Layout.preferredHeight: 22 + visible: network.connectStatusSsid === modelData.ssid + && (network.connectStatus !== "" || network.connectingSsid === modelData.ssid) + + // TBC + // Spinner { + // visible: network.connectingSsid === modelData.ssid + // running: network.connectingSsid === modelData.ssid + // color: Colors.accentPrimary + // anchors.centerIn: parent + // size: 22 + // } + + Text { + visible: network.connectStatus === "success" && !network.connectingSsid + text: "check_circle" + font.family: "Material Symbols Outlined" + font.pointSize: 18 * scaling + color: "#43a047" // TBC: No! + anchors.centerIn: parent + } + + Text { + visible: network.connectStatus === "error" && !network.connectingSsid + text: "error" + font.family: "Material Symbols Outlined" + font.pointSize: 18 * scaling + color: Colors.error + anchors.centerIn: parent + } + } + + Text { + visible: modelData.connected + text: "connected" + color: networkMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.accentPrimary + font.pointSize: 11 * scaling + } + } + + MouseArea { + id: networkMouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + if (modelData.connected) { + network.disconnectNetwork(modelData.ssid) + } else if (network.isSecured(modelData.security) && !modelData.existing) { + passwordPromptSsid = modelData.ssid + showPasswordPrompt = true + passwordInput = "" // Clear previous input + Qt.callLater(function () { + passwordInputField.forceActiveFocus() + }) + } else { + network.connectNetwork(modelData.ssid, modelData.security) + } + } + } + } + + // Password prompt section + Rectangle { + id: passwordPromptSection + Layout.fillWidth: true + Layout.preferredHeight: modelData.ssid === passwordPromptSsid && showPasswordPrompt ? 60 : 0 + Layout.margins: 8 + visible: modelData.ssid === passwordPromptSsid && showPasswordPrompt + color: Colors.surfaceVariant + radius: 8 + + RowLayout { + anchors.fill: parent + anchors.margins: 12 + spacing: 10 + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 36 + + Rectangle { + anchors.fill: parent + radius: 8 + color: "transparent" + border.color: passwordInputField.activeFocus ? Colors.accentPrimary : Colors.outline + border.width: 1 + + TextInput { + id: passwordInputField + anchors.fill: parent + anchors.margins: 12 + text: passwordInput + font.pointSize: 13 * scaling + color: Colors.textPrimary + verticalAlignment: TextInput.AlignVCenter + clip: true + focus: true + selectByMouse: true + activeFocusOnTab: true + inputMethodHints: Qt.ImhNone + echoMode: TextInput.Password + onTextChanged: passwordInput = text + onAccepted: { + network.submitPassword(passwordPromptSsid, passwordInput) + showPasswordPrompt = false + } + + MouseArea { + id: passwordInputMouseArea + anchors.fill: parent + onClicked: passwordInputField.forceActiveFocus() + } + } + } + } + + Rectangle { + Layout.preferredWidth: 80 + Layout.preferredHeight: 36 + radius: 18 + color: Colors.accentPrimary + border.color: Colors.accentPrimary + border.width: 0 + opacity: 1.0 + + Behavior on color { + ColorAnimation { + duration: 100 + } + } + + MouseArea { + anchors.fill: parent + onClicked: { + network.submitPassword(passwordPromptSsid, passwordInput) + showPasswordPrompt = false + } + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onEntered: parent.color = Qt.darker(Colors.accentPrimary, 1.1) + onExited: parent.color = Colors.accentPrimary + } + + Text { + anchors.centerIn: parent + text: "Connect" + color: Colors.backgroundPrimary + font.pointSize: 14 * scaling + font.bold: true + } + } + } + } + } + } + } + } + } + } + } +} From 41d93128b888eadefe173e3acf0b7a01d49a733a Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 11:44:06 -0400 Subject: [PATCH 114/394] Settings formatting (qmlfmt) --- Modules/Settings/SettingsWindow.qml | 80 ++++++++++++++++++------ Modules/Settings/Tabs/About.qml | 16 +++-- Modules/Settings/Tabs/Bar.qml | 16 +++-- Modules/Settings/Tabs/Display.qml | 16 +++-- Modules/Settings/Tabs/General.qml | 46 +++++++++++--- Modules/Settings/Tabs/Misc.qml | 16 +++-- Modules/Settings/Tabs/Network.qml | 16 +++-- Modules/Settings/Tabs/ScreenRecorder.qml | 16 +++-- Modules/Settings/Tabs/TimeWeather.qml | 16 +++-- Modules/Settings/Tabs/Wallpaper.qml | 16 +++-- 10 files changed, 192 insertions(+), 62 deletions(-) diff --git a/Modules/Settings/SettingsWindow.qml b/Modules/Settings/SettingsWindow.qml index 5f55b2a..cdcab4a 100644 --- a/Modules/Settings/SettingsWindow.qml +++ b/Modules/Settings/SettingsWindow.qml @@ -20,21 +20,48 @@ NLoader { // Active tab index unified for sidebar, header, and content stack property int currentTabIndex: 0 // Single source of truth for tabs (explicit icon/label here) - property var tabsModel: [ - { icon: "tune", label: "General", source: "Tabs/General.qml" }, - { icon: "web_asset", label: "Bar", source: "Tabs/Bar.qml" }, - { icon: "schedule", label: "Time & Weather", source: "Tabs/TimeWeather.qml" }, - { icon: "videocam", label: "Screen Recorder", source: "Tabs/ScreenRecorder.qml" }, - { icon: "wifi", label: "Network", source: "Tabs/Network.qml" }, - { icon: "monitor", label: "Display", source: "Tabs/Display.qml" }, - { icon: "image", label: "Wallpaper", source: "Tabs/Wallpaper.qml" }, - { icon: "more_horiz", label: "Misc", source: "Tabs/Misc.qml" }, - { icon: "info", label: "About", source: "Tabs/About.qml" } - ] + property var tabsModel: [{ + "icon": "tune", + "label": "General", + "source": "Tabs/General.qml" + }, { + "icon": "web_asset", + "label": "Bar", + "source": "Tabs/Bar.qml" + }, { + "icon": "schedule", + "label": "Time & Weather", + "source": "Tabs/TimeWeather.qml" + }, { + "icon": "videocam", + "label": "Screen Recorder", + "source": "Tabs/ScreenRecorder.qml" + }, { + "icon": "wifi", + "label": "Network", + "source": "Tabs/Network.qml" + }, { + "icon": "monitor", + "label": "Display", + "source": "Tabs/Display.qml" + }, { + "icon": "image", + "label": "Wallpaper", + "source": "Tabs/Wallpaper.qml" + }, { + "icon": "more_horiz", + "label": "Misc", + "source": "Tabs/Misc.qml" + }, { + "icon": "info", + "label": "About", + "source": "Tabs/About.qml" + }] // Always default to the first tab (General) when the panel becomes visible onVisibleChanged: function () { - if (visible) currentTabIndex = 0 + if (visible) + currentTabIndex = 0 } // Ensure panel shows itself once created @@ -52,8 +79,9 @@ NLoader { anchors.centerIn: parent // Prevent closing when clicking in the panel bg - MouseArea { anchors.fill: parent } - + MouseArea { + anchors.fill: parent + } // Main two-pane layout RowLayout { @@ -97,12 +125,21 @@ NLoader { NText { text: modelData.icon font.family: "Material Symbols Outlined" - font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 } + font.variableAxes: { + "wght": (Font.Normal + Font.Bold) / 2.0 + } color: selected ? Colors.onAccent : Colors.textSecondary } - NText { text: modelData.label; color: selected ? Colors.onAccent : Colors.textPrimary; Layout.fillWidth: true } + NText { + text: modelData.label + color: selected ? Colors.onAccent : Colors.textPrimary + Layout.fillWidth: true + } + } + MouseArea { + anchors.fill: parent + onClicked: settingsPanel.currentTabIndex = index } - MouseArea { anchors.fill: parent; onClicked: settingsPanel.currentTabIndex = index } } } } @@ -142,11 +179,15 @@ NLoader { icon: "close" tooltipText: "Close settings panel" Layout.alignment: Qt.AlignVCenter - onClicked: function () { settingsWindow.isLoaded = !settingsWindow.isLoaded } + onClicked: function () { + settingsWindow.isLoaded = !settingsWindow.isLoaded + } } } - NDivider { Layout.fillWidth: true } + NDivider { + Layout.fillWidth: true + } // Stacked pages StackLayout { @@ -172,4 +213,3 @@ NLoader { } } } - diff --git a/Modules/Settings/Tabs/About.qml b/Modules/Settings/Tabs/About.qml index 676ec93..4824386 100644 --- a/Modules/Settings/Tabs/About.qml +++ b/Modules/Settings/Tabs/About.qml @@ -13,9 +13,17 @@ Item { ColumnLayout { anchors.fill: parent spacing: Style.marginMedium * scaling - NText { text: "About"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } - NText { text: "Coming soon"; color: Colors.textSecondary } - Item { Layout.fillHeight: true } + NText { + text: "About" + font.weight: Style.fontWeightBold + color: Colors.accentSecondary + } + NText { + text: "Coming soon" + color: Colors.textSecondary + } + Item { + Layout.fillHeight: true + } } } - diff --git a/Modules/Settings/Tabs/Bar.qml b/Modules/Settings/Tabs/Bar.qml index 73b46e2..2079a2e 100644 --- a/Modules/Settings/Tabs/Bar.qml +++ b/Modules/Settings/Tabs/Bar.qml @@ -15,9 +15,17 @@ Item { ColumnLayout { anchors.fill: parent spacing: Style.marginMedium * scaling - NText { text: "Bar"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } - NText { text: "Coming soon"; color: Colors.textSecondary } - Item { Layout.fillHeight: true } + NText { + text: "Bar" + font.weight: Style.fontWeightBold + color: Colors.accentSecondary + } + NText { + text: "Coming soon" + color: Colors.textSecondary + } + Item { + Layout.fillHeight: true + } } } - diff --git a/Modules/Settings/Tabs/Display.qml b/Modules/Settings/Tabs/Display.qml index d473471..3916c9e 100644 --- a/Modules/Settings/Tabs/Display.qml +++ b/Modules/Settings/Tabs/Display.qml @@ -13,9 +13,17 @@ Item { ColumnLayout { anchors.fill: parent spacing: Style.marginMedium * scaling - NText { text: "Display"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } - NText { text: "Coming soon"; color: Colors.textSecondary } - Item { Layout.fillHeight: true } + NText { + text: "Display" + font.weight: Style.fontWeightBold + color: Colors.accentSecondary + } + NText { + text: "Coming soon" + color: Colors.textSecondary + } + Item { + Layout.fillHeight: true + } } } - diff --git a/Modules/Settings/Tabs/General.qml b/Modules/Settings/Tabs/General.qml index 729fe09..8c7c35b 100644 --- a/Modules/Settings/Tabs/General.qml +++ b/Modules/Settings/Tabs/General.qml @@ -24,7 +24,11 @@ Item { spacing: Style.marginMedium * scaling // Profile section - NText { text: "Profile"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + NText { + text: "Profile" + font.weight: Style.fontWeightBold + color: Colors.accentSecondary + } RowLayout { Layout.fillWidth: true @@ -50,8 +54,15 @@ Item { ColumnLayout { Layout.fillWidth: true spacing: 2 * scaling - NText { text: "Profile Image"; color: Colors.textPrimary; font.weight: Style.fontWeightBold } - NText { text: "Your profile picture displayed in various places throughout the shell"; color: Colors.textSecondary } + NText { + text: "Profile Image" + color: Colors.textPrimary + font.weight: Style.fontWeightBold + } + NText { + text: "Your profile picture displayed in various places throughout the shell" + color: Colors.textSecondary + } NTextBox { text: Settings.data.general.avatarImage placeholderText: "/home/user/.face" @@ -61,33 +72,48 @@ Item { } } - NDivider { Layout.fillWidth: true; Layout.topMargin: Style.marginSmall * scaling; Layout.bottomMargin: Style.marginSmall * scaling } + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginSmall * scaling + Layout.bottomMargin: Style.marginSmall * scaling + } // UI section - NText { text: "User Interface"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + NText { + text: "User Interface" + font.weight: Style.fontWeightBold + color: Colors.accentSecondary + } NToggle { label: "Show Corners" description: "Display rounded corners on the edge of the screen" value: Settings.data.general.showScreenCorners - onToggled: function (v) { Settings.data.general.showScreenCorners = v } + onToggled: function (v) { + Settings.data.general.showScreenCorners = v + } } NToggle { label: "Show Dock" description: "Display a dock at the bottom of the screen for quick access to applications" value: Settings.data.general.showDock - onToggled: function (v) { Settings.data.general.showDock = v } + onToggled: function (v) { + Settings.data.general.showDock = v + } } NToggle { label: "Dim Desktop" description: "Dim the desktop when panels or menus are open" value: Settings.data.general.dimDesktop - onToggled: function (v) { Settings.data.general.dimDesktop = v } + onToggled: function (v) { + Settings.data.general.dimDesktop = v + } } - Item { Layout.fillHeight: true } + Item { + Layout.fillHeight: true + } } } - diff --git a/Modules/Settings/Tabs/Misc.qml b/Modules/Settings/Tabs/Misc.qml index 866c73f..e52057b 100644 --- a/Modules/Settings/Tabs/Misc.qml +++ b/Modules/Settings/Tabs/Misc.qml @@ -13,9 +13,17 @@ Item { ColumnLayout { anchors.fill: parent spacing: Style.marginMedium * scaling - NText { text: "Misc"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } - NText { text: "Coming soon"; color: Colors.textSecondary } - Item { Layout.fillHeight: true } + NText { + text: "Misc" + font.weight: Style.fontWeightBold + color: Colors.accentSecondary + } + NText { + text: "Coming soon" + color: Colors.textSecondary + } + Item { + Layout.fillHeight: true + } } } - diff --git a/Modules/Settings/Tabs/Network.qml b/Modules/Settings/Tabs/Network.qml index e15423f..2a15699 100644 --- a/Modules/Settings/Tabs/Network.qml +++ b/Modules/Settings/Tabs/Network.qml @@ -13,9 +13,17 @@ Item { ColumnLayout { anchors.fill: parent spacing: Style.marginMedium * scaling - NText { text: "Network"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } - NText { text: "Coming soon"; color: Colors.textSecondary } - Item { Layout.fillHeight: true } + NText { + text: "Network" + font.weight: Style.fontWeightBold + color: Colors.accentSecondary + } + NText { + text: "Coming soon" + color: Colors.textSecondary + } + Item { + Layout.fillHeight: true + } } } - diff --git a/Modules/Settings/Tabs/ScreenRecorder.qml b/Modules/Settings/Tabs/ScreenRecorder.qml index f882f03..3e2cff3 100644 --- a/Modules/Settings/Tabs/ScreenRecorder.qml +++ b/Modules/Settings/Tabs/ScreenRecorder.qml @@ -13,9 +13,17 @@ Item { ColumnLayout { anchors.fill: parent spacing: Style.marginMedium * scaling - NText { text: "Screen Recorder"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } - NText { text: "Coming soon"; color: Colors.textSecondary } - Item { Layout.fillHeight: true } + NText { + text: "Screen Recorder" + font.weight: Style.fontWeightBold + color: Colors.accentSecondary + } + NText { + text: "Coming soon" + color: Colors.textSecondary + } + Item { + Layout.fillHeight: true + } } } - diff --git a/Modules/Settings/Tabs/TimeWeather.qml b/Modules/Settings/Tabs/TimeWeather.qml index 2a425aa..6bf1b1c 100644 --- a/Modules/Settings/Tabs/TimeWeather.qml +++ b/Modules/Settings/Tabs/TimeWeather.qml @@ -13,9 +13,17 @@ Item { ColumnLayout { anchors.fill: parent spacing: Style.marginMedium * scaling - NText { text: "Time & Weather"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } - NText { text: "Coming soon"; color: Colors.textSecondary } - Item { Layout.fillHeight: true } + NText { + text: "Time & Weather" + font.weight: Style.fontWeightBold + color: Colors.accentSecondary + } + NText { + text: "Coming soon" + color: Colors.textSecondary + } + Item { + Layout.fillHeight: true + } } } - diff --git a/Modules/Settings/Tabs/Wallpaper.qml b/Modules/Settings/Tabs/Wallpaper.qml index 8b880b8..cd90098 100644 --- a/Modules/Settings/Tabs/Wallpaper.qml +++ b/Modules/Settings/Tabs/Wallpaper.qml @@ -13,9 +13,17 @@ Item { ColumnLayout { anchors.fill: parent spacing: Style.marginMedium * scaling - NText { text: "Wallpaper"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } - NText { text: "Coming soon"; color: Colors.textSecondary } - Item { Layout.fillHeight: true } + NText { + text: "Wallpaper" + font.weight: Style.fontWeightBold + color: Colors.accentSecondary + } + NText { + text: "Coming soon" + color: Colors.textSecondary + } + Item { + Layout.fillHeight: true + } } } - From f92ac9f0b025f4f2b15f3d9aa80d9fa16bf33463 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 11:44:21 -0400 Subject: [PATCH 115/394] NIconButton support for custom font family --- Widgets/NIconButton.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml index 042438a..2f3b964 100644 --- a/Widgets/NIconButton.qml +++ b/Widgets/NIconButton.qml @@ -18,6 +18,7 @@ Rectangle { property var onExited: function () {} property var onClicked: function () {} property real fontPointSize: Style.fontSizeMedium + property string fontFamily: "Material Symbols Outlined" implicitWidth: size implicitHeight: size @@ -30,7 +31,7 @@ Rectangle { anchors.horizontalCenterOffset: 0 anchors.verticalCenterOffset: 0 text: root.icon - font.family: "Material Symbols Outlined" + font.family: fontFamily font.pointSize: root.fontPointSize * scaling color: root.hovering ? Colors.onAccent : Colors.textPrimary horizontalAlignment: Text.AlignHCenter From 59d80b17981913e8990e5d73d39d3e33c58f27ad Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 11:44:27 -0400 Subject: [PATCH 116/394] formatting --- Widgets/NTextBox.qml | 2 -- 1 file changed, 2 deletions(-) diff --git a/Widgets/NTextBox.qml b/Widgets/NTextBox.qml index 53ec72f..6d6c8a4 100644 --- a/Widgets/NTextBox.qml +++ b/Widgets/NTextBox.qml @@ -45,7 +45,6 @@ Item { // Optional leading icon slot in the future // Item { Layout.preferredWidth: 0 } - TextField { id: input Layout.fillWidth: true @@ -63,4 +62,3 @@ Item { } } } - From 62025463876eeae64d738cace67275e006bea1a5 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 11:59:34 -0400 Subject: [PATCH 117/394] Splitted WiFi (bar icon) and WifiMenu (bar menu) --- Modules/Bar/Bar.qml | 2 +- Modules/Bar/WiFi.qml | 47 +++++ Modules/Bar/WiFiMenu.qml | 309 ++++++++++++++++++++++++++++++ Modules/Bar/Wifi.qml | 403 --------------------------------------- 4 files changed, 357 insertions(+), 404 deletions(-) create mode 100644 Modules/Bar/WiFi.qml create mode 100644 Modules/Bar/WiFiMenu.qml delete mode 100644 Modules/Bar/Wifi.qml diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 8cc968a..31942c1 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -83,7 +83,7 @@ Variants { } // TODO: Notification Icon - Wifi { + WiFi { anchors.verticalCenter: parent.verticalCenter } diff --git a/Modules/Bar/WiFi.qml b/Modules/Bar/WiFi.qml new file mode 100644 index 0000000..c5152e0 --- /dev/null +++ b/Modules/Bar/WiFi.qml @@ -0,0 +1,47 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland +import qs.Services +import qs.Widgets + +NIconButton { + id: root + + readonly property real scaling: Scaling.scale(screen) + readonly property bool wifiEnabled: Settings.data.network.wifiEnabled + + icon: { + let connected = false + for (const net in network.networks) { + if (network.networks[net].connected) { + connected = true + break + } + } + return connected ? network.signalIcon(parent.currentSignal) : "wifi_off" + } + tooltipText: "WiFi Networks" + onClicked: function () { + if (!wifiMenuLoader.active) { + wifiMenuLoader.loading = true + } + if (wifiMenuLoader.item) { + wifiMenuLoader.item.visible = !wifiMenuLoader.item.visible + if (wifiMenuLoader.item.visible) { + network.onMenuOpened() + } else { + network.onMenuClosed() + } + } + } + + Network { + id: network + } + + WiFiMenu { + id: wifiMenuLoader + } +} diff --git a/Modules/Bar/WiFiMenu.qml b/Modules/Bar/WiFiMenu.qml new file mode 100644 index 0000000..903328c --- /dev/null +++ b/Modules/Bar/WiFiMenu.qml @@ -0,0 +1,309 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland +import qs.Services +import qs.Widgets + +// LazyLoader for WiFi menu +LazyLoader { + id: wifiMenuLoader + loading: false + component: NPanel { + id: wifiMenu + + property string passwordPromptSsid: "" + property string passwordInput: "" + property bool showPasswordPrompt: false + + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + + Rectangle { + color: Colors.backgroundSecondary + radius: Style.radiusMedium * scaling + border.color: Colors.backgroundTertiary + border.width: Math.max(1, Style.borderMedium * scaling) + width: 340 * scaling + height: 320 * scaling + anchors.top: parent.top + anchors.right: parent.right + anchors.topMargin: Style.marginTiny * scaling + anchors.rightMargin: Style.marginTiny * scaling + + ColumnLayout { + anchors.fill: parent + anchors.margins: 16 + spacing: 16 + + RowLayout { + Layout.fillWidth: true + spacing: 12 + + Text { + text: "wifi" + font.family: "Material Symbols Outlined" + font.pointSize: 24 * scaling + color: Colors.accentPrimary + } + + Text { + text: "WiFi Networks" + font.pointSize: 18 * scaling + font.bold: true + color: Colors.textPrimary + Layout.fillWidth: true + } + + NIconButton { + icon: "refresh" + onClicked: function () { + network.refreshNetworks() + } + } + + NIconButton { + icon: "close" + onClicked: function () { + wifiMenu.visible = false + network.onMenuClosed() + } + } + } + + Rectangle { + Layout.fillWidth: true + height: 1 + color: Colors.outline + opacity: 0.12 + } + + ListView { + id: networkList + Layout.fillWidth: true + Layout.fillHeight: true + model: Object.values(network.networks) + spacing: 8 + clip: true + + delegate: Item { + width: parent.width + height: modelData.ssid === passwordPromptSsid + && showPasswordPrompt ? 108 : 48 // 48 for network + 60 for password prompt + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 48 + radius: 8 + color: modelData.connected ? Qt.rgba( + Colors.accentPrimary.r, Colors.accentPrimary.g, Colors.accentPrimary.b, + 0.44) : (networkMouseArea.containsMouse ? Colors.highlight : "transparent") + + RowLayout { + anchors.fill: parent + anchors.margins: 8 + spacing: 8 + + Text { + text: network.signalIcon(modelData.signal) + font.family: "Material Symbols Outlined" + font.pointSize: 18 * scaling + color: networkMouseArea.containsMouse ? Colors.backgroundPrimary : (modelData.connected ? Colors.accentPrimary : Colors.textSecondary) + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + + Text { + text: modelData.ssid || "Unknown Network" + color: networkMouseArea.containsMouse ? Colors.backgroundPrimary : (modelData.connected ? Colors.accentPrimary : Colors.textPrimary) + font.pointSize: 14 * scaling + elide: Text.ElideRight + Layout.fillWidth: true + } + + Text { + text: modelData.security && modelData.security !== "--" ? modelData.security : "Open" + color: networkMouseArea.containsMouse ? Colors.backgroundPrimary : (modelData.connected ? Colors.accentPrimary : Colors.textSecondary) + font.pointSize: 11 * scaling + elide: Text.ElideRight + Layout.fillWidth: true + } + + Text { + visible: network.connectStatusSsid === modelData.ssid && network.connectStatus === "error" + && network.connectError.length > 0 + text: network.connectError + color: Colors.error + font.pointSize: 11 * scaling + elide: Text.ElideRight + Layout.fillWidth: true + } + } + + Item { + Layout.preferredWidth: 22 + Layout.preferredHeight: 22 + visible: network.connectStatusSsid === modelData.ssid + && (network.connectStatus !== "" || network.connectingSsid === modelData.ssid) + + // TBC + // Spinner { + // visible: network.connectingSsid === modelData.ssid + // running: network.connectingSsid === modelData.ssid + // color: Colors.accentPrimary + // anchors.centerIn: parent + // size: 22 + // } + Text { + visible: network.connectStatus === "success" && !network.connectingSsid + text: "check_circle" + font.family: "Material Symbols Outlined" + font.pointSize: 18 * scaling + color: "#43a047" // TBC: No! + anchors.centerIn: parent + } + + Text { + visible: network.connectStatus === "error" && !network.connectingSsid + text: "error" + font.family: "Material Symbols Outlined" + font.pointSize: 18 * scaling + color: Colors.error + anchors.centerIn: parent + } + } + + Text { + visible: modelData.connected + text: "connected" + color: networkMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.accentPrimary + font.pointSize: 11 * scaling + } + } + + MouseArea { + id: networkMouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + if (modelData.connected) { + network.disconnectNetwork(modelData.ssid) + } else if (network.isSecured(modelData.security) && !modelData.existing) { + passwordPromptSsid = modelData.ssid + showPasswordPrompt = true + passwordInput = "" // Clear previous input + Qt.callLater(function () { + passwordInputField.forceActiveFocus() + }) + } else { + network.connectNetwork(modelData.ssid, modelData.security) + } + } + } + } + + // Password prompt section + Rectangle { + id: passwordPromptSection + Layout.fillWidth: true + Layout.preferredHeight: modelData.ssid === passwordPromptSsid && showPasswordPrompt ? 60 : 0 + Layout.margins: 8 + visible: modelData.ssid === passwordPromptSsid && showPasswordPrompt + color: Colors.surfaceVariant + radius: 8 + + RowLayout { + anchors.fill: parent + anchors.margins: 12 + spacing: 10 + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 36 + + Rectangle { + anchors.fill: parent + radius: 8 + color: "transparent" + border.color: passwordInputField.activeFocus ? Colors.accentPrimary : Colors.outline + border.width: 1 + + TextInput { + id: passwordInputField + anchors.fill: parent + anchors.margins: 12 + text: passwordInput + font.pointSize: 13 * scaling + color: Colors.textPrimary + verticalAlignment: TextInput.AlignVCenter + clip: true + focus: true + selectByMouse: true + activeFocusOnTab: true + inputMethodHints: Qt.ImhNone + echoMode: TextInput.Password + onTextChanged: passwordInput = text + onAccepted: { + network.submitPassword(passwordPromptSsid, passwordInput) + showPasswordPrompt = false + } + + MouseArea { + id: passwordInputMouseArea + anchors.fill: parent + onClicked: passwordInputField.forceActiveFocus() + } + } + } + } + + Rectangle { + Layout.preferredWidth: 80 + Layout.preferredHeight: 36 + radius: 18 + color: Colors.accentPrimary + border.color: Colors.accentPrimary + border.width: 0 + opacity: 1.0 + + Behavior on color { + ColorAnimation { + duration: 100 + } + } + + MouseArea { + anchors.fill: parent + onClicked: { + network.submitPassword(passwordPromptSsid, passwordInput) + showPasswordPrompt = false + } + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onEntered: parent.color = Qt.darker(Colors.accentPrimary, 1.1) + onExited: parent.color = Colors.accentPrimary + } + + Text { + anchors.centerIn: parent + text: "Connect" + color: Colors.backgroundPrimary + font.pointSize: 14 * scaling + font.bold: true + } + } + } + } + } + } + } + } + } + } +} diff --git a/Modules/Bar/Wifi.qml b/Modules/Bar/Wifi.qml deleted file mode 100644 index 1482722..0000000 --- a/Modules/Bar/Wifi.qml +++ /dev/null @@ -1,403 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls -import Quickshell -import Quickshell.Wayland -import qs.Services -import qs.Widgets - -Item { - id: root - - readonly property real scaling: Scaling.scale(screen) - readonly property bool wifiEnabled: Settings.data.network.wifiEnabled - - property bool menuVisible: false - property string passwordPromptSsid: "" - property string passwordInput: "" - property bool showPasswordPrompt: false - - - width: wifiEnabled ? 22 : 0 - height: wifiEnabled ? 22 : 0 - - - Network { - id: network - } - - // WiFi icon/button - Item { - id: wifiIcon - width: 22 - height: 22 - visible: wifiEnabled - - property int currentSignal: { - let maxSignal = 0 - for (const net in network.networks) { - if (network.networks[net].connected && network.networks[net].signal > maxSignal) { - maxSignal = network.networks[net].signal - } - } - return maxSignal - } - - NIconButton { - id: wifiText - anchors.centerIn: parent - icon: { - let connected = false - for (const net in network.networks) { - if (network.networks[net].connected) { - connected = true - break - } - } - return connected ? network.signalIcon(parent.currentSignal) : "wifi_off" - } - fontFamily: mouseAreaWifi.containsMouse ? "Material Symbols Rounded" : "Material Symbols Outlined" - tooltipText: "WiFi Networks" - onClicked: function () { - if (!wifiMenuLoader.active) { - wifiMenuLoader.loading = true - } - if (wifiMenuLoader.item) { - wifiMenuLoader.item.visible = !wifiMenuLoader.item.visible - if (wifiMenuLoader.item.visible) { - network.onMenuOpened() - } else { - network.onMenuClosed() - } - } - } - } - - // MouseArea { - // id: mouseAreaWifi - // anchors.fill: parent - // hoverEnabled: true - // cursorShape: Qt.PointingHandCursor - // onClicked: { - // wifiTooltip.hide() - // if (!wifiMenuLoader.active) { - // wifiMenuLoader.loading = true - // } - // if (wifiMenuLoader.item) { - // wifiMenuLoader.item.visible = !wifiMenuLoader.item.visible - // if (wifiMenuLoader.item.visible) { - // network.onMenuOpened() - // } else { - // network.onMenuClosed() - // } - // } - // } - // onEntered: wifiTooltip.show() - // onExited: wifiTooltip.hide() - // } - } - - // NTooltip { - // id: wifiTooltip - // target: wifiIcon - - // positionAbove: false - // } - - // LazyLoader for WiFi menu - LazyLoader { - id: wifiMenuLoader - loading: false - component: NPanel { - id: wifiMenu - - WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand - - Rectangle { - color: Colors.backgroundSecondary - radius: Style.radiusMedium * scaling - border.color: Colors.backgroundTertiary - border.width: Math.max(1, Style.borderMedium * scaling) - width: 340 * scaling - height: 320 * scaling - anchors.top: parent.top - anchors.right: parent.right - anchors.topMargin: Style.marginTiny * scaling - anchors.rightMargin: Style.marginTiny * scaling - - ColumnLayout { - anchors.fill: parent - anchors.margins: 16 - spacing: 16 - - RowLayout { - Layout.fillWidth: true - spacing: 12 - - Text { - text: "wifi" - font.family: "Material Symbols Outlined" - font.pointSize: 24 * scaling - color: Colors.accentPrimary - } - - Text { - text: "WiFi Networks" - font.pointSize: 18 * scaling - font.bold: true - color: Colors.textPrimary - Layout.fillWidth: true - } - - NIconButton { - icon: "refresh" - onClicked: function () {network.refreshNetworks() } - } - - NIconButton { - icon: "close" - onClicked: function () { - wifiMenu.visible = false - network.onMenuClosed() - } - } - } - - Rectangle { - Layout.fillWidth: true - height: 1 - color: Colors.outline - opacity: 0.12 - } - - ListView { - id: networkList - Layout.fillWidth: true - Layout.fillHeight: true - model: Object.values(network.networks) - spacing: 8 - clip: true - - delegate: Item { - width: parent.width - height: modelData.ssid === passwordPromptSsid - && showPasswordPrompt ? 108 : 48 // 48 for network + 60 for password prompt - - ColumnLayout { - anchors.fill: parent - spacing: 0 - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 48 - radius: 8 - color: modelData.connected ? Qt.rgba( - Colors.accentPrimary.r, Colors.accentPrimary.g, Colors.accentPrimary.b, - 0.44) : (networkMouseArea.containsMouse ? Colors.highlight : "transparent") - - RowLayout { - anchors.fill: parent - anchors.margins: 8 - spacing: 8 - - Text { - text: network.signalIcon(modelData.signal) - font.family: "Material Symbols Outlined" - font.pointSize: 18 * scaling - color: networkMouseArea.containsMouse ? Colors.backgroundPrimary : (modelData.connected ? Colors.accentPrimary : Colors.textSecondary) - } - - ColumnLayout { - Layout.fillWidth: true - spacing: 2 - - Text { - text: modelData.ssid || "Unknown Network" - color: networkMouseArea.containsMouse ? Colors.backgroundPrimary : (modelData.connected ? Colors.accentPrimary : Colors.textPrimary) - font.pointSize: 14 * scaling - elide: Text.ElideRight - Layout.fillWidth: true - } - - Text { - text: modelData.security && modelData.security !== "--" ? modelData.security : "Open" - color: networkMouseArea.containsMouse ? Colors.backgroundPrimary : (modelData.connected ? Colors.accentPrimary : Colors.textSecondary) - font.pointSize: 11 * scaling - elide: Text.ElideRight - Layout.fillWidth: true - } - - Text { - visible: network.connectStatusSsid === modelData.ssid && network.connectStatus === "error" - && network.connectError.length > 0 - text: network.connectError - color: Colors.error - font.pointSize: 11 * scaling - elide: Text.ElideRight - Layout.fillWidth: true - } - } - - Item { - Layout.preferredWidth: 22 - Layout.preferredHeight: 22 - visible: network.connectStatusSsid === modelData.ssid - && (network.connectStatus !== "" || network.connectingSsid === modelData.ssid) - - // TBC - // Spinner { - // visible: network.connectingSsid === modelData.ssid - // running: network.connectingSsid === modelData.ssid - // color: Colors.accentPrimary - // anchors.centerIn: parent - // size: 22 - // } - - Text { - visible: network.connectStatus === "success" && !network.connectingSsid - text: "check_circle" - font.family: "Material Symbols Outlined" - font.pointSize: 18 * scaling - color: "#43a047" // TBC: No! - anchors.centerIn: parent - } - - Text { - visible: network.connectStatus === "error" && !network.connectingSsid - text: "error" - font.family: "Material Symbols Outlined" - font.pointSize: 18 * scaling - color: Colors.error - anchors.centerIn: parent - } - } - - Text { - visible: modelData.connected - text: "connected" - color: networkMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.accentPrimary - font.pointSize: 11 * scaling - } - } - - MouseArea { - id: networkMouseArea - anchors.fill: parent - hoverEnabled: true - onClicked: { - if (modelData.connected) { - network.disconnectNetwork(modelData.ssid) - } else if (network.isSecured(modelData.security) && !modelData.existing) { - passwordPromptSsid = modelData.ssid - showPasswordPrompt = true - passwordInput = "" // Clear previous input - Qt.callLater(function () { - passwordInputField.forceActiveFocus() - }) - } else { - network.connectNetwork(modelData.ssid, modelData.security) - } - } - } - } - - // Password prompt section - Rectangle { - id: passwordPromptSection - Layout.fillWidth: true - Layout.preferredHeight: modelData.ssid === passwordPromptSsid && showPasswordPrompt ? 60 : 0 - Layout.margins: 8 - visible: modelData.ssid === passwordPromptSsid && showPasswordPrompt - color: Colors.surfaceVariant - radius: 8 - - RowLayout { - anchors.fill: parent - anchors.margins: 12 - spacing: 10 - - Item { - Layout.fillWidth: true - Layout.preferredHeight: 36 - - Rectangle { - anchors.fill: parent - radius: 8 - color: "transparent" - border.color: passwordInputField.activeFocus ? Colors.accentPrimary : Colors.outline - border.width: 1 - - TextInput { - id: passwordInputField - anchors.fill: parent - anchors.margins: 12 - text: passwordInput - font.pointSize: 13 * scaling - color: Colors.textPrimary - verticalAlignment: TextInput.AlignVCenter - clip: true - focus: true - selectByMouse: true - activeFocusOnTab: true - inputMethodHints: Qt.ImhNone - echoMode: TextInput.Password - onTextChanged: passwordInput = text - onAccepted: { - network.submitPassword(passwordPromptSsid, passwordInput) - showPasswordPrompt = false - } - - MouseArea { - id: passwordInputMouseArea - anchors.fill: parent - onClicked: passwordInputField.forceActiveFocus() - } - } - } - } - - Rectangle { - Layout.preferredWidth: 80 - Layout.preferredHeight: 36 - radius: 18 - color: Colors.accentPrimary - border.color: Colors.accentPrimary - border.width: 0 - opacity: 1.0 - - Behavior on color { - ColorAnimation { - duration: 100 - } - } - - MouseArea { - anchors.fill: parent - onClicked: { - network.submitPassword(passwordPromptSsid, passwordInput) - showPasswordPrompt = false - } - cursorShape: Qt.PointingHandCursor - hoverEnabled: true - onEntered: parent.color = Qt.darker(Colors.accentPrimary, 1.1) - onExited: parent.color = Colors.accentPrimary - } - - Text { - anchors.centerIn: parent - text: "Connect" - color: Colors.backgroundPrimary - font.pointSize: 14 * scaling - font.bold: true - } - } - } - } - } - } - } - } - } - } - } -} From 1addb7d37ab4072ee17255bba7e7961ebfd65705 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 12:06:50 -0400 Subject: [PATCH 118/394] WiFi menu: better look, still wip --- Modules/Bar/WiFiMenu.qml | 39 +++++++++++++++++++-------------------- Services/Style.qml | 1 + 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Modules/Bar/WiFiMenu.qml b/Modules/Bar/WiFiMenu.qml index 903328c..5886e71 100644 --- a/Modules/Bar/WiFiMenu.qml +++ b/Modules/Bar/WiFiMenu.qml @@ -33,23 +33,23 @@ LazyLoader { ColumnLayout { anchors.fill: parent - anchors.margins: 16 - spacing: 16 + anchors.margins: Style.marginLarge * scaling + spacing: Style.marginMedium * scaling RowLayout { Layout.fillWidth: true - spacing: 12 + spacing: Style.marginMedium * scaling - Text { + NText { text: "wifi" font.family: "Material Symbols Outlined" - font.pointSize: 24 * scaling + font.pointSize: Style.fontSizeXL * scaling color: Colors.accentPrimary } - Text { + NText { text: "WiFi Networks" - font.pointSize: 18 * scaling + font.pointSize: Style.fontSizeLarge * scaling font.bold: true color: Colors.textPrimary Layout.fillWidth: true @@ -71,11 +71,8 @@ LazyLoader { } } - Rectangle { - Layout.fillWidth: true - height: 1 - color: Colors.outline - opacity: 0.12 + NDivider { + } ListView { @@ -83,7 +80,7 @@ LazyLoader { Layout.fillWidth: true Layout.fillHeight: true model: Object.values(network.networks) - spacing: 8 + spacing: Style.marginMedium * scaling clip: true delegate: Item { @@ -98,7 +95,7 @@ LazyLoader { Rectangle { Layout.fillWidth: true Layout.preferredHeight: 48 - radius: 8 + radius: Style.radiusMedium * scaling color: modelData.connected ? Qt.rgba( Colors.accentPrimary.r, Colors.accentPrimary.g, Colors.accentPrimary.b, 0.44) : (networkMouseArea.containsMouse ? Colors.highlight : "transparent") @@ -108,10 +105,10 @@ LazyLoader { anchors.margins: 8 spacing: 8 - Text { + NText { text: network.signalIcon(modelData.signal) font.family: "Material Symbols Outlined" - font.pointSize: 18 * scaling + font.pointSize: Style.fontSizeXL * scaling color: networkMouseArea.containsMouse ? Colors.backgroundPrimary : (modelData.connected ? Colors.accentPrimary : Colors.textSecondary) } @@ -119,18 +116,20 @@ LazyLoader { Layout.fillWidth: true spacing: 2 - Text { + // SSID + NText { text: modelData.ssid || "Unknown Network" color: networkMouseArea.containsMouse ? Colors.backgroundPrimary : (modelData.connected ? Colors.accentPrimary : Colors.textPrimary) - font.pointSize: 14 * scaling + font.pointSize: Style.fontSizeNormal * scaling elide: Text.ElideRight Layout.fillWidth: true } - Text { + // Security Protocol + NText { text: modelData.security && modelData.security !== "--" ? modelData.security : "Open" color: networkMouseArea.containsMouse ? Colors.backgroundPrimary : (modelData.connected ? Colors.accentPrimary : Colors.textSecondary) - font.pointSize: 11 * scaling + font.pointSize: Style.fontSizeTiny * scaling elide: Text.ElideRight Layout.fillWidth: true } diff --git a/Services/Style.qml b/Services/Style.qml index 7bb9608..0b21d1d 100644 --- a/Services/Style.qml +++ b/Services/Style.qml @@ -13,6 +13,7 @@ Singleton { */ // Font size + property real fontSizeTiny: 7 property real fontSizeSmall: 9 property real fontSizeMedium: 11 property real fontSizeLarge: 13 From f5cb7b183d2ec1b7884403c7886b799101bc7a05 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 13:29:22 -0400 Subject: [PATCH 119/394] WiFi: Okayish, looks good but not perfect. --- Modules/Bar/WiFi.qml | 2 +- Modules/Bar/WiFiMenu.qml | 98 +++++++++++++++++++++++----------------- Services/Network.qml | 2 +- 3 files changed, 59 insertions(+), 43 deletions(-) diff --git a/Modules/Bar/WiFi.qml b/Modules/Bar/WiFi.qml index c5152e0..b343cd8 100644 --- a/Modules/Bar/WiFi.qml +++ b/Modules/Bar/WiFi.qml @@ -25,7 +25,7 @@ NIconButton { tooltipText: "WiFi Networks" onClicked: function () { if (!wifiMenuLoader.active) { - wifiMenuLoader.loading = true + wifiMenuLoader.isLoaded = true } if (wifiMenuLoader.item) { wifiMenuLoader.item.visible = !wifiMenuLoader.item.visible diff --git a/Modules/Bar/WiFiMenu.qml b/Modules/Bar/WiFiMenu.qml index 5886e71..fa0e987 100644 --- a/Modules/Bar/WiFiMenu.qml +++ b/Modules/Bar/WiFiMenu.qml @@ -7,16 +7,26 @@ import qs.Services import qs.Widgets // LazyLoader for WiFi menu -LazyLoader { - id: wifiMenuLoader - loading: false - component: NPanel { - id: wifiMenu +NLoader { + id: root + + content: Component { + NPanel { + id: wifiPanel property string passwordPromptSsid: "" property string passwordInput: "" property bool showPasswordPrompt: false + Connections { + target: wifiPanel + ignoreUnknownSignals: true + function onDismissed() { + wifiPanel.visible = false + network.onMenuClosed() + } + } + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand Rectangle { @@ -48,13 +58,23 @@ LazyLoader { } NText { - text: "WiFi Networks" + text: "WiFi" font.pointSize: Style.fontSizeLarge * scaling font.bold: true color: Colors.textPrimary Layout.fillWidth: true } + NToggle { + baseSize: Style.baseWidgetSize * 0.75 + value: Settings.data.network.wifiEnabled + onToggled: function (value) { + Settings.data.network.wifiEnabled = value + // TBC: This should be done in a service + Quickshell.execDetached(["nmcli", "radio", "wifi", Settings.data.network.wifiEnabled ? "on" : "off"]) + } + } + NIconButton { icon: "refresh" onClicked: function () { @@ -65,15 +85,13 @@ LazyLoader { NIconButton { icon: "close" onClicked: function () { - wifiMenu.visible = false + wifiPanel.visible = false network.onMenuClosed() } } } - NDivider { - - } + NDivider {} ListView { id: networkList @@ -96,42 +114,40 @@ LazyLoader { Layout.fillWidth: true Layout.preferredHeight: 48 radius: Style.radiusMedium * scaling - color: modelData.connected ? Qt.rgba( - Colors.accentPrimary.r, Colors.accentPrimary.g, Colors.accentPrimary.b, - 0.44) : (networkMouseArea.containsMouse ? Colors.highlight : "transparent") + color: modelData.connected ? Colors.accentPrimary : (networkMouseArea.containsMouse ? Colors.highlight : "transparent") RowLayout { anchors.fill: parent - anchors.margins: 8 - spacing: 8 + anchors.margins: Style.marginSmall * scaling + spacing: Style.marginSmall * scaling NText { text: network.signalIcon(modelData.signal) font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXL * scaling - color: networkMouseArea.containsMouse ? Colors.backgroundPrimary : (modelData.connected ? Colors.accentPrimary : Colors.textSecondary) + color: modelData.connected ? Colors.backgroundPrimary : (networkMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary) } ColumnLayout { Layout.fillWidth: true - spacing: 2 + spacing: Style.marginTiny * scaling // SSID NText { text: modelData.ssid || "Unknown Network" - color: networkMouseArea.containsMouse ? Colors.backgroundPrimary : (modelData.connected ? Colors.accentPrimary : Colors.textPrimary) font.pointSize: Style.fontSizeNormal * scaling elide: Text.ElideRight Layout.fillWidth: true + color: modelData.connected ? Colors.backgroundPrimary : (networkMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary) } // Security Protocol NText { text: modelData.security && modelData.security !== "--" ? modelData.security : "Open" - color: networkMouseArea.containsMouse ? Colors.backgroundPrimary : (modelData.connected ? Colors.accentPrimary : Colors.textSecondary) font.pointSize: Style.fontSizeTiny * scaling elide: Text.ElideRight Layout.fillWidth: true + color: modelData.connected ? Colors.backgroundPrimary : (networkMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary) } Text { @@ -139,7 +155,7 @@ LazyLoader { && network.connectError.length > 0 text: network.connectError color: Colors.error - font.pointSize: 11 * scaling + font.pointSize: Style.fontSizeSmall * scaling elide: Text.ElideRight Layout.fillWidth: true } @@ -159,7 +175,8 @@ LazyLoader { // anchors.centerIn: parent // size: 22 // } - Text { + + NText { visible: network.connectStatus === "success" && !network.connectingSsid text: "check_circle" font.family: "Material Symbols Outlined" @@ -168,21 +185,21 @@ LazyLoader { anchors.centerIn: parent } - Text { + NText { visible: network.connectStatus === "error" && !network.connectingSsid text: "error" font.family: "Material Symbols Outlined" - font.pointSize: 18 * scaling + font.pointSize: Style.fontSizeSmall * scaling color: Colors.error anchors.centerIn: parent } } - Text { + NText { visible: modelData.connected text: "connected" color: networkMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.accentPrimary - font.pointSize: 11 * scaling + font.pointSize: Style.fontSizeSmall * scaling } } @@ -215,12 +232,12 @@ LazyLoader { Layout.margins: 8 visible: modelData.ssid === passwordPromptSsid && showPasswordPrompt color: Colors.surfaceVariant - radius: 8 + radius: Style.radiusSmall * scaling RowLayout { anchors.fill: parent - anchors.margins: 12 - spacing: 10 + anchors.margins: Style.spacingSmall * scaling + spacing: Style.spacingSmall * scaling Item { Layout.fillWidth: true @@ -236,9 +253,9 @@ LazyLoader { TextInput { id: passwordInputField anchors.fill: parent - anchors.margins: 12 + anchors.margins: Style.spacingMedium * scaling text: passwordInput - font.pointSize: 13 * scaling + font.pointSize: Style.fontSizeMedium * scaling color: Colors.textPrimary verticalAlignment: TextInput.AlignVCenter clip: true @@ -265,18 +282,24 @@ LazyLoader { Rectangle { Layout.preferredWidth: 80 Layout.preferredHeight: 36 - radius: 18 + radius: Style.radiusMedium * scaling color: Colors.accentPrimary border.color: Colors.accentPrimary border.width: 0 - opacity: 1.0 Behavior on color { ColorAnimation { - duration: 100 + duration: Style.animationFast } } + NText { + anchors.centerIn: parent + text: "Connect" + color: Colors.backgroundPrimary + font.pointSize: Style.fontSizeSmall * scaling + } + MouseArea { anchors.fill: parent onClicked: { @@ -288,14 +311,6 @@ LazyLoader { onEntered: parent.color = Qt.darker(Colors.accentPrimary, 1.1) onExited: parent.color = Colors.accentPrimary } - - Text { - anchors.centerIn: parent - text: "Connect" - color: Colors.backgroundPrimary - font.pointSize: 14 * scaling - font.bold: true - } } } } @@ -306,3 +321,4 @@ LazyLoader { } } } +} \ No newline at end of file diff --git a/Services/Network.qml b/Services/Network.qml index 7427a2a..92527b8 100644 --- a/Services/Network.qml +++ b/Services/Network.qml @@ -20,7 +20,7 @@ QtObject { return "network_wifi_2_bar" if (signal >= 20) return "network_wifi_1_bar" - return "wifi_0_bar" + return "signal_wifi_0_bar" } function isSecured(security) { From 13841f959dbd682ca150c4ffebdb4d8ee1a0307c Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 13:35:09 -0400 Subject: [PATCH 120/394] NToggle: support for different size --- Widgets/NToggle.qml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Widgets/NToggle.qml b/Widgets/NToggle.qml index 3318bf8..55eb098 100644 --- a/Widgets/NToggle.qml +++ b/Widgets/NToggle.qml @@ -11,6 +11,7 @@ RowLayout { property string description: "" property bool value: false property bool hovering: false + property int baseSize: Style.baseWidgetSize property var onToggled: function (value) {} Layout.fillWidth: true @@ -38,16 +39,16 @@ RowLayout { Rectangle { id: switcher - width: Style.baseWidgetSize * 1.625 * scaling - height: Style.baseWidgetSize * scaling + width: root.baseSize * 1.625 * scaling + height: root.baseSize * scaling radius: height * 0.5 color: value ? Colors.accentPrimary : Colors.surfaceVariant border.color: value ? Colors.accentPrimary : Colors.outline border.width: Math.max(1, Style.borderMedium * scaling) Rectangle { - width: (Style.baseWidgetSize - 4) * scaling - height: (Style.baseWidgetSize - 4) * scaling + width: (root.baseSize - 4) * scaling + height: (root.baseSize - 4) * scaling radius: height * 0.5 color: Colors.surface border.color: hovering ? Colors.textDisabled : Colors.outline From ee323964f015ff099f2bb3118abdcab6101642ec Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 13:43:29 -0400 Subject: [PATCH 121/394] NBusyIndicator (aka Spinner) --- Modules/Bar/WiFiMenu.qml | 555 +++++++++++++++++++------------------ Widgets/NBusyIndicator.qml | 56 ++++ 2 files changed, 334 insertions(+), 277 deletions(-) create mode 100644 Widgets/NBusyIndicator.qml diff --git a/Modules/Bar/WiFiMenu.qml b/Modules/Bar/WiFiMenu.qml index fa0e987..9ee0979 100644 --- a/Modules/Bar/WiFiMenu.qml +++ b/Modules/Bar/WiFiMenu.qml @@ -12,306 +12,308 @@ NLoader { content: Component { NPanel { - id: wifiPanel + id: wifiPanel - property string passwordPromptSsid: "" - property string passwordInput: "" - property bool showPasswordPrompt: false + property string passwordPromptSsid: "" + property string passwordInput: "" + property bool showPasswordPrompt: false - Connections { - target: wifiPanel - ignoreUnknownSignals: true - function onDismissed() { - wifiPanel.visible = false - network.onMenuClosed() - } - } - - WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand - - Rectangle { - color: Colors.backgroundSecondary - radius: Style.radiusMedium * scaling - border.color: Colors.backgroundTertiary - border.width: Math.max(1, Style.borderMedium * scaling) - width: 340 * scaling - height: 320 * scaling - anchors.top: parent.top - anchors.right: parent.right - anchors.topMargin: Style.marginTiny * scaling - anchors.rightMargin: Style.marginTiny * scaling - - ColumnLayout { - anchors.fill: parent - anchors.margins: Style.marginLarge * scaling - spacing: Style.marginMedium * scaling - - RowLayout { - Layout.fillWidth: true - spacing: Style.marginMedium * scaling - - NText { - text: "wifi" - font.family: "Material Symbols Outlined" - font.pointSize: Style.fontSizeXL * scaling - color: Colors.accentPrimary - } - - NText { - text: "WiFi" - font.pointSize: Style.fontSizeLarge * scaling - font.bold: true - color: Colors.textPrimary - Layout.fillWidth: true - } - - NToggle { - baseSize: Style.baseWidgetSize * 0.75 - value: Settings.data.network.wifiEnabled - onToggled: function (value) { - Settings.data.network.wifiEnabled = value - // TBC: This should be done in a service - Quickshell.execDetached(["nmcli", "radio", "wifi", Settings.data.network.wifiEnabled ? "on" : "off"]) - } - } - - NIconButton { - icon: "refresh" - onClicked: function () { - network.refreshNetworks() - } - } - - NIconButton { - icon: "close" - onClicked: function () { - wifiPanel.visible = false - network.onMenuClosed() - } - } + Connections { + target: wifiPanel + ignoreUnknownSignals: true + function onDismissed() { + wifiPanel.visible = false + network.onMenuClosed() } + } - NDivider {} + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand - ListView { - id: networkList - Layout.fillWidth: true - Layout.fillHeight: true - model: Object.values(network.networks) + Rectangle { + color: Colors.backgroundSecondary + radius: Style.radiusMedium * scaling + border.color: Colors.backgroundTertiary + border.width: Math.max(1, Style.borderMedium * scaling) + width: 340 * scaling + height: 320 * scaling + anchors.top: parent.top + anchors.right: parent.right + anchors.topMargin: Style.marginTiny * scaling + anchors.rightMargin: Style.marginTiny * scaling + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginLarge * scaling spacing: Style.marginMedium * scaling - clip: true - delegate: Item { - width: parent.width - height: modelData.ssid === passwordPromptSsid - && showPasswordPrompt ? 108 : 48 // 48 for network + 60 for password prompt + RowLayout { + Layout.fillWidth: true + spacing: Style.marginMedium * scaling - ColumnLayout { - anchors.fill: parent - spacing: 0 + NText { + text: "wifi" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXL * scaling + color: Colors.accentPrimary + } - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 48 - radius: Style.radiusMedium * scaling - color: modelData.connected ? Colors.accentPrimary : (networkMouseArea.containsMouse ? Colors.highlight : "transparent") + NText { + text: "WiFi" + font.pointSize: Style.fontSizeLarge * scaling + font.bold: true + color: Colors.textPrimary + Layout.fillWidth: true + } - RowLayout { - anchors.fill: parent - anchors.margins: Style.marginSmall * scaling - spacing: Style.marginSmall * scaling - - NText { - text: network.signalIcon(modelData.signal) - font.family: "Material Symbols Outlined" - font.pointSize: Style.fontSizeXL * scaling - color: modelData.connected ? Colors.backgroundPrimary : (networkMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary) - } - - ColumnLayout { - Layout.fillWidth: true - spacing: Style.marginTiny * scaling - - // SSID - NText { - text: modelData.ssid || "Unknown Network" - font.pointSize: Style.fontSizeNormal * scaling - elide: Text.ElideRight - Layout.fillWidth: true - color: modelData.connected ? Colors.backgroundPrimary : (networkMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary) - } - - // Security Protocol - NText { - text: modelData.security && modelData.security !== "--" ? modelData.security : "Open" - font.pointSize: Style.fontSizeTiny * scaling - elide: Text.ElideRight - Layout.fillWidth: true - color: modelData.connected ? Colors.backgroundPrimary : (networkMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary) - } - - Text { - visible: network.connectStatusSsid === modelData.ssid && network.connectStatus === "error" - && network.connectError.length > 0 - text: network.connectError - color: Colors.error - font.pointSize: Style.fontSizeSmall * scaling - elide: Text.ElideRight - Layout.fillWidth: true - } - } - - Item { - Layout.preferredWidth: 22 - Layout.preferredHeight: 22 - visible: network.connectStatusSsid === modelData.ssid - && (network.connectStatus !== "" || network.connectingSsid === modelData.ssid) - - // TBC - // Spinner { - // visible: network.connectingSsid === modelData.ssid - // running: network.connectingSsid === modelData.ssid - // color: Colors.accentPrimary - // anchors.centerIn: parent - // size: 22 - // } - - NText { - visible: network.connectStatus === "success" && !network.connectingSsid - text: "check_circle" - font.family: "Material Symbols Outlined" - font.pointSize: 18 * scaling - color: "#43a047" // TBC: No! - anchors.centerIn: parent - } - - NText { - visible: network.connectStatus === "error" && !network.connectingSsid - text: "error" - font.family: "Material Symbols Outlined" - font.pointSize: Style.fontSizeSmall * scaling - color: Colors.error - anchors.centerIn: parent - } - } - - NText { - visible: modelData.connected - text: "connected" - color: networkMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.accentPrimary - font.pointSize: Style.fontSizeSmall * scaling - } - } - - MouseArea { - id: networkMouseArea - anchors.fill: parent - hoverEnabled: true - onClicked: { - if (modelData.connected) { - network.disconnectNetwork(modelData.ssid) - } else if (network.isSecured(modelData.security) && !modelData.existing) { - passwordPromptSsid = modelData.ssid - showPasswordPrompt = true - passwordInput = "" // Clear previous input - Qt.callLater(function () { - passwordInputField.forceActiveFocus() - }) - } else { - network.connectNetwork(modelData.ssid, modelData.security) - } - } - } + NToggle { + baseSize: Style.baseWidgetSize * 0.75 + value: Settings.data.network.wifiEnabled + onToggled: function (value) { + Settings.data.network.wifiEnabled = value + // TBC: This should be done in a service + Quickshell.execDetached(["nmcli", "radio", "wifi", Settings.data.network.wifiEnabled ? "on" : "off"]) } + } - // Password prompt section - Rectangle { - id: passwordPromptSection - Layout.fillWidth: true - Layout.preferredHeight: modelData.ssid === passwordPromptSsid && showPasswordPrompt ? 60 : 0 - Layout.margins: 8 - visible: modelData.ssid === passwordPromptSsid && showPasswordPrompt - color: Colors.surfaceVariant - radius: Style.radiusSmall * scaling + NIconButton { + icon: "refresh" + onClicked: function () { + network.refreshNetworks() + } + } - RowLayout { - anchors.fill: parent - anchors.margins: Style.spacingSmall * scaling - spacing: Style.spacingSmall * scaling + NIconButton { + icon: "close" + onClicked: function () { + wifiPanel.visible = false + network.onMenuClosed() + } + } + } - Item { - Layout.fillWidth: true - Layout.preferredHeight: 36 + NDivider {} + + ListView { + id: networkList + Layout.fillWidth: true + Layout.fillHeight: true + model: Object.values(network.networks) + spacing: Style.marginMedium * scaling + clip: true + + delegate: Item { + width: parent.width + height: modelData.ssid === passwordPromptSsid + && showPasswordPrompt ? 108 * scaling : Style.baseWidgetSize * 1.5 * scaling + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: Style.baseWidgetSize * 1.5 * scaling + radius: Style.radiusMedium * scaling + color: modelData.connected ? Colors.accentPrimary : (networkMouseArea.containsMouse ? Colors.highlight : "transparent") + + RowLayout { + anchors.fill: parent + anchors.margins: Style.marginSmall * scaling + spacing: Style.marginSmall * scaling + + NText { + text: network.signalIcon(modelData.signal) + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXL * scaling + color: modelData.connected ? Colors.backgroundPrimary : (networkMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary) + } + + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginTiny * scaling + + // SSID + NText { + text: modelData.ssid || "Unknown Network" + font.pointSize: Style.fontSizeNormal * scaling + elide: Text.ElideRight + Layout.fillWidth: true + color: modelData.connected ? Colors.backgroundPrimary : (networkMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary) + } + + // Security Protocol + NText { + text: modelData.security && modelData.security !== "--" ? modelData.security : "Open" + font.pointSize: Style.fontSizeTiny * scaling + elide: Text.ElideRight + Layout.fillWidth: true + color: modelData.connected ? Colors.backgroundPrimary : (networkMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary) + } + + Text { + visible: network.connectStatusSsid === modelData.ssid && network.connectStatus === "error" + && network.connectError.length > 0 + text: network.connectError + color: Colors.error + font.pointSize: Style.fontSizeSmall * scaling + elide: Text.ElideRight + Layout.fillWidth: true + } + } + + Item { + Layout.preferredWidth: 22 + Layout.preferredHeight: 22 + visible: network.connectStatusSsid === modelData.ssid + && (network.connectStatus !== "" || network.connectingSsid === modelData.ssid) + + NBusyIndicator { + visible: network.connectingSsid === modelData.ssid + running: network.connectingSsid === modelData.ssid + color: Colors.accentPrimary + anchors.centerIn: parent + size: 22 * scaling + } + + // TBC: Does nothing on my setup + NText { + visible: network.connectStatus === "success" && !network.connectingSsid + text: "check_circle" + font.family: "Material Symbols Outlined" + font.pointSize: 18 * scaling + color: "#43a047" // TBC: No! + anchors.centerIn: parent + } + + // TBC: Does nothing on my setup + NText { + visible: network.connectStatus === "error" && !network.connectingSsid + text: "error" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeSmall * scaling + color: Colors.error + anchors.centerIn: parent + } + } + + NText { + visible: modelData.connected + text: "connected" + font.pointSize: Style.fontSizeSmall * scaling + color: modelData.connected ? Colors.backgroundPrimary : (networkMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary) + } + } + + MouseArea { + id: networkMouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + if (modelData.connected) { + network.disconnectNetwork(modelData.ssid) + } else if (network.isSecured(modelData.security) && !modelData.existing) { + passwordPromptSsid = modelData.ssid + showPasswordPrompt = true + passwordInput = "" // Clear previous input + Qt.callLater(function () { + passwordInputField.forceActiveFocus() + }) + } else { + network.connectNetwork(modelData.ssid, modelData.security) + } + } + } + } + + // Password prompt section + Rectangle { + id: passwordPromptSection + Layout.fillWidth: true + Layout.preferredHeight: modelData.ssid === passwordPromptSsid && showPasswordPrompt ? 60 : 0 + Layout.margins: 8 + visible: modelData.ssid === passwordPromptSsid && showPasswordPrompt + color: Colors.surfaceVariant + radius: Style.radiusSmall * scaling + + RowLayout { + anchors.fill: parent + anchors.margins: Style.spacingSmall * scaling + spacing: Style.spacingSmall * scaling + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 36 + + Rectangle { + anchors.fill: parent + radius: 8 + color: "transparent" + border.color: passwordInputField.activeFocus ? Colors.accentPrimary : Colors.outline + border.width: 1 + + TextInput { + id: passwordInputField + anchors.fill: parent + anchors.margins: Style.spacingMedium * scaling + text: passwordInput + font.pointSize: Style.fontSizeMedium * scaling + color: Colors.textPrimary + verticalAlignment: TextInput.AlignVCenter + clip: true + focus: true + selectByMouse: true + activeFocusOnTab: true + inputMethodHints: Qt.ImhNone + echoMode: TextInput.Password + onTextChanged: passwordInput = text + onAccepted: { + network.submitPassword(passwordPromptSsid, passwordInput) + showPasswordPrompt = false + } + + MouseArea { + id: passwordInputMouseArea + anchors.fill: parent + onClicked: passwordInputField.forceActiveFocus() + } + } + } + } Rectangle { - anchors.fill: parent - radius: 8 - color: "transparent" - border.color: passwordInputField.activeFocus ? Colors.accentPrimary : Colors.outline - border.width: 1 + Layout.preferredWidth: 80 + Layout.preferredHeight: 36 + radius: Style.radiusMedium * scaling + color: Colors.accentPrimary + border.color: Colors.accentPrimary + border.width: 0 - TextInput { - id: passwordInputField + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + + NText { + anchors.centerIn: parent + text: "Connect" + color: Colors.backgroundPrimary + font.pointSize: Style.fontSizeSmall * scaling + } + + MouseArea { anchors.fill: parent - anchors.margins: Style.spacingMedium * scaling - text: passwordInput - font.pointSize: Style.fontSizeMedium * scaling - color: Colors.textPrimary - verticalAlignment: TextInput.AlignVCenter - clip: true - focus: true - selectByMouse: true - activeFocusOnTab: true - inputMethodHints: Qt.ImhNone - echoMode: TextInput.Password - onTextChanged: passwordInput = text - onAccepted: { + onClicked: { network.submitPassword(passwordPromptSsid, passwordInput) showPasswordPrompt = false } - - MouseArea { - id: passwordInputMouseArea - anchors.fill: parent - onClicked: passwordInputField.forceActiveFocus() - } + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onEntered: parent.color = Qt.darker(Colors.accentPrimary, 1.1) + onExited: parent.color = Colors.accentPrimary } } } - - Rectangle { - Layout.preferredWidth: 80 - Layout.preferredHeight: 36 - radius: Style.radiusMedium * scaling - color: Colors.accentPrimary - border.color: Colors.accentPrimary - border.width: 0 - - Behavior on color { - ColorAnimation { - duration: Style.animationFast - } - } - - NText { - anchors.centerIn: parent - text: "Connect" - color: Colors.backgroundPrimary - font.pointSize: Style.fontSizeSmall * scaling - } - - MouseArea { - anchors.fill: parent - onClicked: { - network.submitPassword(passwordPromptSsid, passwordInput) - showPasswordPrompt = false - } - cursorShape: Qt.PointingHandCursor - hoverEnabled: true - onEntered: parent.color = Qt.darker(Colors.accentPrimary, 1.1) - onExited: parent.color = Colors.accentPrimary - } - } } } } @@ -321,4 +323,3 @@ NLoader { } } } -} \ No newline at end of file diff --git a/Widgets/NBusyIndicator.qml b/Widgets/NBusyIndicator.qml new file mode 100644 index 0000000..0f963e2 --- /dev/null +++ b/Widgets/NBusyIndicator.qml @@ -0,0 +1,56 @@ +import QtQuick +import qs.Services + +Item { + id: root + + readonly property real scaling: Scaling.scale(screen) + + property bool running: false + property color color: "white" + property int size: baseWidgetSize * 0.5 * scaling + property int strokeWidth: 2 * scaling + property int duration: 1000 + + implicitWidth: size + implicitHeight: size + + Canvas { + id: canvas + anchors.fill: parent + + onPaint: { + var ctx = getContext("2d") + ctx.reset() + + var centerX = width / 2 + var centerY = height / 2 + var radius = Math.min(width, height) / 2 - strokeWidth / 2 + + ctx.strokeStyle = root.color + ctx.lineWidth = root.strokeWidth + ctx.lineCap = "round" + + // Draw arc with gap (270 degrees with 90 degree gap) + ctx.beginPath() + ctx.arc(centerX, centerY, radius, -Math.PI / 2 + rotationAngle, -Math.PI / 2 + rotationAngle + Math.PI * 1.5) + ctx.stroke() + } + + property real rotationAngle: 0 + + onRotationAngleChanged: { + requestPaint() + } + + NumberAnimation { + target: canvas + property: "rotationAngle" + running: root.running + from: 0 + to: 2 * Math.PI + duration: root.duration + loops: Animation.Infinite + } + } +} From 5f4e3587a61e4d31160f100f05a1d7c7889c9d25 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 13:45:46 -0400 Subject: [PATCH 122/394] Removed AudioDeviceSelector which will be reimplemented in settings --- Modules/Audio/AudioDeviceSelector.qml | 388 -------------------------- 1 file changed, 388 deletions(-) delete mode 100644 Modules/Audio/AudioDeviceSelector.qml diff --git a/Modules/Audio/AudioDeviceSelector.qml b/Modules/Audio/AudioDeviceSelector.qml deleted file mode 100644 index d8253c2..0000000 --- a/Modules/Audio/AudioDeviceSelector.qml +++ /dev/null @@ -1,388 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell -import Quickshell.Services.Pipewire -import qs.Services -import qs.Widgets - -NLoader { - id: root - - content: Component { - NPanel { - id: demoPanel - - readonly property real scaling: Scaling.scale(screen) - - // Ensure panel shows itself once created - Component.onCompleted: show() - - Rectangle { - color: Colors.backgroundPrimary - radius: Style.radiusMedium * scaling - border.color: Colors.backgroundTertiary - border.width: Math.max(1, Style.borderMedium * scaling) - width: 500 * scaling - height: 400 - anchors.centerIn: parent - - // Prevent closing when clicking in the panel bg - MouseArea { - anchors.fill: parent - } - - NText { - text: "Audio Device Selector" - } - } - } - } -} // NPanel {// id: ioSelector// property int tabIndex: 0// property Item anchorItem: null// signal panelClosed()// function sinkNodes() {// let nodes = Pipewire.nodes && Pipewire.nodes.values ? Pipewire.nodes.values.filter(function(n) {// return n.isSink && n.audio && n.isStream === false;// }) : [];// if (Pipewire.defaultAudioSink)// nodes = nodes.slice().sort(function(a, b) {// if (a.id === Pipewire.defaultAudioSink.id)// return -1; - -// if (b.id === Pipewire.defaultAudioSink.id) -// return 1; - -// return 0; -// }); - -// return nodes; -// } - -// function sourceNodes() { -// let nodes = Pipewire.nodes && Pipewire.nodes.values ? Pipewire.nodes.values.filter(function(n) { -// return !n.isSink && n.audio && n.isStream === false; -// }) : []; -// if (Pipewire.defaultAudioSource) -// nodes = nodes.slice().sort(function(a, b) { -// if (a.id === Pipewire.defaultAudioSource.id) -// return -1; - -// if (b.id === Pipewire.defaultAudioSource.id) -// return 1; - -// return 0; -// }); - -// return nodes; -// } - -// Component.onCompleted: { -// if (Pipewire.nodes && Pipewire.nodes.values) { -// for (var i = 0; i < Pipewire.nodes.values.length; ++i) { -// var n = Pipewire.nodes.values[i]; -// } -// } -// } -// Component.onDestruction: { -// } -// onVisibleChanged: { -// if (!visible) -// panelClosed(); - -// } - -// // Bind all Pipewire nodes so their properties are valid -// PwObjectTracker { -// id: nodeTracker - -// objects: Pipewire.nodes -// } - -// Rectangle { -// color: Theme.backgroundPrimary -// radius: 20 -// width: 340 -// height: 340 -// anchors.top: parent.top -// anchors.right: parent.right -// anchors.topMargin: 4 -// anchors.rightMargin: 4 - -// // Prevent closing when clicking in the panel bg -// MouseArea { -// anchors.fill: parent -// } - -// ColumnLayout { -// anchors.fill: parent -// anchors.margins: 16 -// spacing: 10 - -// // Tabs centered inside the window -// RowLayout { -// Layout.fillWidth: true -// Layout.alignment: Qt.AlignHCenter -// spacing: 0 - -// Tabs { -// id: ioTabs - -// tabsModel: [{ -// "label": "Output", -// "icon": "volume_up" -// }, { -// "label": "Input", -// "icon": "mic" -// }] -// currentIndex: tabIndex -// onTabChanged: { -// tabIndex = currentIndex; -// } -// } - -// } - -// // Add vertical space between tabs and entries -// Item { -// height: 36 -// Layout.fillWidth: true -// } - -// // Output Devices -// Flickable { -// id: sinkList - -// visible: tabIndex === 0 -// contentHeight: sinkColumn.height -// clip: true -// interactive: contentHeight > height -// width: parent.width -// height: 220 - -// ColumnLayout { -// id: sinkColumn - -// width: sinkList.width -// spacing: 6 - -// Repeater { -// model: ioSelector.sinkNodes() - -// Rectangle { -// width: parent.width -// height: 36 -// color: "transparent" -// radius: 6 - -// RowLayout { -// anchors.fill: parent -// anchors.margins: 6 -// spacing: 8 - -// Text { -// text: "volume_up" -// font.family: "Material Symbols Outlined" -// font.pixelSize: 16 * Theme.scale(screen) -// color: (Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary -// Layout.alignment: Qt.AlignVCenter -// } - -// ColumnLayout { -// Layout.fillWidth: true -// spacing: 1 -// Layout.maximumWidth: sinkList.width - 120 // Reserve space for the Set button - -// Text { -// text: modelData.nickname || modelData.description || modelData.name -// font.bold: true -// font.pixelSize: 12 * Theme.scale(screen) -// color: (Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary -// elide: Text.ElideRight -// maximumLineCount: 1 -// Layout.fillWidth: true -// } - -// Text { -// text: modelData.description !== modelData.nickname ? modelData.description : "" -// font.pixelSize: 10 * Theme.scale(screen) -// color: Theme.textSecondary -// elide: Text.ElideRight -// maximumLineCount: 1 -// Layout.fillWidth: true -// } - -// } - -// Rectangle { -// visible: Pipewire.preferredDefaultAudioSink !== modelData -// width: 60 -// height: 20 -// radius: 4 -// color: Theme.accentPrimary -// border.color: Theme.accentPrimary -// border.width: 1 -// Layout.alignment: Qt.AlignVCenter - -// Text { -// anchors.centerIn: parent -// text: "Set" -// color: Theme.onAccent -// font.pixelSize: 10 * Theme.scale(screen) -// font.bold: true -// } - -// MouseArea { -// anchors.fill: parent -// cursorShape: Qt.PointingHandCursor -// onClicked: Pipewire.preferredDefaultAudioSink = modelData -// } - -// } - -// Text { -// text: "(Current)" -// visible: Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === modelData.id -// color: Theme.accentPrimary -// font.pixelSize: 10 * Theme.scale(screen) -// Layout.alignment: Qt.AlignVCenter -// } - -// } - -// } - -// } - -// } - -// ScrollBar.vertical: ScrollBar { -// } - -// } - -// // Input Devices -// Flickable { -// id: sourceList - -// visible: tabIndex === 1 -// contentHeight: sourceColumn.height -// clip: true -// interactive: contentHeight > height -// width: parent.width -// height: 220 - -// ColumnLayout { -// id: sourceColumn - -// width: sourceList.width -// spacing: 6 - -// Repeater { -// model: ioSelector.sourceNodes() - -// Rectangle { -// width: parent.width -// height: 36 -// color: "transparent" -// radius: 6 - -// RowLayout { -// anchors.fill: parent -// anchors.margins: 6 -// spacing: 8 - -// Text { -// text: "mic" -// font.family: "Material Symbols Outlined" -// font.pixelSize: 16 * Theme.scale(screen) -// color: (Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary -// Layout.alignment: Qt.AlignVCenter -// } - -// ColumnLayout { -// Layout.fillWidth: true -// spacing: 1 -// Layout.maximumWidth: sourceList.width - 120 // Reserve space for the Set button - -// Text { -// text: modelData.nickname || modelData.description || modelData.name -// font.bold: true -// font.pixelSize: 12 * Theme.scale(screen) -// color: (Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id) ? Theme.accentPrimary : Theme.textPrimary -// elide: Text.ElideRight -// maximumLineCount: 1 -// Layout.fillWidth: true -// } - -// Text { -// text: modelData.description !== modelData.nickname ? modelData.description : "" -// font.pixelSize: 10 * Theme.scale(screen) -// color: Theme.textSecondary -// elide: Text.ElideRight -// maximumLineCount: 1 -// Layout.fillWidth: true -// } - -// } - -// Rectangle { -// visible: Pipewire.preferredDefaultAudioSource !== modelData -// width: 60 -// height: 20 -// radius: 4 -// color: Theme.accentPrimary -// border.color: Theme.accentPrimary -// border.width: 1 -// Layout.alignment: Qt.AlignVCenter - -// Text { -// anchors.centerIn: parent -// text: "Set" -// color: Theme.onAccent -// font.pixelSize: 10 * Theme.scale(screen) -// font.bold: true -// } - -// MouseArea { -// anchors.fill: parent -// cursorShape: Qt.PointingHandCursor -// onClicked: Pipewire.preferredDefaultAudioSource = modelData -// } - -// } - -// Text { -// text: "(Current)" -// visible: Pipewire.defaultAudioSource && Pipewire.defaultAudioSource.id === modelData.id -// color: Theme.accentPrimary -// font.pixelSize: 10 * Theme.scale(screen) -// Layout.alignment: Qt.AlignVCenter -// } - -// } - -// } - -// } - -// } - -// ScrollBar.vertical: ScrollBar { -// } - -// } - -// } - -// } - -// Connections { -// function onReadyChanged() { -// if (Pipewire.ready && Pipewire.nodes && Pipewire.nodes.values) { -// for (var i = 0; i < Pipewire.nodes.values.length; ++i) { -// var n = Pipewire.nodes.values[i]; -// } -// } -// } - -// function onDefaultAudioSinkChanged() { -// } - -// function onDefaultAudioSourceChanged() { -// } - -// target: Pipewire -// } -// } - From ebdc0a458f334d9adceeb62aaac8c5ce859efd2f Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 13:46:48 -0400 Subject: [PATCH 123/394] AudioDeviceSelector: really bye bye --- shell.qml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/shell.qml b/shell.qml index b3f16c9..9c1ba50 100644 --- a/shell.qml +++ b/shell.qml @@ -35,10 +35,6 @@ ShellRoot { id: notification } - AudioDeviceSelector { - id: audioDeviceSelector - } - Calendar { id: calendar } From a0df2a7af0391e7b0387d0d5954e4cd4d84f3abf Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 13:46:59 -0400 Subject: [PATCH 124/394] Removed rippleEffect color --- Assets/Wallust/Templates/noctalia.json | 2 -- Services/Colors.qml | 6 ++---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Assets/Wallust/Templates/noctalia.json b/Assets/Wallust/Templates/noctalia.json index 22da0f5..f10f03e 100644 --- a/Assets/Wallust/Templates/noctalia.json +++ b/Assets/Wallust/Templates/noctalia.json @@ -18,8 +18,6 @@ "warning": "{{ color5 | lighten(0.3) }}", "highlight": "{{ color4 | lighten(0.4) }}", - "rippleEffect": "{{ color4 | lighten(0.1) }}", - "onAccent": "{{ background }}", "outline": "{{ background | lighten(0.3) }}", diff --git a/Services/Colors.qml b/Services/Colors.qml index d8ad5e6..25e6b74 100644 --- a/Services/Colors.qml +++ b/Services/Colors.qml @@ -31,9 +31,8 @@ Singleton { property color error: themeData.error property color warning: themeData.warning - // Highlights & Focus + // Highlights property color highlight: themeData.highlight - property color rippleEffect: themeData.rippleEffect // Additional Theme Properties property color onAccent: themeData.onAccent @@ -86,9 +85,8 @@ Singleton { property string error: "#eb6f92" property string warning: "#f6c177" - // Highlights & Focus + // Highlights property string highlight: "#c4a7e7" - property string rippleEffect: "#9ccfd8" // Additional Theme Properties property string onAccent: "#191724" From bff8049f2ccd033f9cfaee246d5fd4714c71dde2 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 13:59:02 -0400 Subject: [PATCH 125/394] WifiMenu : Text -> NText --- Modules/Bar/WiFiMenu.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Bar/WiFiMenu.qml b/Modules/Bar/WiFiMenu.qml index 9ee0979..b0db279 100644 --- a/Modules/Bar/WiFiMenu.qml +++ b/Modules/Bar/WiFiMenu.qml @@ -150,7 +150,7 @@ NLoader { color: modelData.connected ? Colors.backgroundPrimary : (networkMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary) } - Text { + NText { visible: network.connectStatusSsid === modelData.ssid && network.connectStatus === "error" && network.connectError.length > 0 text: network.connectError From 06c0c57a2304f4739ea4594d936ae402e59c98c9 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 14:33:32 -0400 Subject: [PATCH 126/394] NComboBox looking almost great --- Assets/Colors/default.json | 2 - Modules/DemoPanel/DemoPanel.qml | 83 +++++++++++++++++----------- Widgets/NComboBox.qml | 96 +++++++++++++++++++++++++++++++++ 3 files changed, 148 insertions(+), 33 deletions(-) create mode 100644 Widgets/NComboBox.qml diff --git a/Assets/Colors/default.json b/Assets/Colors/default.json index a89ca83..74a6369 100644 --- a/Assets/Colors/default.json +++ b/Assets/Colors/default.json @@ -18,8 +18,6 @@ "warning": "#f6c177", "highlight": "#c4a7e7", - "rippleEffect": "#31748f", - "onAccent": "#191724", "outline": "#44415a", diff --git a/Modules/DemoPanel/DemoPanel.qml b/Modules/DemoPanel/DemoPanel.qml index 0877655..331a989 100644 --- a/Modules/DemoPanel/DemoPanel.qml +++ b/Modules/DemoPanel/DemoPanel.qml @@ -38,6 +38,49 @@ NLoader { anchors.margins: Style.marginXL * scaling spacing: Style.marginSmall * scaling + // NSlider + ColumnLayout { + spacing: 16 * scaling + NText { + text: "Scaling" + color: Colors.accentSecondary + font.weight: Style.fontWeightBold + } + RowLayout { + spacing: Style.marginSmall * scaling + NText { + text: `${Math.round(Scaling.overrideScale * 100)}%` + Layout.alignment: Qt.AlignVCenter + } + NSlider { + id: scaleSlider + from: 0.6 + to: 1.8 + stepSize: 0.01 + value: Scaling.overrideScale + implicitWidth: bgRect.width * 0.75 + onMoved: function () { + Scaling.overrideScale = value + } + onPressedChanged: function () { + Scaling.overrideEnabled = true + } + } + NIconButton { + icon: "refresh" + sizeMultiplier: 1.0 + fontPointSize: Style.fontSizeXL * scaling + onClicked: function () { + Scaling.overrideEnabled = false + Scaling.overrideScale = 1.0 + } + } + } + NDivider { + Layout.fillWidth: true + } + } + // NIconButton ColumnLayout { spacing: 16 * scaling @@ -81,44 +124,22 @@ NLoader { } } - // NSlider + // NComboBox ColumnLayout { - spacing: 16 * scaling + spacing: Style.marginLarge * scaling NText { - text: "Scaling" + text: "NComboBox" color: Colors.accentSecondary font.weight: Style.fontWeightBold } - RowLayout { - spacing: Style.marginSmall * scaling - NText { - text: `${Math.round(Scaling.overrideScale * 100)}%` - Layout.alignment: Qt.AlignVCenter - } - NSlider { - id: scaleSlider - from: 0.6 - to: 1.8 - stepSize: 0.01 - value: Scaling.overrideScale - implicitWidth: bgRect.width * 0.75 - onMoved: function () { - Scaling.overrideScale = value - } - onPressedChanged: function () { - Scaling.overrideEnabled = true - } - } - NIconButton { - icon: "refresh" - sizeMultiplier: 1.0 - fontPointSize: Style.fontSizeXL * scaling - onClicked: function () { - Scaling.overrideEnabled = false - Scaling.overrideScale = 1.0 - } + + NComboBox {// label: "Label" + // description: "Description" + onSelected: function (value) { + console.log("NComboBox: " + value) } } + NDivider { Layout.fillWidth: true } diff --git a/Widgets/NComboBox.qml b/Widgets/NComboBox.qml new file mode 100644 index 0000000..64fba2e --- /dev/null +++ b/Widgets/NComboBox.qml @@ -0,0 +1,96 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Services +import qs.Widgets + +ComboBox { + id: root + + readonly property real scaling: Scaling.scale(screen) + property list optionsKeys: ['cat', 'dog', 'bird'] + property list optionsLabels: ['Cat ', 'Dog', 'Bird'] + property string currentKey: "cat" + property var onSelected: function (string) {} + + Layout.fillWidth: true + Layout.preferredHeight: Style.baseWidgetSize * scaling + + model: optionsKeys + currentIndex: model.indexOf(currentKey) + onActivated: { + root.onSelected(model[currentIndex]) + } + + // Rounded background + background: Rectangle { + implicitWidth: 120 * scaling + implicitHeight: 40 * scaling + color: Colors.surfaceVariant + border.color: root.activeFocus ? Colors.highlight : Colors.outline + border.width: Math.max(1, Style.borderThin * scaling) + radius: Style.radiusMedium * scaling + } + + // Label (currently selected) + contentItem: NText { + leftPadding: Style.spacingLarge * scaling + rightPadding: root.indicator.width + Style.spacingLarge * scaling + font.pointSize: Style.fontSizeMedium * scaling + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + text: { + return root.optionsLabels[root.currentIndex] + } + } + + // Drop down indicator + indicator: NText { + x: root.width - width - Style.spacingMedium * scaling + y: root.topPadding + (root.availableHeight - height) / 2 + text: "arrow_drop_down" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXL * scaling + } + + popup: Popup { + y: root.height + width: root.width + implicitHeight: contentItem.implicitHeight + padding: 1 + + contentItem: ListView { + clip: true + implicitHeight: contentHeight + model: root.popup.visible ? root.delegateModel : null + currentIndex: root.highlightedIndex + ScrollIndicator.vertical: ScrollIndicator {} + } + + background: Rectangle { + color: Colors.surfaceVariant + border.color: Colors.outline + border.width: Math.max(1, Style.borderThin * scaling) + radius: Style.radiusMedium * scaling + } + } + + delegate: ItemDelegate { + width: root.width + highlighted: root.highlightedIndex === index + + contentItem: Text { + text: { + return root.optionsLabels[root.model.indexOf(modelData)] + } + font.pointSize: Style.fontSizeSmall * scaling + color: highlighted ? Colors.backgroundPrimary : Colors.textPrimary + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + background: Rectangle { + color: highlighted ? Colors.highlight : "transparent" + } + } +} From 3e46eed9a1ef811387bcc412a83ded3c67ed34fd Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 14:45:59 -0400 Subject: [PATCH 127/394] NComboBox completed --- Widgets/NComboBox.qml | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/Widgets/NComboBox.qml b/Widgets/NComboBox.qml index 64fba2e..cf24759 100644 --- a/Widgets/NComboBox.qml +++ b/Widgets/NComboBox.qml @@ -8,8 +8,8 @@ ComboBox { id: root readonly property real scaling: Scaling.scale(screen) - property list optionsKeys: ['cat', 'dog', 'bird'] - property list optionsLabels: ['Cat ', 'Dog', 'Bird'] + property list optionsKeys: ['cat', 'dog', 'bird', 'monkey', 'fish', 'turtle', 'elephant', 'tiger'] + property list optionsLabels: ['Cat ', 'Dog', 'Bird', 'Monkey', 'Fish', 'Turtle', 'Elephant', 'Tiger'] property string currentKey: "cat" property var onSelected: function (string) {} @@ -25,7 +25,7 @@ ComboBox { // Rounded background background: Rectangle { implicitWidth: 120 * scaling - implicitHeight: 40 * scaling + implicitHeight: Style.baseWidgetSize * scaling color: Colors.surfaceVariant border.color: root.activeFocus ? Colors.highlight : Colors.outline border.width: Math.max(1, Style.borderThin * scaling) @@ -37,6 +37,7 @@ ComboBox { leftPadding: Style.spacingLarge * scaling rightPadding: root.indicator.width + Style.spacingLarge * scaling font.pointSize: Style.fontSizeMedium * scaling + font.weight: Style.fontWeightBold verticalAlignment: Text.AlignVCenter elide: Text.ElideRight text: { @@ -56,8 +57,8 @@ ComboBox { popup: Popup { y: root.height width: root.width - implicitHeight: contentItem.implicitHeight - padding: 1 + implicitHeight: Math.min(160 * scaling, contentItem.implicitHeight + Style.spacingMedium * scaling * 2) + padding: Style.spacingMedium * scaling contentItem: ListView { clip: true @@ -79,18 +80,20 @@ ComboBox { width: root.width highlighted: root.highlightedIndex === index - contentItem: Text { + contentItem: NText { text: { return root.optionsLabels[root.model.indexOf(modelData)] } - font.pointSize: Style.fontSizeSmall * scaling + font.pointSize: Style.fontSizeMedium * scaling color: highlighted ? Colors.backgroundPrimary : Colors.textPrimary verticalAlignment: Text.AlignVCenter elide: Text.ElideRight } background: Rectangle { + width: root.width - Style.spacingMedium * scaling * 3 color: highlighted ? Colors.highlight : "transparent" + radius: Style.radiusSmall * scaling } } } From 8cc3c47f097cc4b5f244a011ef71198f43eb3196 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 14:54:54 -0400 Subject: [PATCH 128/394] NComboBox not default options --- Modules/DemoPanel/DemoPanel.qml | 8 +++++--- Widgets/NComboBox.qml | 9 +++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Modules/DemoPanel/DemoPanel.qml b/Modules/DemoPanel/DemoPanel.qml index 331a989..f2ca9fd 100644 --- a/Modules/DemoPanel/DemoPanel.qml +++ b/Modules/DemoPanel/DemoPanel.qml @@ -133,10 +133,12 @@ NLoader { font.weight: Style.fontWeightBold } - NComboBox {// label: "Label" - // description: "Description" + NComboBox { + optionsKeys: ["cat", "dog", "bird", "monkey", "fish", "turtle", "elephant", "tiger"] + optionsLabels: ["Cat", "Dog", "Bird", "Monkey", "Fish", "Turtle", "Elephant", "Tiger"] + currentKey: "cat" onSelected: function (value) { - console.log("NComboBox: " + value) + console.log("NComboBox: selected " + value) } } diff --git a/Widgets/NComboBox.qml b/Widgets/NComboBox.qml index cf24759..a936170 100644 --- a/Widgets/NComboBox.qml +++ b/Widgets/NComboBox.qml @@ -8,9 +8,10 @@ ComboBox { id: root readonly property real scaling: Scaling.scale(screen) - property list optionsKeys: ['cat', 'dog', 'bird', 'monkey', 'fish', 'turtle', 'elephant', 'tiger'] - property list optionsLabels: ['Cat ', 'Dog', 'Bird', 'Monkey', 'Fish', 'Turtle', 'Elephant', 'Tiger'] - property string currentKey: "cat" + + property list optionsKeys: [] + property list optionsLabels: [] + property string currentKey: '' property var onSelected: function (string) {} Layout.fillWidth: true @@ -19,7 +20,7 @@ ComboBox { model: optionsKeys currentIndex: model.indexOf(currentKey) onActivated: { - root.onSelected(model[currentIndex]) + root.onSelected(model[currentIndex]) } // Rounded background From ab7bb4a6d4e1e81d760db941d6e7cd6a21532f08 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 15:06:54 -0400 Subject: [PATCH 129/394] Better busy indicator defaults --- Modules/Bar/WiFiMenu.qml | 2 +- Modules/DemoPanel/DemoPanel.qml | 25 +++++++++++++++++++++---- Widgets/NBusyIndicator.qml | 10 +++++----- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/Modules/Bar/WiFiMenu.qml b/Modules/Bar/WiFiMenu.qml index b0db279..ed5dca6 100644 --- a/Modules/Bar/WiFiMenu.qml +++ b/Modules/Bar/WiFiMenu.qml @@ -172,7 +172,7 @@ NLoader { running: network.connectingSsid === modelData.ssid color: Colors.accentPrimary anchors.centerIn: parent - size: 22 * scaling + size: Style.baseWidgetSize * 0.7 * scaling } // TBC: Does nothing on my setup diff --git a/Modules/DemoPanel/DemoPanel.qml b/Modules/DemoPanel/DemoPanel.qml index f2ca9fd..e06d04d 100644 --- a/Modules/DemoPanel/DemoPanel.qml +++ b/Modules/DemoPanel/DemoPanel.qml @@ -24,8 +24,8 @@ NLoader { radius: Style.radiusMedium * scaling border.color: Colors.backgroundTertiary border.width: Math.max(1, Style.borderMedium * scaling) - width: 500 * scaling - height: 400 * scaling + width: 600 * scaling + height: 600 * scaling anchors.centerIn: parent // Prevent closing when clicking in the panel bg @@ -104,7 +104,7 @@ NLoader { // NToggle ColumnLayout { - spacing: Style.marginLarge * scaling + spacing: Style.marginMedium * scaling NText { text: "NToggle" color: Colors.accentSecondary @@ -126,7 +126,7 @@ NLoader { // NComboBox ColumnLayout { - spacing: Style.marginLarge * scaling + spacing: Style.marginMedium * scaling NText { text: "NComboBox" color: Colors.accentSecondary @@ -146,6 +146,23 @@ NLoader { Layout.fillWidth: true } } + + // NBusyIndicator + ColumnLayout { + spacing: Style.marginMedium * scaling + NText { + text: "NBusyIndicator" + color: Colors.accentSecondary + font.weight: Style.fontWeightBold + } + + NBusyIndicator { + } + + NDivider { + Layout.fillWidth: true + } + } } } } diff --git a/Widgets/NBusyIndicator.qml b/Widgets/NBusyIndicator.qml index 0f963e2..d405449 100644 --- a/Widgets/NBusyIndicator.qml +++ b/Widgets/NBusyIndicator.qml @@ -6,10 +6,10 @@ Item { readonly property real scaling: Scaling.scale(screen) - property bool running: false - property color color: "white" - property int size: baseWidgetSize * 0.5 * scaling - property int strokeWidth: 2 * scaling + property bool running: true + property color color: Colors.accentPrimary + property int size: Style.baseWidgetSize * scaling + property int strokeWidth: Style.borderThick * scaling property int duration: 1000 implicitWidth: size @@ -28,7 +28,7 @@ Item { var radius = Math.min(width, height) / 2 - strokeWidth / 2 ctx.strokeStyle = root.color - ctx.lineWidth = root.strokeWidth + ctx.lineWidth = Math.max(1, root.strokeWidth) ctx.lineCap = "round" // Draw arc with gap (270 degrees with 90 degree gap) From 924ecb7ece596ba404bd2bfb8b165d07e448b4ad Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 15:22:29 -0400 Subject: [PATCH 130/394] Test case for improper scaling of Rectangles when below Layout --- Modules/DemoPanel/DemoPanel.qml | 17 +++++++++++++++-- Widgets/NComboBox.qml | 2 +- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/Modules/DemoPanel/DemoPanel.qml b/Modules/DemoPanel/DemoPanel.qml index e06d04d..76ab436 100644 --- a/Modules/DemoPanel/DemoPanel.qml +++ b/Modules/DemoPanel/DemoPanel.qml @@ -33,6 +33,13 @@ NLoader { anchors.fill: parent } + // A basic rectangle, which scale ok because out of layout + Rectangle { + width: Style.baseWidgetSize * scaling + height: Style.baseWidgetSize * scaling + color: Colors.accentTertiary + } + ColumnLayout { anchors.fill: parent anchors.margins: Style.marginXL * scaling @@ -156,13 +163,19 @@ NLoader { font.weight: Style.fontWeightBold } - NBusyIndicator { - } + NBusyIndicator {} NDivider { Layout.fillWidth: true } } + + // A basic rectangle, which does not scale right?! + Rectangle { + width: Style.baseWidgetSize * scaling + height: Style.baseWidgetSize * scaling + color: Colors.accentPrimary + } } } } diff --git a/Widgets/NComboBox.qml b/Widgets/NComboBox.qml index a936170..459117a 100644 --- a/Widgets/NComboBox.qml +++ b/Widgets/NComboBox.qml @@ -8,7 +8,7 @@ ComboBox { id: root readonly property real scaling: Scaling.scale(screen) - + property list optionsKeys: [] property list optionsLabels: [] property string currentKey: '' From be4da5efaf2142ee1b67df540dbd2c0652cd1403 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 15:31:05 -0400 Subject: [PATCH 131/394] NToggle: finaly fixed the scaling issues --- Widgets/NToggle.qml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Widgets/NToggle.qml b/Widgets/NToggle.qml index 55eb098..5150cd2 100644 --- a/Widgets/NToggle.qml +++ b/Widgets/NToggle.qml @@ -39,16 +39,16 @@ RowLayout { Rectangle { id: switcher - width: root.baseSize * 1.625 * scaling - height: root.baseSize * scaling + implicitWidth: root.baseSize * 1.625 * scaling + implicitHeight: root.baseSize * scaling radius: height * 0.5 color: value ? Colors.accentPrimary : Colors.surfaceVariant border.color: value ? Colors.accentPrimary : Colors.outline border.width: Math.max(1, Style.borderMedium * scaling) Rectangle { - width: (root.baseSize - 4) * scaling - height: (root.baseSize - 4) * scaling + implicitWidth: (root.baseSize - 4) * scaling + implicitHeight: (root.baseSize - 4) * scaling radius: height * 0.5 color: Colors.surface border.color: hovering ? Colors.textDisabled : Colors.outline From 457033b9790fadd9dfa979358078c615683ee4dc Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 15:40:07 -0400 Subject: [PATCH 132/394] Cleaned up demopanel --- Modules/DemoPanel/DemoPanel.qml | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/Modules/DemoPanel/DemoPanel.qml b/Modules/DemoPanel/DemoPanel.qml index 76ab436..e66541d 100644 --- a/Modules/DemoPanel/DemoPanel.qml +++ b/Modules/DemoPanel/DemoPanel.qml @@ -33,13 +33,6 @@ NLoader { anchors.fill: parent } - // A basic rectangle, which scale ok because out of layout - Rectangle { - width: Style.baseWidgetSize * scaling - height: Style.baseWidgetSize * scaling - color: Colors.accentTertiary - } - ColumnLayout { anchors.fill: parent anchors.margins: Style.marginXL * scaling @@ -169,13 +162,6 @@ NLoader { Layout.fillWidth: true } } - - // A basic rectangle, which does not scale right?! - Rectangle { - width: Style.baseWidgetSize * scaling - height: Style.baseWidgetSize * scaling - color: Colors.accentPrimary - } } } } From e1edfd3689678560ee5ff2f46cb851408552e593 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 15:40:39 -0400 Subject: [PATCH 133/394] Settings Service: Create the .cache/noctalia folder, use default wallpaper --- Modules/Background/Background.qml | 3 +-- Services/Settings.qml | 20 ++++++++++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/Modules/Background/Background.qml b/Modules/Background/Background.qml index e6216df..a48f660 100644 --- a/Modules/Background/Background.qml +++ b/Modules/Background/Background.qml @@ -8,9 +8,8 @@ Variants { delegate: PanelWindow { required property ShellScreen modelData - property string wallpaperSource: Qt.resolvedUrl("../../Assets/Tests/wallpaper.png") + property string wallpaperSource: Settings.data.wallpaper.current - //property string wallpaperSource: Qt.resolvedUrl("/home/lysec/Pictures/wallpapers/wallhaven-6lqvql.jpg") visible: wallpaperSource !== "" color: "transparent" screen: modelData diff --git a/Services/Settings.qml b/Services/Settings.qml index 7b33d9a..5d20d13 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -5,20 +5,28 @@ import qs.Services pragma Singleton Singleton { + id: root + property string shellName: "noctalia" - property string settingsDir: Quickshell.env("NOCTALIA_SETTINGS_DIR") + property string configDir: Quickshell.env("NOCTALIA_CONFIG_DIR") || (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env( "HOME") + "/.config") + "/" + shellName + "/" - property string settingsFile: Quickshell.env("NOCTALIA_SETTINGS_FILE") || (settingsDir + "settings.json") - property string colorsFile: Quickshell.env("NOCTALIA_COLORS_FILE") || (settingsDir + "colors.json") + property string cacheDir: Quickshell.env("NOCTALIA_CACHE_DIR") + || (Quickshell.env("XDG_CACHE_HOME") || Quickshell.env( + "HOME") + "/.cache") + "/" + shellName + "/" + property string settingsFile: Quickshell.env("NOCTALIA_SETTINGS_FILE") || (configDir + "settings.json") + property string colorsFile: Quickshell.env("NOCTALIA_COLORS_FILE") || (configDir + "colors.json") property var data: settingAdapter + property string defaultWallpaper: Qt.resolvedUrl("../Assets/Tests/wallpaper.png") + // Needed to only have one NPanel loaded at a time. // property var openPanel: null Item { Component.onCompleted: { - // ensure settings dir - Quickshell.execDetached(["mkdir", "-p", settingsDir]) + // ensure settings dir exists + Quickshell.execDetached(["mkdir", "-p", configDir]) + Quickshell.execDetached(["mkdir", "-p", cacheDir]) } } @@ -96,7 +104,7 @@ Singleton { wallpaper: JsonObject { property string directory: "/usr/share/wallpapers" - property string current: "" + property string current: defaultWallpaper property bool isRandom: false property int randomInterval: 300 property bool generateTheme: false From 330f2e964c9262538be8a060a4777f170163244f Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 16:35:48 -0400 Subject: [PATCH 134/394] Location Service WIP --- Services/Location.qml | 126 ++++++++++++++++++++++++++++++++++++++++-- Services/Settings.qml | 29 ++++++---- 2 files changed, 139 insertions(+), 16 deletions(-) diff --git a/Services/Location.qml b/Services/Location.qml index 64d8820..95336ab 100644 --- a/Services/Location.qml +++ b/Services/Location.qml @@ -1,10 +1,128 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Services pragma Singleton -import Quickshell -import qs.Services - // Weather logic and caching -// Calendar Hollidays logic and caching Singleton { id: root + + property string locationFile: Quickshell.env("NOCTALIA_WEATHER_FILE") || (Settings.cacheDir + "location.json") + + // Used to access via Location.data.xxx.yyy + property var data: adapter + + function quickstart() { + console.log(locationFile) + } + + FileView { + path: locationFile + watchChanges: true + onFileChanged: reload() + onAdapterUpdated: writeAdapter() + Component.onCompleted: function () { + reload() + } + onLoaded: function () {} + onLoadFailed: function (error) { + if (error.toString().includes("No such file") || error === 2) + // File doesn't exist, create it with default values + writeAdapter() + } + + JsonAdapter { + id: adapter + + // main + property JsonObject main + + main: JsonObject { + property string latitude: "" + property string longitude: "" + property int weatherLastFetched: 0 + } + + // weather + property JsonObject weather + + weather: JsonObject { + } + } + } + + // -------------------------------- + function getWeather() { + if (data.main.latitude === "" || data.main.longitude === "") { + geocodeLocation(Settings.data.location.name, function (lat, lon) { + console.log(Settings.data.location.name + ": " + lat + " / " + lon); + }) + } + } + + // -------------------------------- + function geocodeLocation(locationName, callback, errorCallback) { + var geoUrl = "https://geocoding-api.open-meteo.com/v1/search?name=" + encodeURIComponent( + locationName) + "&language=en&format=json" + var xhr = new XMLHttpRequest() + xhr.onreadystatechange = function () { + if (xhr.readyState === XMLHttpRequest.DONE) { + if (xhr.status === 200) { + try { + var geoData = JSON.parse(xhr.responseText) + if (geoData.results && geoData.results.length > 0) { + callback(geoData.results[0].latitude, geoData.results[0].longitude) + } else { + errorCallback("Location not found.") + } + } catch (e) { + errorCallback("Failed to parse geocoding data.") + } + } else { + errorCallback("Geocoding error: " + xhr.status) + } + } + } + xhr.open("GET", geoUrl) + xhr.send() + } + + // -------------------------------- + function fetchWeather(latitude, longitude, callback, errorCallback) { + var url = "https://api.open-meteo.com/v1/forecast?latitude=" + latitude + "&longitude=" + longitude + + "¤t_weather=true¤t=relativehumidity_2m,surface_pressure&daily=temperature_2m_max,temperature_2m_min,weathercode&timezone=auto" + var xhr = new XMLHttpRequest() + xhr.onreadystatechange = function () { + if (xhr.readyState === XMLHttpRequest.DONE) { + if (xhr.status === 200) { + try { + var weatherData = JSON.parse(xhr.responseText) + callback(weatherData) + } catch (e) { + errorCallback("Failed to parse weather data.") + } + } else { + errorCallback("Weather fetch error: " + xhr.status) + } + } + } + xhr.open("GET", url) + xhr.send() + } + + + + // function fetchCityWeather(city, callback, errorCallback) { + // fetchCoordinates(city, function (lat, lon) { + // fetchWeather(lat, lon, function (weatherData) { + // callback({ + // "city": city, + // "latitude": lat, + // "longitude": lon, + // "weather": weatherData + // }) + // }, errorCallback) + // }, errorCallback) + // } } diff --git a/Services/Settings.qml b/Services/Settings.qml index 5d20d13..647bdec 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -7,23 +7,30 @@ pragma Singleton Singleton { id: root + // Define our app directories + // Default config directory: ~/.config/noctalia + // Default cache directory: ~/.cache/noctalia property string shellName: "noctalia" - property string configDir: Quickshell.env("NOCTALIA_CONFIG_DIR") - || (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env( - "HOME") + "/.config") + "/" + shellName + "/" - property string cacheDir: Quickshell.env("NOCTALIA_CACHE_DIR") - || (Quickshell.env("XDG_CACHE_HOME") || Quickshell.env( - "HOME") + "/.cache") + "/" + shellName + "/" + property string configDir: Quickshell.env("NOCTALIA_CONFIG_DIR") || (Quickshell.env("XDG_CONFIG_HOME") + || Quickshell.env( + "HOME") + "/.config") + "/" + shellName + "/" + property string cacheDir: Quickshell.env("NOCTALIA_CACHE_DIR") || (Quickshell.env("XDG_CACHE_HOME") || Quickshell.env( + "HOME") + "/.cache") + "/" + shellName + "/" + property string settingsFile: Quickshell.env("NOCTALIA_SETTINGS_FILE") || (configDir + "settings.json") property string colorsFile: Quickshell.env("NOCTALIA_COLORS_FILE") || (configDir + "colors.json") - property var data: settingAdapter property string defaultWallpaper: Qt.resolvedUrl("../Assets/Tests/wallpaper.png") + property string defaultAvatar: Quickshell.env("HOME") + "/.face" + + // Used to access via Settings.data.xxx.yyy + property var data: adapter // Needed to only have one NPanel loaded at a time. // property var openPanel: null Item { Component.onCompleted: { + // ensure settings dir exists Quickshell.execDetached(["mkdir", "-p", configDir]) Quickshell.execDetached(["mkdir", "-p", cacheDir]) @@ -36,8 +43,6 @@ Singleton { // Qt.callLater(function () { // WallpaperManager.setCurrentWallpaper(settings.currentWallpaper, true); // }) - id: settingFileView - path: settingsFile watchChanges: true onFileChanged: reload() @@ -53,7 +58,7 @@ Singleton { } JsonAdapter { - id: settingAdapter + id: adapter // bar property JsonObject bar @@ -70,7 +75,7 @@ Singleton { property JsonObject general general: JsonObject { - property string avatarImage: Quickshell.env("HOME") + "/.face" + property string avatarImage: defaultAvatar property bool dimDesktop: true property bool showScreenCorners: false property bool showDock: false @@ -80,7 +85,7 @@ Singleton { property JsonObject location location: JsonObject { - property bool name: true + property string name: "Dinslaken" property bool useFahrenheit: false property bool reverseDayMonth: false property bool use12HourClock: false From 644f647653a4090c502f07b8bfe64fc7f49abdb7 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 17:57:09 -0400 Subject: [PATCH 135/394] Location service with caching --- Services/Location.qml | 101 ++++++++++++++++++++++++------------------ shell.qml | 5 +++ 2 files changed, 63 insertions(+), 43 deletions(-) diff --git a/Services/Location.qml b/Services/Location.qml index 95336ab..fc79a67 100644 --- a/Services/Location.qml +++ b/Services/Location.qml @@ -9,13 +9,8 @@ Singleton { id: root property string locationFile: Quickshell.env("NOCTALIA_WEATHER_FILE") || (Settings.cacheDir + "location.json") - - // Used to access via Location.data.xxx.yyy - property var data: adapter - - function quickstart() { - console.log(locationFile) - } + property int weatherUpdateFrequency: 30 * 60 * 1000 // 30 minutes expressed in milliseconds + property var data: adapter // Used to access via Location.data.xxx.yyy FileView { path: locationFile @@ -25,44 +20,70 @@ Singleton { Component.onCompleted: function () { reload() } - onLoaded: function () {} + onLoaded: function () { + updateWeather() + } onLoadFailed: function (error) { - if (error.toString().includes("No such file") || error === 2) + if (error.toString().includes("No such file") || error === 2) { // File doesn't exist, create it with default values writeAdapter() + } } JsonAdapter { id: adapter - // main - property JsonObject main + property string latitude: "" + property string longitude: "" + property string weatherLastFetch: "" + property var weather: null + } + } - main: JsonObject { - property string latitude: "" - property string longitude: "" - property int weatherLastFetched: 0 - } - // weather - property JsonObject weather - - weather: JsonObject { - } + Timer { + id: updateTimer + interval: 60 * 1000 + repeat: true + onTriggered: { + updateWeather(); } } // -------------------------------- - function getWeather() { - if (data.main.latitude === "" || data.main.longitude === "") { - geocodeLocation(Settings.data.location.name, function (lat, lon) { - console.log(Settings.data.location.name + ": " + lat + " / " + lon); - }) + function init() { + // does nothing but ensure the singleton is created + } + + + // -------------------------------- + function updateWeather() { + var now = Date.now() + if ((data.weatherLastFetch === "") || (now >= data.weatherLastFetch + weatherUpdateFrequency)) { + getFreshWeather() } } // -------------------------------- - function geocodeLocation(locationName, callback, errorCallback) { + function getFreshWeather() { + if (data.latitude === "" || data.longitude === "") { + console.log("Geocoding location") + _geocodeLocation(Settings.data.location.name, function (lat, lon) { + console.log("Geocoded " + Settings.data.location.name + " to : " + lat + " / " + lon) + + // Save GPS coordinates + data.latitude = lat + data.longitude = lon + + _fetchWeather(data.latitude, data.longitude, errorCallback) + }, errorCallback) + } else { + _fetchWeather(data.latitude, data.longitude, errorCallback) + } + } + + // -------------------------------- + function _geocodeLocation(locationName, callback, errorCallback) { var geoUrl = "https://geocoding-api.open-meteo.com/v1/search?name=" + encodeURIComponent( locationName) + "&language=en&format=json" var xhr = new XMLHttpRequest() @@ -89,7 +110,7 @@ Singleton { } // -------------------------------- - function fetchWeather(latitude, longitude, callback, errorCallback) { + function _fetchWeather(latitude, longitude, errorCallback) { var url = "https://api.open-meteo.com/v1/forecast?latitude=" + latitude + "&longitude=" + longitude + "¤t_weather=true¤t=relativehumidity_2m,surface_pressure&daily=temperature_2m_max,temperature_2m_min,weathercode&timezone=auto" var xhr = new XMLHttpRequest() @@ -98,7 +119,11 @@ Singleton { if (xhr.status === 200) { try { var weatherData = JSON.parse(xhr.responseText) - callback(weatherData) + + // Save to json + data.weather = weatherData + data.weatherLastFetch = Date.now() + console.log("Cached weather to disk") } catch (e) { errorCallback("Failed to parse weather data.") } @@ -111,18 +136,8 @@ Singleton { xhr.send() } - - - // function fetchCityWeather(city, callback, errorCallback) { - // fetchCoordinates(city, function (lat, lon) { - // fetchWeather(lat, lon, function (weatherData) { - // callback({ - // "city": city, - // "latitude": lat, - // "longitude": lon, - // "weather": weatherData - // }) - // }, errorCallback) - // }, errorCallback) - // } + // -------------------------------- + function errorCallback(message) { + console.error(message) + } } diff --git a/shell.qml b/shell.qml index 9c1ba50..2964a6e 100644 --- a/shell.qml +++ b/shell.qml @@ -38,4 +38,9 @@ ShellRoot { Calendar { id: calendar } + + Component.onCompleted: { + // On startup, check if we need to get fresh weather data + Location.init() + } } From 14f4beff032ba1f022bd8a4e0ce9f59f8584783e Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Tue, 12 Aug 2025 00:03:21 +0200 Subject: [PATCH 136/394] Make tabs look good in SettingsWindow --- Modules/Settings/.gitkeep | 0 Modules/Settings/SettingsWindow.qml | 50 ++++++++++++++--------------- shell.qml | 3 +- 3 files changed, 26 insertions(+), 27 deletions(-) delete mode 100644 Modules/Settings/.gitkeep diff --git a/Modules/Settings/.gitkeep b/Modules/Settings/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/Modules/Settings/SettingsWindow.qml b/Modules/Settings/SettingsWindow.qml index cdcab4a..b9f7caa 100644 --- a/Modules/Settings/SettingsWindow.qml +++ b/Modules/Settings/SettingsWindow.qml @@ -17,9 +17,7 @@ NLoader { WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand readonly property real scaling: Scaling.scale(screen) - // Active tab index unified for sidebar, header, and content stack property int currentTabIndex: 0 - // Single source of truth for tabs (explicit icon/label here) property var tabsModel: [{ "icon": "tune", "label": "General", @@ -58,13 +56,11 @@ NLoader { "source": "Tabs/About.qml" }] - // Always default to the first tab (General) when the panel becomes visible onVisibleChanged: function () { if (visible) currentTabIndex = 0 } - // Ensure panel shows itself once created Component.onCompleted: show() Rectangle { @@ -78,18 +74,16 @@ NLoader { height: 640 * scaling anchors.centerIn: parent - // Prevent closing when clicking in the panel bg MouseArea { anchors.fill: parent } - // Main two-pane layout RowLayout { anchors.fill: parent anchors.margins: Style.marginLarge * scaling spacing: Style.marginLarge * scaling - // Sidebar + // Sidebar with tighter spacing Rectangle { id: sidebar Layout.preferredWidth: 260 * scaling @@ -99,45 +93,55 @@ NLoader { border.color: Colors.outline border.width: Math.max(1, Style.borderThin * scaling) - ColumnLayout { + Column { anchors.fill: parent anchors.margins: Style.marginSmall * scaling - spacing: Style.marginSmall * scaling + spacing: 2 * scaling // Minimal spacing between tabs Repeater { id: sections model: settingsPanel.tabsModel delegate: Rectangle { + id: tabItem readonly property bool selected: index === settingsPanel.currentTabIndex - Layout.fillWidth: true - height: 44 * scaling + width: parent.width + height: 32 * scaling // Back to original height radius: Style.radiusSmall * scaling - color: selected ? Colors.highlight : "transparent" - border.color: Colors.outline - border.width: Math.max(1, Style.borderThin * scaling) + color: selected ? Colors.accentPrimary : (tabItem.hovering ? Colors.highlight : "transparent") + border.color: "transparent" + border.width: 0 + + // Subtle hover effect: only icon/text color tint on hover + property bool hovering: false RowLayout { anchors.fill: parent - anchors.leftMargin: Style.marginMedium * scaling - anchors.rightMargin: Style.marginMedium * scaling - spacing: Style.marginSmall * scaling + anchors.leftMargin: Style.marginSmall * scaling + anchors.rightMargin: Style.marginSmall * scaling + spacing: Style.marginTiny * scaling NText { text: modelData.icon font.family: "Material Symbols Outlined" font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 } - color: selected ? Colors.onAccent : Colors.textSecondary + color: selected ? Colors.onAccent : (tabItem.hovering ? Colors.onAccent : Colors.textSecondary) } NText { text: modelData.label - color: selected ? Colors.onAccent : Colors.textPrimary + color: selected ? Colors.onAccent : (tabItem.hovering ? Colors.onAccent : Colors.textPrimary) + font.weight: Style.fontWeightBold Layout.fillWidth: true } } MouseArea { anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton + onEntered: tabItem.hovering = true + onExited: tabItem.hovering = false + onCanceled: tabItem.hovering = false onClicked: settingsPanel.currentTabIndex = index } } @@ -145,7 +149,7 @@ NLoader { } } - // Content + // Content (unchanged) Rectangle { id: contentPane Layout.fillWidth: true @@ -156,14 +160,12 @@ NLoader { border.width: Math.max(1, Style.borderThin * scaling) clip: true - // Content layout: header + divider + pages ColumnLayout { id: contentLayout anchors.fill: parent anchors.margins: Style.marginLarge * scaling spacing: Style.marginSmall * scaling - // Header row RowLayout { id: headerRow Layout.fillWidth: true @@ -189,14 +191,12 @@ NLoader { Layout.fillWidth: true } - // Stacked pages StackLayout { id: stack Layout.fillWidth: true Layout.fillHeight: true currentIndex: settingsPanel.currentTabIndex - // Pages generated from tabsModel Repeater { model: settingsPanel.tabsModel delegate: Loader { @@ -212,4 +212,4 @@ NLoader { } } } -} +} \ No newline at end of file diff --git a/shell.qml b/shell.qml index 2964a6e..1f7e057 100644 --- a/shell.qml +++ b/shell.qml @@ -1,5 +1,4 @@ -// Disable reload popup -//@ pragma Env QS_NO_RELOAD_POPUP=1 +// Disable reload popup add this as a new row: //pragma Env QS_NO_RELOAD_POPUP=1 import QtQuick import Quickshell import Quickshell.Io From e56a89e3edd3eb125c015863e8ac9bb413086f1a Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 18:05:39 -0400 Subject: [PATCH 137/394] Timestamp in seconds (Epoch style) --- Services/Location.qml | 7 +++---- Services/Time.qml | 7 ++++++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Services/Location.qml b/Services/Location.qml index fc79a67..3e97c7b 100644 --- a/Services/Location.qml +++ b/Services/Location.qml @@ -9,7 +9,7 @@ Singleton { id: root property string locationFile: Quickshell.env("NOCTALIA_WEATHER_FILE") || (Settings.cacheDir + "location.json") - property int weatherUpdateFrequency: 30 * 60 * 1000 // 30 minutes expressed in milliseconds + property int weatherUpdateFrequency: 30 * 60 // 30 minutes expressed in seconds property var data: adapter // Used to access via Location.data.xxx.yyy FileView { @@ -58,8 +58,7 @@ Singleton { // -------------------------------- function updateWeather() { - var now = Date.now() - if ((data.weatherLastFetch === "") || (now >= data.weatherLastFetch + weatherUpdateFrequency)) { + if ((data.weatherLastFetch === "") || (Time.timestamp >= data.weatherLastFetch + weatherUpdateFrequency)) { getFreshWeather() } } @@ -122,7 +121,7 @@ Singleton { // Save to json data.weather = weatherData - data.weatherLastFetch = Date.now() + data.weatherLastFetch = Time.timestamp console.log("Cached weather to disk") } catch (e) { errorCallback("Failed to parse weather data.") diff --git a/Services/Time.qml b/Services/Time.qml index 702e1b9..f277a5d 100644 --- a/Services/Time.qml +++ b/Services/Time.qml @@ -10,7 +10,7 @@ Singleton { property var date: new Date() property string time: Settings.data.location.use12HourClock ? Qt.formatDateTime(date, "h:mm AP") : Qt.formatDateTime( date, "HH:mm") - property string dateString: { + readonly property string dateString: { let now = date let dayName = now.toLocaleDateString(Qt.locale(), "ddd") dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1) @@ -38,6 +38,11 @@ Singleton { + (Settings.data.location.reverseDayMonth ? `${month} ${day}${suffix} ${year}` : `${day}${suffix} ${month} ${year}`) } + // Returns a Unix Timestamp (in seconds) + readonly property string timestamp: { + return Math.floor(Date.now() / 1000); + } + // Format an easy to read approximate duration ex: 4h32m // Used to display the time remaining on the Battery widget function formatVagueHumanReadableDuration(totalSeconds) { From 916d86cd97382a3cdcb1f45171f5d425d3abccf0 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 18:06:32 -0400 Subject: [PATCH 138/394] Formatting --- Modules/Settings/SettingsWindow.qml | 6 +++--- Services/Location.qml | 9 +++------ Services/Time.qml | 4 ++-- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/Modules/Settings/SettingsWindow.qml b/Modules/Settings/SettingsWindow.qml index b9f7caa..9227cb3 100644 --- a/Modules/Settings/SettingsWindow.qml +++ b/Modules/Settings/SettingsWindow.qml @@ -96,7 +96,7 @@ NLoader { Column { anchors.fill: parent anchors.margins: Style.marginSmall * scaling - spacing: 2 * scaling // Minimal spacing between tabs + spacing: 2 * scaling // Minimal spacing between tabs Repeater { id: sections @@ -106,7 +106,7 @@ NLoader { id: tabItem readonly property bool selected: index === settingsPanel.currentTabIndex width: parent.width - height: 32 * scaling // Back to original height + height: 32 * scaling // Back to original height radius: Style.radiusSmall * scaling color: selected ? Colors.accentPrimary : (tabItem.hovering ? Colors.highlight : "transparent") border.color: "transparent" @@ -212,4 +212,4 @@ NLoader { } } } -} \ No newline at end of file +} diff --git a/Services/Location.qml b/Services/Location.qml index 3e97c7b..341aefd 100644 --- a/Services/Location.qml +++ b/Services/Location.qml @@ -40,22 +40,19 @@ Singleton { } } - Timer { id: updateTimer interval: 60 * 1000 repeat: true onTriggered: { - updateWeather(); + updateWeather() } } // -------------------------------- - function init() { - // does nothing but ensure the singleton is created + function init() {// does nothing but ensure the singleton is created } - // -------------------------------- function updateWeather() { if ((data.weatherLastFetch === "") || (Time.timestamp >= data.weatherLastFetch + weatherUpdateFrequency)) { @@ -135,7 +132,7 @@ Singleton { xhr.send() } - // -------------------------------- + // -------------------------------- function errorCallback(message) { console.error(message) } diff --git a/Services/Time.qml b/Services/Time.qml index f277a5d..3d827ad 100644 --- a/Services/Time.qml +++ b/Services/Time.qml @@ -39,8 +39,8 @@ Singleton { } // Returns a Unix Timestamp (in seconds) - readonly property string timestamp: { - return Math.floor(Date.now() / 1000); + readonly property string timestamp: { + return Math.floor(Date.now() / 1000) } // Format an easy to read approximate duration ex: 4h32m From 18b08bf64704f4bf28403eaccb38705d457a4cba Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 18:20:12 -0400 Subject: [PATCH 139/394] Default Settings and colors - Replaced Dinslaken by Tokyo - Renamed Colors.highlight to Colors.hover --- Assets/Colors/default.json | 2 +- Modules/Bar/TrayMenu.qml | 4 ++-- Modules/Bar/WiFiMenu.qml | 2 +- Modules/Settings/SettingsWindow.qml | 2 +- Services/Colors.qml | 8 ++++---- Services/Settings.qml | 2 +- Widgets/NComboBox.qml | 4 ++-- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Assets/Colors/default.json b/Assets/Colors/default.json index 74a6369..ddd0a7a 100644 --- a/Assets/Colors/default.json +++ b/Assets/Colors/default.json @@ -17,7 +17,7 @@ "error": "#eb6f92", "warning": "#f6c177", - "highlight": "#c4a7e7", + "hover": "#c4a7e7", "onAccent": "#191724", "outline": "#44415a", diff --git a/Modules/Bar/TrayMenu.qml b/Modules/Bar/TrayMenu.qml index 953f075..2d04dcb 100644 --- a/Modules/Bar/TrayMenu.qml +++ b/Modules/Bar/TrayMenu.qml @@ -112,7 +112,7 @@ PopupWindow { Rectangle { id: bg anchors.fill: parent - color: mouseArea.containsMouse ? Colors.highlight : "transparent" + color: mouseArea.containsMouse ? Colors.hover : "transparent" radius: Style.radiusSmall * scaling visible: !(modelData?.isSeparator ?? false) @@ -347,7 +347,7 @@ PopupWindow { Rectangle { id: bg anchors.fill: parent - color: mouseArea.containsMouse ? Colors.highlight : "transparent" + color: mouseArea.containsMouse ? Colors.hover : "transparent" radius: Style.radiusSmall * scaling visible: !(modelData?.isSeparator ?? false) property color hoverTextColor: mouseArea.containsMouse ? Colors.onAccent : Colors.textPrimary diff --git a/Modules/Bar/WiFiMenu.qml b/Modules/Bar/WiFiMenu.qml index ed5dca6..6775d18 100644 --- a/Modules/Bar/WiFiMenu.qml +++ b/Modules/Bar/WiFiMenu.qml @@ -114,7 +114,7 @@ NLoader { Layout.fillWidth: true Layout.preferredHeight: Style.baseWidgetSize * 1.5 * scaling radius: Style.radiusMedium * scaling - color: modelData.connected ? Colors.accentPrimary : (networkMouseArea.containsMouse ? Colors.highlight : "transparent") + color: modelData.connected ? Colors.accentPrimary : (networkMouseArea.containsMouse ? Colors.hover : "transparent") RowLayout { anchors.fill: parent diff --git a/Modules/Settings/SettingsWindow.qml b/Modules/Settings/SettingsWindow.qml index 9227cb3..405f7b5 100644 --- a/Modules/Settings/SettingsWindow.qml +++ b/Modules/Settings/SettingsWindow.qml @@ -108,7 +108,7 @@ NLoader { width: parent.width height: 32 * scaling // Back to original height radius: Style.radiusSmall * scaling - color: selected ? Colors.accentPrimary : (tabItem.hovering ? Colors.highlight : "transparent") + color: selected ? Colors.accentPrimary : (tabItem.hovering ? Colors.hover : "transparent") border.color: "transparent" border.width: 0 diff --git a/Services/Colors.qml b/Services/Colors.qml index 25e6b74..0bd125a 100644 --- a/Services/Colors.qml +++ b/Services/Colors.qml @@ -31,8 +31,8 @@ Singleton { property color error: themeData.error property color warning: themeData.warning - // Highlights - property color highlight: themeData.highlight + // Hover + property color hover: themeData.hover // Additional Theme Properties property color onAccent: themeData.onAccent @@ -85,8 +85,8 @@ Singleton { property string error: "#eb6f92" property string warning: "#f6c177" - // Highlights - property string highlight: "#c4a7e7" + // Hover + property string hover: "#c4a7e7" // Additional Theme Properties property string onAccent: "#191724" diff --git a/Services/Settings.qml b/Services/Settings.qml index 647bdec..f13de43 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -85,7 +85,7 @@ Singleton { property JsonObject location location: JsonObject { - property string name: "Dinslaken" + property string name: "Tokyo" property bool useFahrenheit: false property bool reverseDayMonth: false property bool use12HourClock: false diff --git a/Widgets/NComboBox.qml b/Widgets/NComboBox.qml index 459117a..bf971c0 100644 --- a/Widgets/NComboBox.qml +++ b/Widgets/NComboBox.qml @@ -28,7 +28,7 @@ ComboBox { implicitWidth: 120 * scaling implicitHeight: Style.baseWidgetSize * scaling color: Colors.surfaceVariant - border.color: root.activeFocus ? Colors.highlight : Colors.outline + border.color: root.activeFocus ? Colors.hover : Colors.outline border.width: Math.max(1, Style.borderThin * scaling) radius: Style.radiusMedium * scaling } @@ -93,7 +93,7 @@ ComboBox { background: Rectangle { width: root.width - Style.spacingMedium * scaling * 3 - color: highlighted ? Colors.highlight : "transparent" + color: highlighted ? Colors.hover : "transparent" radius: Style.radiusSmall * scaling } } From 152272c51a9653743f31a8a81b01d256fe6037c5 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 18:33:13 -0400 Subject: [PATCH 140/394] Location fix timestamp and logic --- Services/Location.qml | 11 ++++++++++- Services/Time.qml | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Services/Location.qml b/Services/Location.qml index 341aefd..94d9e57 100644 --- a/Services/Location.qml +++ b/Services/Location.qml @@ -35,7 +35,7 @@ Singleton { property string latitude: "" property string longitude: "" - property string weatherLastFetch: "" + property int weatherLastFetch: 0 property var weather: null } } @@ -53,6 +53,14 @@ Singleton { function init() {// does nothing but ensure the singleton is created } + // -------------------------------- + function resetWeather() { + data.latitude = "" + data.longitude = "" + data.weatherLastFetch = 0 + data.weather = null + } + // -------------------------------- function updateWeather() { if ((data.weatherLastFetch === "") || (Time.timestamp >= data.weatherLastFetch + weatherUpdateFrequency)) { @@ -107,6 +115,7 @@ Singleton { // -------------------------------- function _fetchWeather(latitude, longitude, errorCallback) { + console.log("Getting weather") var url = "https://api.open-meteo.com/v1/forecast?latitude=" + latitude + "&longitude=" + longitude + "¤t_weather=true¤t=relativehumidity_2m,surface_pressure&daily=temperature_2m_max,temperature_2m_min,weathercode&timezone=auto" var xhr = new XMLHttpRequest() diff --git a/Services/Time.qml b/Services/Time.qml index 3d827ad..0ce3593 100644 --- a/Services/Time.qml +++ b/Services/Time.qml @@ -39,7 +39,7 @@ Singleton { } // Returns a Unix Timestamp (in seconds) - readonly property string timestamp: { + readonly property int timestamp: { return Math.floor(Date.now() / 1000) } From 8cb519e5f4a8d22235699ede5c9d2706ce8f0437 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Tue, 12 Aug 2025 00:45:53 +0200 Subject: [PATCH 141/394] Add basic settings, gotta fix layout --- Modules/Settings/Tabs/About.qml | 157 +++++++++++++++++++++-- Modules/Settings/Tabs/Bar.qml | 43 ++++++- Modules/Settings/Tabs/Display.qml | 89 +++++++++++-- Modules/Settings/Tabs/Misc.qml | 21 ++- Modules/Settings/Tabs/Network.qml | 40 ++++-- Modules/Settings/Tabs/ScreenRecorder.qml | 92 +++++++++++-- Modules/Settings/Tabs/TimeWeather.qml | 51 ++++++-- Modules/Settings/Tabs/Wallpaper.qml | 107 +++++++++++++-- Services/Settings.qml | 8 +- 9 files changed, 537 insertions(+), 71 deletions(-) diff --git a/Modules/Settings/Tabs/About.qml b/Modules/Settings/Tabs/About.qml index 4824386..5eb8af1 100644 --- a/Modules/Settings/Tabs/About.qml +++ b/Modules/Settings/Tabs/About.qml @@ -1,29 +1,170 @@ import QtQuick +import QtQuick.Controls import QtQuick.Layouts +import QtQuick.Effects +import Quickshell +import Quickshell.Io import qs.Services import qs.Widgets Item { + id: root property real scaling: 1 readonly property string tabIcon: "info" readonly property string tabLabel: "About" readonly property int tabIndex: 8 anchors.fill: parent + property string latestVersion: "Unknown" + property string currentVersion: "Unknown" + property var contributors: [] + property string githubDataPath: Settings.configDir + "github_data.json" + + function loadFromFile() { + const now = Date.now() + const data = githubData + if (!data.timestamp || (now - data.timestamp > 3600 * 1000)) { // 1h cache + fetchFromGitHub() + return + } + if (data.version) root.latestVersion = data.version + if (data.contributors) root.contributors = data.contributors + } + + function fetchFromGitHub() { + versionProcess.running = true + contributorsProcess.running = true + } + + function saveData() { + githubData.timestamp = Date.now() + Qt.callLater(function () { githubDataFile.writeAdapter() }) + } + ColumnLayout { anchors.fill: parent + anchors.margins: Style.marginLarge * scaling spacing: Style.marginMedium * scaling - NText { - text: "About" - font.weight: Style.fontWeightBold - color: Colors.accentSecondary + + // Header + NText { text: "Noctalia: quiet by design"; font.weight: Style.fontWeightBold; color: Colors.textPrimary } + NText { text: "It may just be another quickshell setup but it won't get in your way."; color: Colors.textSecondary } + + // Versions grid + RowLayout { + spacing: Style.marginLarge * scaling + ColumnLayout { NText { text: "Latest Version:"; color: Colors.textSecondary }; NText { text: root.latestVersion; font.weight: Style.fontWeightBold; color: Colors.textPrimary } } + ColumnLayout { NText { text: "Installed Version:"; color: Colors.textSecondary }; NText { text: root.currentVersion; font.weight: Style.fontWeightBold; color: Colors.textPrimary } } + Item { Layout.fillWidth: true } + NIconButton { + icon: "system_update" + tooltipText: "Open latest release" + onClicked: Quickshell.execDetached(["xdg-open", "https://github.com/Ly-sec/Noctalia/releases/latest"]) } } - NText { - text: "Coming soon" - color: Colors.textSecondary + + NDivider { Layout.fillWidth: true } + + // Contributors + RowLayout { spacing: Style.marginSmall * scaling + NText { text: "Contributors"; font.weight: Style.fontWeightBold; color: Colors.textPrimary } + NText { text: "(" + root.contributors.length + ")"; color: Colors.textSecondary } } - Item { + + GridView { + id: contributorsGrid + Layout.fillWidth: true Layout.fillHeight: true + cellWidth: 200 * scaling + cellHeight: 100 * scaling + model: root.contributors + delegate: Rectangle { + width: contributorsGrid.cellWidth - 8 * scaling + height: contributorsGrid.cellHeight - 4 * scaling + radius: Style.radiusLarge * scaling + color: contributorArea.containsMouse ? Colors.highlight : "transparent" + RowLayout { + anchors.fill: parent + anchors.margins: Style.marginSmall * scaling + spacing: Style.marginSmall * scaling + Item { + Layout.preferredWidth: 40 * scaling + Layout.preferredHeight: 40 * scaling + Image { id: avatarImage; anchors.fill: parent; source: modelData.avatar_url || ""; asynchronous: true; visible: false; fillMode: Image.PreserveAspectCrop } + MultiEffect { anchors.fill: parent; source: avatarImage; maskEnabled: true; maskSource: mask } + Item { id: mask; anchors.fill: parent; visible: false; Rectangle { anchors.fill: parent; radius: width / 2 } } + NText { anchors.centerIn: parent; text: "person"; font.family: "Material Symbols Outlined"; color: contributorArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary; visible: !avatarImage.source || avatarImage.status !== Image.Ready } + } + ColumnLayout { Layout.fillWidth: true; spacing: 2 * scaling + NText { text: modelData.login || "Unknown"; color: contributorArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary } + NText { text: (modelData.contributions || 0) + " commits"; color: contributorArea.containsMouse ? Colors.backgroundPrimary : Colors.textSecondary } + } + } + MouseArea { id: contributorArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onClicked: if (modelData.html_url) Quickshell.execDetached(["xdg-open", modelData.html_url]) } + } + } + + Item { Layout.fillHeight: true } + } + + // Processes and persistence + Process { + id: currentVersionProcess + command: ["sh", "-c", "cd " + Quickshell.shellDir + " && git describe --tags --abbrev=0 2>/dev/null || echo 'Unknown'"] + Component.onCompleted: running = true + stdout: StdioCollector { + onStreamFinished: { + const version = text.trim() + if (version && version !== "Unknown") { + root.currentVersion = version + } else { + currentVersionProcess.command = ["sh", "-c", "cd " + Quickshell.shellDir + " && cat package.json 2>/dev/null | grep '\"version\"' | cut -d'\"' -f4 || echo 'Unknown'"] + currentVersionProcess.running = true + } + } + } + } + + FileView { + id: githubDataFile + path: root.githubDataPath + blockLoading: true + printErrors: true + watchChanges: true + onFileChanged: githubDataFile.reload() + onLoaded: loadFromFile() + onLoadFailed: { + githubData.version = "Unknown"; githubData.contributors = []; githubData.timestamp = 0; githubDataFile.writeAdapter(); fetchFromGitHub() + } + Component.onCompleted: { if (path) reload() } + JsonAdapter { id: githubData; property string version: "Unknown"; property var contributors: []; property double timestamp: 0 } + } + + Process { + id: versionProcess + command: ["curl", "-s", "https://api.github.com/repos/Ly-sec/Noctalia/releases/latest"] + stdout: StdioCollector { + onStreamFinished: { + try { + const data = JSON.parse(text) + if (data.tag_name) { const version = data.tag_name; githubData.version = version; root.latestVersion = version } + saveData() + } catch (e) { console.error("Failed to parse version:", e) } + } + } + } + + Process { + id: contributorsProcess + command: ["curl", "-s", "https://api.github.com/repos/Ly-sec/Noctalia/contributors?per_page=100"] + stdout: StdioCollector { + onStreamFinished: { + try { + const data = JSON.parse(text) + githubData.contributors = data || [] + root.contributors = githubData.contributors + saveData() + } catch (e) { console.error("Failed to parse contributors:", e); root.contributors = [] } + } } } } diff --git a/Modules/Settings/Tabs/Bar.qml b/Modules/Settings/Tabs/Bar.qml index 2079a2e..3154cda 100644 --- a/Modules/Settings/Tabs/Bar.qml +++ b/Modules/Settings/Tabs/Bar.qml @@ -15,17 +15,48 @@ Item { ColumnLayout { anchors.fill: parent spacing: Style.marginMedium * scaling + NText { - text: "Bar" + text: "Elements" font.weight: Style.fontWeightBold color: Colors.accentSecondary } - NText { - text: "Coming soon" - color: Colors.textSecondary + + NToggle { + label: "Show Active Window" + description: "Display the title of the currently focused window below the bar" + value: Settings.data.bar.showActiveWindow + onToggled: function (newValue) { Settings.data.bar.showActiveWindow = newValue } } - Item { - Layout.fillHeight: true + + NToggle { + label: "Show Active Window Icon" + description: "Display the icon of the currently focused window" + value: Settings.data.bar.showActiveWindowIcon + onToggled: function (newValue) { Settings.data.bar.showActiveWindowIcon = newValue } } + + NToggle { + label: "Show System Info" + description: "Display system information (CPU, RAM, Temperature)" + value: Settings.data.bar.showSystemInfo + onToggled: function (newValue) { Settings.data.bar.showSystemInfo = newValue } + } + + NToggle { + label: "Show Taskbar" + description: "Display a taskbar showing currently open windows" + value: Settings.data.bar.showTaskbar + onToggled: function (newValue) { Settings.data.bar.showTaskbar = newValue } + } + + NToggle { + label: "Show Media" + description: "Display media controls and information" + value: Settings.data.bar.showMedia + onToggled: function (newValue) { Settings.data.bar.showMedia = newValue } + } + + Item { Layout.fillHeight: true } } } diff --git a/Modules/Settings/Tabs/Display.qml b/Modules/Settings/Tabs/Display.qml index 3916c9e..83a52ff 100644 --- a/Modules/Settings/Tabs/Display.qml +++ b/Modules/Settings/Tabs/Display.qml @@ -1,5 +1,6 @@ import QtQuick import QtQuick.Layouts +import Quickshell import qs.Services import qs.Widgets @@ -10,20 +11,86 @@ Item { readonly property int tabIndex: 5 anchors.fill: parent + // Helper functions to update arrays immutably + function addMonitor(list, name) { + const arr = (list || []).slice(); if (!arr.includes(name)) arr.push(name); return arr + } + function removeMonitor(list, name) { + return (list || []).filter(function (n) { return n !== name }) + } + ColumnLayout { anchors.fill: parent spacing: Style.marginMedium * scaling - NText { - text: "Display" - font.weight: Style.fontWeightBold - color: Colors.accentSecondary - } - NText { - text: "Coming soon" - color: Colors.textSecondary - } - Item { - Layout.fillHeight: true + + NText { text: "Per‑monitor configuration"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + + Repeater { + model: Quickshell.screens || [] + delegate: Rectangle { + Layout.fillWidth: true + radius: Style.radiusMedium * scaling + color: Colors.surface + border.color: Colors.outline + border.width: Math.max(1, Style.borderThin * scaling) + implicitHeight: contentCol.implicitHeight + Style.marginLarge * scaling + + ColumnLayout { + id: contentCol + anchors.fill: parent + anchors.margins: Style.marginMedium * scaling + spacing: Style.marginSmall * scaling + + NText { text: (modelData.name || "Unknown"); font.weight: Style.fontWeightBold; color: Colors.accentPrimary } + + RowLayout { + spacing: Style.marginMedium * scaling + NText { text: `Resolution: ${modelData.width}x${modelData.height}`; color: Colors.textSecondary } + NText { text: `Position: (${modelData.x}, ${modelData.y})`; color: Colors.textSecondary } + } + + NToggle { + label: "Bar" + description: "Display the top bar on this monitor" + value: (Settings.data.bar.monitors || []).indexOf(modelData.name) !== -1 + onToggled: function (newValue) { + if (newValue) { + Settings.data.bar.monitors = addMonitor(Settings.data.bar.monitors, modelData.name) + } else { + Settings.data.bar.monitors = removeMonitor(Settings.data.bar.monitors, modelData.name) + } + } + } + + NToggle { + label: "Dock" + description: "Display the dock on this monitor" + value: (Settings.data.dock.monitors || []).indexOf(modelData.name) !== -1 + onToggled: function (newValue) { + if (newValue) { + Settings.data.dock.monitors = addMonitor(Settings.data.dock.monitors, modelData.name) + } else { + Settings.data.dock.monitors = removeMonitor(Settings.data.dock.monitors, modelData.name) + } + } + } + + NToggle { + label: "Notifications" + description: "Display notifications on this monitor" + value: (Settings.data.notifications.monitors || []).indexOf(modelData.name) !== -1 + onToggled: function (newValue) { + if (newValue) { + Settings.data.notifications.monitors = addMonitor(Settings.data.notifications.monitors, modelData.name) + } else { + Settings.data.notifications.monitors = removeMonitor(Settings.data.notifications.monitors, modelData.name) + } + } + } + } + } } + + Item { Layout.fillHeight: true } } } diff --git a/Modules/Settings/Tabs/Misc.qml b/Modules/Settings/Tabs/Misc.qml index e52057b..976331f 100644 --- a/Modules/Settings/Tabs/Misc.qml +++ b/Modules/Settings/Tabs/Misc.qml @@ -13,17 +13,24 @@ Item { ColumnLayout { anchors.fill: parent spacing: Style.marginMedium * scaling + NText { - text: "Misc" + text: "Media" font.weight: Style.fontWeightBold color: Colors.accentSecondary } - NText { - text: "Coming soon" - color: Colors.textSecondary - } - Item { - Layout.fillHeight: true + + NText { text: "Visualizer Type"; color: Colors.textPrimary; font.weight: Style.fontWeightBold } + NText { text: "Choose the style of the audio visualizer"; color: Colors.textSecondary } + + NComboBox { + id: visualizerTypeComboBox + optionsKeys: ["radial", "fire", "diamond"] + optionsLabels: ["Radial", "Fire", "Diamond"] + currentKey: Settings.data.audioVisualizer.type + onSelected: function (key) { Settings.data.audioVisualizer.type = key } } + + Item { Layout.fillHeight: true } } } diff --git a/Modules/Settings/Tabs/Network.qml b/Modules/Settings/Tabs/Network.qml index 2a15699..8e922a7 100644 --- a/Modules/Settings/Tabs/Network.qml +++ b/Modules/Settings/Tabs/Network.qml @@ -1,5 +1,7 @@ import QtQuick import QtQuick.Layouts +import Quickshell +import Quickshell.Bluetooth import qs.Services import qs.Widgets @@ -13,17 +15,35 @@ Item { ColumnLayout { anchors.fill: parent spacing: Style.marginMedium * scaling - NText { - text: "Network" - font.weight: Style.fontWeightBold - color: Colors.accentSecondary + + NText { text: "Wi‑Fi"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + + NToggle { + label: "Enable Wi‑Fi" + description: "Turn Wi‑Fi radio on or off" + value: Settings.data.network.wifiEnabled + onToggled: function (newValue) { + Settings.data.network.wifiEnabled = newValue + Quickshell.execDetached(["nmcli", "radio", "wifi", newValue ? "on" : "off"]) } } - NText { - text: "Coming soon" - color: Colors.textSecondary - } - Item { - Layout.fillHeight: true + + NDivider { Layout.fillWidth: true } + + NText { text: "Bluetooth"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + + NToggle { + label: "Enable Bluetooth" + description: "Turn Bluetooth radio on or off" + value: Settings.data.network.bluetoothEnabled + onToggled: function (newValue) { + Settings.data.network.bluetoothEnabled = newValue + if (Bluetooth.defaultAdapter) { + Bluetooth.defaultAdapter.enabled = newValue + if (Bluetooth.defaultAdapter.enabled) Bluetooth.defaultAdapter.discovering = true + } + } } + + Item { Layout.fillHeight: true } } } diff --git a/Modules/Settings/Tabs/ScreenRecorder.qml b/Modules/Settings/Tabs/ScreenRecorder.qml index 3e2cff3..0b3c972 100644 --- a/Modules/Settings/Tabs/ScreenRecorder.qml +++ b/Modules/Settings/Tabs/ScreenRecorder.qml @@ -13,17 +13,91 @@ Item { ColumnLayout { anchors.fill: parent spacing: Style.marginMedium * scaling - NText { - text: "Screen Recorder" - font.weight: Style.fontWeightBold - color: Colors.accentSecondary + + NText { text: "Screen Recording"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + + // Output Directory + NText { text: "Output Directory"; color: Colors.textPrimary; font.weight: Style.fontWeightBold } + NText { text: "Directory where screen recordings will be saved"; color: Colors.textSecondary } + NTextBox { + text: Settings.data.screenRecorder.directory + Layout.fillWidth: true + onEditingFinished: Settings.data.screenRecorder.directory = text } - NText { - text: "Coming soon" - color: Colors.textSecondary + + // Frame Rate + NText { text: "Frame Rate"; color: Colors.textPrimary; font.weight: Style.fontWeightBold } + NText { text: "Target frame rate for screen recordings (default: 60)"; color: Colors.textSecondary } + RowLayout { + Layout.fillWidth: true + NText { text: Settings.data.screenRecorder.frameRate + " FPS"; color: Colors.textPrimary } + Item { Layout.fillWidth: true } } - Item { - Layout.fillHeight: true + NSlider { + Layout.fillWidth: true + from: 24; to: 144; stepSize: 1 + value: Settings.data.screenRecorder.frameRate + onMoved: Settings.data.screenRecorder.frameRate = Math.round(value) + cutoutColor: Colors.surface } + + // Audio Source + NText { text: "Audio Source"; color: Colors.textPrimary; font.weight: Style.fontWeightBold } + NText { text: "Audio source to capture during recording"; color: Colors.textSecondary } + NComboBox { + optionsKeys: ["default_output", "default_input", "both"] + optionsLabels: ["System Audio", "Microphone", "System Audio + Microphone"] + currentKey: Settings.data.screenRecorder.audioSource + onSelected: function (key) { Settings.data.screenRecorder.audioSource = key } + } + + // Video Quality + NText { text: "Video Quality"; color: Colors.textPrimary; font.weight: Style.fontWeightBold } + NText { text: "Higher quality results in larger file sizes"; color: Colors.textSecondary } + NComboBox { + optionsKeys: ["medium", "high", "very_high", "ultra"] + optionsLabels: ["Medium", "High", "Very High", "Ultra"] + currentKey: Settings.data.screenRecorder.quality + onSelected: function (key) { Settings.data.screenRecorder.quality = key } + } + + // Video Codec + NText { text: "Video Codec"; color: Colors.textPrimary; font.weight: Style.fontWeightBold } + NText { text: "Different codecs offer different compression and compatibility"; color: Colors.textSecondary } + NComboBox { + optionsKeys: ["h264", "hevc", "av1", "vp8", "vp9"] + optionsLabels: ["H264", "HEVC", "AV1", "VP8", "VP9"] + currentKey: Settings.data.screenRecorder.videoCodec + onSelected: function (key) { Settings.data.screenRecorder.videoCodec = key } + } + + // Audio Codec + NText { text: "Audio Codec"; color: Colors.textPrimary; font.weight: Style.fontWeightBold } + NText { text: "Opus is recommended for best performance and smallest audio size"; color: Colors.textSecondary } + NComboBox { + optionsKeys: ["opus", "aac"] + optionsLabels: ["OPUS", "AAC"] + currentKey: Settings.data.screenRecorder.audioCodec + onSelected: function (key) { Settings.data.screenRecorder.audioCodec = key } + } + + // Color Range + NText { text: "Color Range"; color: Colors.textPrimary; font.weight: Style.fontWeightBold } + NText { text: "Limited is recommended for better compatibility"; color: Colors.textSecondary } + NComboBox { + optionsKeys: ["limited", "full"] + optionsLabels: ["Limited", "Full"] + currentKey: Settings.data.screenRecorder.colorRange + onSelected: function (key) { Settings.data.screenRecorder.colorRange = key } + } + + NToggle { + label: "Show Cursor" + description: "Record mouse cursor in the video" + value: Settings.data.screenRecorder.showCursor + onToggled: function (newValue) { Settings.data.screenRecorder.showCursor = newValue } + } + + Item { Layout.fillHeight: true } } } diff --git a/Modules/Settings/Tabs/TimeWeather.qml b/Modules/Settings/Tabs/TimeWeather.qml index 6bf1b1c..a3496db 100644 --- a/Modules/Settings/Tabs/TimeWeather.qml +++ b/Modules/Settings/Tabs/TimeWeather.qml @@ -13,17 +13,50 @@ Item { ColumnLayout { anchors.fill: parent spacing: Style.marginMedium * scaling - NText { - text: "Time & Weather" - font.weight: Style.fontWeightBold - color: Colors.accentSecondary + + NText { text: "Time"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + + NToggle { + label: "Use 12 Hour Clock" + description: "Display time in 12-hour format (e.g., 2:30 PM) instead of 24-hour format" + value: Settings.data.location.use12HourClock + onToggled: function (newValue) { Settings.data.location.use12HourClock = newValue } } - NText { - text: "Coming soon" - color: Colors.textSecondary + + NToggle { + label: "US Style Date" + description: "Display dates in MM/DD/YYYY format instead of DD/MM/YYYY" + value: Settings.data.location.reverseDayMonth + onToggled: function (newValue) { Settings.data.location.reverseDayMonth = newValue } } - Item { - Layout.fillHeight: true + + NDivider { Layout.fillWidth: true } + + NText { text: "Weather"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + + NText { text: "City"; color: Colors.textPrimary; font.weight: Style.fontWeightBold } + NText { text: "Your city name for weather information"; color: Colors.textSecondary } + NTextBox { + text: Settings.data.location.name + Layout.fillWidth: true + onEditingFinished: Settings.data.location.name = text } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginSmall * scaling + ColumnLayout { Layout.fillWidth: true; spacing: 2 * scaling + NText { text: "Temperature Unit"; color: Colors.textPrimary; font.weight: Style.fontWeightBold } + NText { text: "Choose between Celsius and Fahrenheit"; color: Colors.textSecondary; wrapMode: Text.WordWrap } + } + NComboBox { + optionsKeys: ["c", "f"] + optionsLabels: ["Celsius", "Fahrenheit"] + currentKey: Settings.data.location.useFahrenheit ? "f" : "c" + onSelected: function (key) { Settings.data.location.useFahrenheit = (key === "f") } + } + } + + Item { Layout.fillHeight: true } } } diff --git a/Modules/Settings/Tabs/Wallpaper.qml b/Modules/Settings/Tabs/Wallpaper.qml index cd90098..b426a83 100644 --- a/Modules/Settings/Tabs/Wallpaper.qml +++ b/Modules/Settings/Tabs/Wallpaper.qml @@ -13,17 +13,106 @@ Item { ColumnLayout { anchors.fill: parent spacing: Style.marginMedium * scaling - NText { - text: "Wallpaper" - font.weight: Style.fontWeightBold - color: Colors.accentSecondary + + NText { text: "Wallpaper Settings"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + + // Folder + NText { text: "Wallpaper Folder"; color: Colors.textPrimary; font.weight: Style.fontWeightBold } + NText { text: "Path to your wallpaper folder"; color: Colors.textSecondary; wrapMode: Text.WordWrap } + NTextBox { + text: Settings.data.wallpaper.directory + Layout.fillWidth: true + onEditingFinished: Settings.data.wallpaper.directory = text } - NText { - text: "Coming soon" - color: Colors.textSecondary + + NDivider { Layout.fillWidth: true } + + NText { text: "Automation"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + + NToggle { + label: "Random Wallpaper" + description: "Automatically select random wallpapers from the folder" + value: Settings.data.wallpaper.isRandom + onToggled: function (newValue) { Settings.data.wallpaper.isRandom = newValue } } - Item { - Layout.fillHeight: true + + NToggle { + label: "Use Wallpaper Theme" + description: "Automatically adjust theme colors based on wallpaper" + value: Settings.data.wallpaper.generateTheme + onToggled: function (newValue) { Settings.data.wallpaper.generateTheme = newValue } } + + NText { text: "Wallpaper Interval"; color: Colors.textPrimary; font.weight: Style.fontWeightBold } + NText { text: "How often to change wallpapers automatically (in seconds)"; color: Colors.textSecondary } + RowLayout { + Layout.fillWidth: true + NText { text: Settings.data.wallpaper.randomInterval + " seconds"; color: Colors.textPrimary } + Item { Layout.fillWidth: true } + } + NSlider { + Layout.fillWidth: true + from: 10; to: 900; stepSize: 10 + value: Settings.data.wallpaper.randomInterval + onMoved: Settings.data.wallpaper.randomInterval = Math.round(value) + cutoutColor: Colors.backgroundPrimary + } + + NDivider { Layout.fillWidth: true } + + NText { text: "SWWW"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + + NToggle { + label: "Use SWWW" + description: "Use SWWW daemon for advanced wallpaper management" + value: Settings.data.wallpaper.swww.enabled + onToggled: function (newValue) { Settings.data.wallpaper.swww.enabled = newValue } + } + + // SWWW settings + ColumnLayout { + spacing: Style.marginSmall * scaling + visible: Settings.data.wallpaper.swww.enabled + + NText { text: "Resize Mode"; color: Colors.textPrimary; font.weight: Style.fontWeightBold } + NText { text: "How SWWW should resize wallpapers to fit the screen"; color: Colors.textSecondary; wrapMode: Text.WordWrap } + NComboBox { + optionsKeys: ["no", "crop", "fit", "stretch"] + optionsLabels: ["No", "Crop", "Fit", "Stretch"] + currentKey: Settings.data.wallpaper.swww.resizeMethod + onSelected: function (key) { Settings.data.wallpaper.swww.resizeMethod = key } + } + + NText { text: "Transition Type"; color: Colors.textPrimary; font.weight: Style.fontWeightBold } + NText { text: "Animation type when switching between wallpapers"; color: Colors.textSecondary; wrapMode: Text.WordWrap } + NComboBox { + optionsKeys: ["none", "simple", "fade", "left", "right", "top", "bottom", "wipe", "wave", "grow", "center", "any", "outer", "random"] + optionsLabels: ["None", "Simple", "Fade", "Left", "Right", "Top", "Bottom", "Wipe", "Wave", "Grow", "Center", "Any", "Outer", "Random"] + currentKey: Settings.data.wallpaper.swww.transitionType + onSelected: function (key) { Settings.data.wallpaper.swww.transitionType = key } + } + + NText { text: "Transition FPS"; color: Colors.textPrimary; font.weight: Style.fontWeightBold } + RowLayout { Layout.fillWidth: true; NText { text: Settings.data.wallpaper.swww.transitionFps + " FPS"; color: Colors.textPrimary }; Item { Layout.fillWidth: true } } + NSlider { + Layout.fillWidth: true + from: 30; to: 500; stepSize: 5 + value: Settings.data.wallpaper.swww.transitionFps + onMoved: Settings.data.wallpaper.swww.transitionFps = Math.round(value) + cutoutColor: Colors.backgroundPrimary + } + + NText { text: "Transition Duration"; color: Colors.textPrimary; font.weight: Style.fontWeightBold } + RowLayout { Layout.fillWidth: true; NText { text: Settings.data.wallpaper.swww.transitionDuration.toFixed(2) + " s"; color: Colors.textPrimary }; Item { Layout.fillWidth: true } } + NSlider { + Layout.fillWidth: true + from: 0.25; to: 10; stepSize: 0.05 + value: Settings.data.wallpaper.swww.transitionDuration + onMoved: Settings.data.wallpaper.swww.transitionDuration = value + cutoutColor: Colors.backgroundPrimary + } + } + + Item { Layout.fillHeight: true } } } diff --git a/Services/Settings.qml b/Services/Settings.qml index f13de43..4f1a444 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -26,8 +26,8 @@ Singleton { // Used to access via Settings.data.xxx.yyy property var data: adapter - // Needed to only have one NPanel loaded at a time. - // property var openPanel: null + // Needed to only have one NPanel loaded at a time. <--- VERY BROKEN + //property var openPanel: null Item { Component.onCompleted: { @@ -68,6 +68,8 @@ Singleton { property bool showActiveWindowIcon: false property bool showSystemInfo: false property bool showMedia: false + // New: optional taskbar visibility in bar + property bool showTaskbar: false property list monitors: [] } @@ -102,6 +104,8 @@ Singleton { property string quality: "very_high" property string colorRange: "limited" property bool showCursor: true + // New: optional audio source selection (default: system output) + property string audioSource: "default_output" } // wallpaper From ce66b99cee978f51c845313013c4b0268d4b0b01 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 21:15:03 -0400 Subject: [PATCH 142/394] SidePanel: basic weather display --- Modules/SidePanel/WeatherCard.qml | 33 ++++++++++++++++----- Services/Location.qml | 49 +++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 7 deletions(-) diff --git a/Modules/SidePanel/WeatherCard.qml b/Modules/SidePanel/WeatherCard.qml index 5d52dae..5a1a29e 100644 --- a/Modules/SidePanel/WeatherCard.qml +++ b/Modules/SidePanel/WeatherCard.qml @@ -24,18 +24,27 @@ NBox { RowLayout { spacing: Style.marginSmall * scaling Text { - text: "sunny" + text: Location.weatherSymbolFromCode(Location.data.weather.current_weather.weathercode) font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXXL * 1.25 * scaling color: Colors.accentSecondary } ColumnLayout { - NText { - text: "Dinslaken (GMT+2)" + RowLayout { + NText { + text: Settings.data.location.name + font.weight: Style.fontWeightBold + font.pointSize: Style.fontSizeLarge * scaling + } + NText { + text: "(" + Location.data. weather.timezone_abbreviation + ")" + font.pointSize: Style.fontSizeTiny * scaling + } } + NText { text: "26°C" - font.pointSize: (Style.fontSizeXL + 6) * scaling + font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold } } @@ -55,17 +64,27 @@ NBox { delegate: ColumnLayout { spacing: 2 * scaling NText { - text: ["Sun", "Mon", "Tue", "Wed", "Thu"][index] + text: Qt.formatDateTime(new Date(Location.data.weather.daily.time[index]), "ddd") font.weight: Style.fontWeightBold } NText { - text: index % 2 === 0 ? "wb_sunny" : "cloud" + text: Location.weatherSymbolFromCode(Location.data.weather.daily.weathercode[index]) font.family: "Material Symbols Outlined" font.weight: Style.fontWeightBold color: Colors.textSecondary } NText { - text: "26° / 14°" + text: { + var max = Location.data.weather.daily.temperature_2m_max[index] + var min = Location.data.weather.daily.temperature_2m_min[index] + if (Settings.data.location.useFahrenheit) { + max = Location.celsiusToFahrenheit(max) + min = Location.celsiusToFahrenheit(min) + } + max = Math.round(max) + min = Math.round(min) + return `${max}° / ${min}°` + } color: Colors.textSecondary } } diff --git a/Services/Location.qml b/Services/Location.qml index 94d9e57..7692723 100644 --- a/Services/Location.qml +++ b/Services/Location.qml @@ -145,4 +145,53 @@ Singleton { function errorCallback(message) { console.error(message) } + + // -------------------------------- + function weatherSymbolFromCode(code) { + if (code === 0) + return "sunny" + if (code === 1 || code === 2) + return "partly_cloudy_day" + if (code === 3) + return "cloud" + if (code >= 45 && code <= 48) + return "foggy" + if (code >= 51 && code <= 67) + return "rainy" + if (code >= 71 && code <= 77) + return "weather_snowy" + if (code >= 80 && code <= 82) + return "rainy" + if (code >= 95 && code <= 99) + return "thunderstorm" + return "cloud" + } + + // -------------------------------- + function weatherDescriptionFromCode(code) { + if (code === 0) + return "Clear sky" + if (code === 1) + return "Mainly clear" + if (code === 2) + return "Partly cloudy" + if (code === 3) + return "Overcast" + if (code === 45 || code === 48) + return "Fog" + if (code >= 51 && code <= 67) + return "Drizzle" + if (code >= 71 && code <= 77) + return "Snow" + if (code >= 80 && code <= 82) + return "Rain showers" + if (code >= 95 && code <= 99) + return "Thunderstorm" + return "Unknown" + } + + // -------------------------------- + function celsiusToFahrenheit(celsius) { + return 32 + celsius * 1.8 + } } From 84c81ecb7710517ab93fc28b71aeff942275d535 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 21:15:54 -0400 Subject: [PATCH 143/394] Formatting --- Modules/Settings/Tabs/Bar.qml | 24 +++-- Modules/Settings/Tabs/Display.qml | 38 +++++-- Modules/Settings/Tabs/Misc.qml | 19 +++- Modules/Settings/Tabs/Network.qml | 26 +++-- Modules/Settings/Tabs/ScreenRecorder.qml | 124 ++++++++++++++++++----- Modules/Settings/Tabs/TimeWeather.qml | 59 ++++++++--- 6 files changed, 229 insertions(+), 61 deletions(-) diff --git a/Modules/Settings/Tabs/Bar.qml b/Modules/Settings/Tabs/Bar.qml index 3154cda..6a6ce6f 100644 --- a/Modules/Settings/Tabs/Bar.qml +++ b/Modules/Settings/Tabs/Bar.qml @@ -26,37 +26,49 @@ Item { label: "Show Active Window" description: "Display the title of the currently focused window below the bar" value: Settings.data.bar.showActiveWindow - onToggled: function (newValue) { Settings.data.bar.showActiveWindow = newValue } + onToggled: function (newValue) { + Settings.data.bar.showActiveWindow = newValue + } } NToggle { label: "Show Active Window Icon" description: "Display the icon of the currently focused window" value: Settings.data.bar.showActiveWindowIcon - onToggled: function (newValue) { Settings.data.bar.showActiveWindowIcon = newValue } + onToggled: function (newValue) { + Settings.data.bar.showActiveWindowIcon = newValue + } } NToggle { label: "Show System Info" description: "Display system information (CPU, RAM, Temperature)" value: Settings.data.bar.showSystemInfo - onToggled: function (newValue) { Settings.data.bar.showSystemInfo = newValue } + onToggled: function (newValue) { + Settings.data.bar.showSystemInfo = newValue + } } NToggle { label: "Show Taskbar" description: "Display a taskbar showing currently open windows" value: Settings.data.bar.showTaskbar - onToggled: function (newValue) { Settings.data.bar.showTaskbar = newValue } + onToggled: function (newValue) { + Settings.data.bar.showTaskbar = newValue + } } NToggle { label: "Show Media" description: "Display media controls and information" value: Settings.data.bar.showMedia - onToggled: function (newValue) { Settings.data.bar.showMedia = newValue } + onToggled: function (newValue) { + Settings.data.bar.showMedia = newValue + } } - Item { Layout.fillHeight: true } + Item { + Layout.fillHeight: true + } } } diff --git a/Modules/Settings/Tabs/Display.qml b/Modules/Settings/Tabs/Display.qml index 83a52ff..f7295bc 100644 --- a/Modules/Settings/Tabs/Display.qml +++ b/Modules/Settings/Tabs/Display.qml @@ -13,17 +13,26 @@ Item { // Helper functions to update arrays immutably function addMonitor(list, name) { - const arr = (list || []).slice(); if (!arr.includes(name)) arr.push(name); return arr + const arr = (list || []).slice() + if (!arr.includes(name)) + arr.push(name) + return arr } function removeMonitor(list, name) { - return (list || []).filter(function (n) { return n !== name }) + return (list || []).filter(function (n) { + return n !== name + }) } ColumnLayout { anchors.fill: parent spacing: Style.marginMedium * scaling - NText { text: "Per‑monitor configuration"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + NText { + text: "Per‑monitor configuration" + font.weight: Style.fontWeightBold + color: Colors.accentSecondary + } Repeater { model: Quickshell.screens || [] @@ -41,12 +50,22 @@ Item { anchors.margins: Style.marginMedium * scaling spacing: Style.marginSmall * scaling - NText { text: (modelData.name || "Unknown"); font.weight: Style.fontWeightBold; color: Colors.accentPrimary } + NText { + text: (modelData.name || "Unknown") + font.weight: Style.fontWeightBold + color: Colors.accentPrimary + } RowLayout { spacing: Style.marginMedium * scaling - NText { text: `Resolution: ${modelData.width}x${modelData.height}`; color: Colors.textSecondary } - NText { text: `Position: (${modelData.x}, ${modelData.y})`; color: Colors.textSecondary } + NText { + text: `Resolution: ${modelData.width}x${modelData.height}` + color: Colors.textSecondary + } + NText { + text: `Position: (${modelData.x}, ${modelData.y})` + color: Colors.textSecondary + } } NToggle { @@ -83,7 +102,8 @@ Item { if (newValue) { Settings.data.notifications.monitors = addMonitor(Settings.data.notifications.monitors, modelData.name) } else { - Settings.data.notifications.monitors = removeMonitor(Settings.data.notifications.monitors, modelData.name) + Settings.data.notifications.monitors = removeMonitor(Settings.data.notifications.monitors, + modelData.name) } } } @@ -91,6 +111,8 @@ Item { } } - Item { Layout.fillHeight: true } + Item { + Layout.fillHeight: true + } } } diff --git a/Modules/Settings/Tabs/Misc.qml b/Modules/Settings/Tabs/Misc.qml index 976331f..d64f3dc 100644 --- a/Modules/Settings/Tabs/Misc.qml +++ b/Modules/Settings/Tabs/Misc.qml @@ -20,17 +20,28 @@ Item { color: Colors.accentSecondary } - NText { text: "Visualizer Type"; color: Colors.textPrimary; font.weight: Style.fontWeightBold } - NText { text: "Choose the style of the audio visualizer"; color: Colors.textSecondary } + NText { + text: "Visualizer Type" + color: Colors.textPrimary + font.weight: Style.fontWeightBold + } + NText { + text: "Choose the style of the audio visualizer" + color: Colors.textSecondary + } NComboBox { id: visualizerTypeComboBox optionsKeys: ["radial", "fire", "diamond"] optionsLabels: ["Radial", "Fire", "Diamond"] currentKey: Settings.data.audioVisualizer.type - onSelected: function (key) { Settings.data.audioVisualizer.type = key } + onSelected: function (key) { + Settings.data.audioVisualizer.type = key + } } - Item { Layout.fillHeight: true } + Item { + Layout.fillHeight: true + } } } diff --git a/Modules/Settings/Tabs/Network.qml b/Modules/Settings/Tabs/Network.qml index 8e922a7..fd16ed7 100644 --- a/Modules/Settings/Tabs/Network.qml +++ b/Modules/Settings/Tabs/Network.qml @@ -16,7 +16,11 @@ Item { anchors.fill: parent spacing: Style.marginMedium * scaling - NText { text: "Wi‑Fi"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + NText { + text: "Wi‑Fi" + font.weight: Style.fontWeightBold + color: Colors.accentSecondary + } NToggle { label: "Enable Wi‑Fi" @@ -24,12 +28,19 @@ Item { value: Settings.data.network.wifiEnabled onToggled: function (newValue) { Settings.data.network.wifiEnabled = newValue - Quickshell.execDetached(["nmcli", "radio", "wifi", newValue ? "on" : "off"]) } + Quickshell.execDetached(["nmcli", "radio", "wifi", newValue ? "on" : "off"]) + } } - NDivider { Layout.fillWidth: true } + NDivider { + Layout.fillWidth: true + } - NText { text: "Bluetooth"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + NText { + text: "Bluetooth" + font.weight: Style.fontWeightBold + color: Colors.accentSecondary + } NToggle { label: "Enable Bluetooth" @@ -39,11 +50,14 @@ Item { Settings.data.network.bluetoothEnabled = newValue if (Bluetooth.defaultAdapter) { Bluetooth.defaultAdapter.enabled = newValue - if (Bluetooth.defaultAdapter.enabled) Bluetooth.defaultAdapter.discovering = true + if (Bluetooth.defaultAdapter.enabled) + Bluetooth.defaultAdapter.discovering = true } } } - Item { Layout.fillHeight: true } + Item { + Layout.fillHeight: true + } } } diff --git a/Modules/Settings/Tabs/ScreenRecorder.qml b/Modules/Settings/Tabs/ScreenRecorder.qml index 0b3c972..b414ea2 100644 --- a/Modules/Settings/Tabs/ScreenRecorder.qml +++ b/Modules/Settings/Tabs/ScreenRecorder.qml @@ -14,11 +14,22 @@ Item { anchors.fill: parent spacing: Style.marginMedium * scaling - NText { text: "Screen Recording"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + NText { + text: "Screen Recording" + font.weight: Style.fontWeightBold + color: Colors.accentSecondary + } // Output Directory - NText { text: "Output Directory"; color: Colors.textPrimary; font.weight: Style.fontWeightBold } - NText { text: "Directory where screen recordings will be saved"; color: Colors.textSecondary } + NText { + text: "Output Directory" + color: Colors.textPrimary + font.weight: Style.fontWeightBold + } + NText { + text: "Directory where screen recordings will be saved" + color: Colors.textSecondary + } NTextBox { text: Settings.data.screenRecorder.directory Layout.fillWidth: true @@ -26,78 +37,141 @@ Item { } // Frame Rate - NText { text: "Frame Rate"; color: Colors.textPrimary; font.weight: Style.fontWeightBold } - NText { text: "Target frame rate for screen recordings (default: 60)"; color: Colors.textSecondary } + NText { + text: "Frame Rate" + color: Colors.textPrimary + font.weight: Style.fontWeightBold + } + NText { + text: "Target frame rate for screen recordings (default: 60)" + color: Colors.textSecondary + } RowLayout { Layout.fillWidth: true - NText { text: Settings.data.screenRecorder.frameRate + " FPS"; color: Colors.textPrimary } - Item { Layout.fillWidth: true } + NText { + text: Settings.data.screenRecorder.frameRate + " FPS" + color: Colors.textPrimary + } + Item { + Layout.fillWidth: true + } } NSlider { Layout.fillWidth: true - from: 24; to: 144; stepSize: 1 + from: 24 + to: 144 + stepSize: 1 value: Settings.data.screenRecorder.frameRate onMoved: Settings.data.screenRecorder.frameRate = Math.round(value) cutoutColor: Colors.surface } // Audio Source - NText { text: "Audio Source"; color: Colors.textPrimary; font.weight: Style.fontWeightBold } - NText { text: "Audio source to capture during recording"; color: Colors.textSecondary } + NText { + text: "Audio Source" + color: Colors.textPrimary + font.weight: Style.fontWeightBold + } + NText { + text: "Audio source to capture during recording" + color: Colors.textSecondary + } NComboBox { optionsKeys: ["default_output", "default_input", "both"] optionsLabels: ["System Audio", "Microphone", "System Audio + Microphone"] currentKey: Settings.data.screenRecorder.audioSource - onSelected: function (key) { Settings.data.screenRecorder.audioSource = key } + onSelected: function (key) { + Settings.data.screenRecorder.audioSource = key + } } // Video Quality - NText { text: "Video Quality"; color: Colors.textPrimary; font.weight: Style.fontWeightBold } - NText { text: "Higher quality results in larger file sizes"; color: Colors.textSecondary } + NText { + text: "Video Quality" + color: Colors.textPrimary + font.weight: Style.fontWeightBold + } + NText { + text: "Higher quality results in larger file sizes" + color: Colors.textSecondary + } NComboBox { optionsKeys: ["medium", "high", "very_high", "ultra"] optionsLabels: ["Medium", "High", "Very High", "Ultra"] currentKey: Settings.data.screenRecorder.quality - onSelected: function (key) { Settings.data.screenRecorder.quality = key } + onSelected: function (key) { + Settings.data.screenRecorder.quality = key + } } // Video Codec - NText { text: "Video Codec"; color: Colors.textPrimary; font.weight: Style.fontWeightBold } - NText { text: "Different codecs offer different compression and compatibility"; color: Colors.textSecondary } + NText { + text: "Video Codec" + color: Colors.textPrimary + font.weight: Style.fontWeightBold + } + NText { + text: "Different codecs offer different compression and compatibility" + color: Colors.textSecondary + } NComboBox { optionsKeys: ["h264", "hevc", "av1", "vp8", "vp9"] optionsLabels: ["H264", "HEVC", "AV1", "VP8", "VP9"] currentKey: Settings.data.screenRecorder.videoCodec - onSelected: function (key) { Settings.data.screenRecorder.videoCodec = key } + onSelected: function (key) { + Settings.data.screenRecorder.videoCodec = key + } } // Audio Codec - NText { text: "Audio Codec"; color: Colors.textPrimary; font.weight: Style.fontWeightBold } - NText { text: "Opus is recommended for best performance and smallest audio size"; color: Colors.textSecondary } + NText { + text: "Audio Codec" + color: Colors.textPrimary + font.weight: Style.fontWeightBold + } + NText { + text: "Opus is recommended for best performance and smallest audio size" + color: Colors.textSecondary + } NComboBox { optionsKeys: ["opus", "aac"] optionsLabels: ["OPUS", "AAC"] currentKey: Settings.data.screenRecorder.audioCodec - onSelected: function (key) { Settings.data.screenRecorder.audioCodec = key } + onSelected: function (key) { + Settings.data.screenRecorder.audioCodec = key + } } // Color Range - NText { text: "Color Range"; color: Colors.textPrimary; font.weight: Style.fontWeightBold } - NText { text: "Limited is recommended for better compatibility"; color: Colors.textSecondary } + NText { + text: "Color Range" + color: Colors.textPrimary + font.weight: Style.fontWeightBold + } + NText { + text: "Limited is recommended for better compatibility" + color: Colors.textSecondary + } NComboBox { optionsKeys: ["limited", "full"] optionsLabels: ["Limited", "Full"] currentKey: Settings.data.screenRecorder.colorRange - onSelected: function (key) { Settings.data.screenRecorder.colorRange = key } + onSelected: function (key) { + Settings.data.screenRecorder.colorRange = key + } } NToggle { label: "Show Cursor" description: "Record mouse cursor in the video" value: Settings.data.screenRecorder.showCursor - onToggled: function (newValue) { Settings.data.screenRecorder.showCursor = newValue } + onToggled: function (newValue) { + Settings.data.screenRecorder.showCursor = newValue + } } - Item { Layout.fillHeight: true } + Item { + Layout.fillHeight: true + } } } diff --git a/Modules/Settings/Tabs/TimeWeather.qml b/Modules/Settings/Tabs/TimeWeather.qml index a3496db..b282157 100644 --- a/Modules/Settings/Tabs/TimeWeather.qml +++ b/Modules/Settings/Tabs/TimeWeather.qml @@ -14,28 +14,49 @@ Item { anchors.fill: parent spacing: Style.marginMedium * scaling - NText { text: "Time"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + NText { + text: "Time" + font.weight: Style.fontWeightBold + color: Colors.accentSecondary + } NToggle { label: "Use 12 Hour Clock" description: "Display time in 12-hour format (e.g., 2:30 PM) instead of 24-hour format" value: Settings.data.location.use12HourClock - onToggled: function (newValue) { Settings.data.location.use12HourClock = newValue } + onToggled: function (newValue) { + Settings.data.location.use12HourClock = newValue + } } NToggle { label: "US Style Date" description: "Display dates in MM/DD/YYYY format instead of DD/MM/YYYY" value: Settings.data.location.reverseDayMonth - onToggled: function (newValue) { Settings.data.location.reverseDayMonth = newValue } + onToggled: function (newValue) { + Settings.data.location.reverseDayMonth = newValue + } } - NDivider { Layout.fillWidth: true } + NDivider { + Layout.fillWidth: true + } - NText { text: "Weather"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + NText { + text: "Weather" + font.weight: Style.fontWeightBold + color: Colors.accentSecondary + } - NText { text: "City"; color: Colors.textPrimary; font.weight: Style.fontWeightBold } - NText { text: "Your city name for weather information"; color: Colors.textSecondary } + NText { + text: "City" + color: Colors.textPrimary + font.weight: Style.fontWeightBold + } + NText { + text: "Your city name for weather information" + color: Colors.textSecondary + } NTextBox { text: Settings.data.location.name Layout.fillWidth: true @@ -45,18 +66,32 @@ Item { RowLayout { Layout.fillWidth: true spacing: Style.marginSmall * scaling - ColumnLayout { Layout.fillWidth: true; spacing: 2 * scaling - NText { text: "Temperature Unit"; color: Colors.textPrimary; font.weight: Style.fontWeightBold } - NText { text: "Choose between Celsius and Fahrenheit"; color: Colors.textSecondary; wrapMode: Text.WordWrap } + ColumnLayout { + Layout.fillWidth: true + spacing: 2 * scaling + NText { + text: "Temperature Unit" + color: Colors.textPrimary + font.weight: Style.fontWeightBold + } + NText { + text: "Choose between Celsius and Fahrenheit" + color: Colors.textSecondary + wrapMode: Text.WordWrap + } } NComboBox { optionsKeys: ["c", "f"] optionsLabels: ["Celsius", "Fahrenheit"] currentKey: Settings.data.location.useFahrenheit ? "f" : "c" - onSelected: function (key) { Settings.data.location.useFahrenheit = (key === "f") } + onSelected: function (key) { + Settings.data.location.useFahrenheit = (key === "f") + } } } - Item { Layout.fillHeight: true } + Item { + Layout.fillHeight: true + } } } From c71029487a02add3ac61fb529d8e2ed21c557ec2 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 21:35:51 -0400 Subject: [PATCH 144/394] Better weather --- Modules/SidePanel/WeatherCard.qml | 23 +++++++++++++++++------ Services/Location.qml | 12 ++++++++++-- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/Modules/SidePanel/WeatherCard.qml b/Modules/SidePanel/WeatherCard.qml index 5a1a29e..48965b7 100644 --- a/Modules/SidePanel/WeatherCard.qml +++ b/Modules/SidePanel/WeatherCard.qml @@ -21,9 +21,10 @@ NBox { anchors.margins: Style.marginMedium * scaling spacing: Style.marginMedium * scaling + RowLayout { spacing: Style.marginSmall * scaling - Text { + NText { text: Location.weatherSymbolFromCode(Location.data.weather.current_weather.weathercode) font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXXL * 1.25 * scaling @@ -37,13 +38,21 @@ NBox { font.pointSize: Style.fontSizeLarge * scaling } NText { - text: "(" + Location.data. weather.timezone_abbreviation + ")" + text: `(${Location.data.weather.timezone_abbreviation})` font.pointSize: Style.fontSizeTiny * scaling + visible: Location.data.weather } } NText { - text: "26°C" + text: { + var temp = Location.data.weather.current_weather.temperature + if (Settings.data.location.useFahrenheit) { + temp = Location.celsiusToFahrenheit(temp) + } + temp = Math.round(temp) + return `${temp}°` + } font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold } @@ -60,9 +69,9 @@ NBox { Layout.fillWidth: true spacing: Style.marginMedium * scaling Repeater { - model: 5 + model: Location.data.weather.daily.time delegate: ColumnLayout { - spacing: 2 * scaling + spacing: Style.spacingSmall * scaling NText { text: Qt.formatDateTime(new Date(Location.data.weather.daily.time[index]), "ddd") font.weight: Style.fontWeightBold @@ -70,6 +79,7 @@ NBox { NText { text: Location.weatherSymbolFromCode(Location.data.weather.daily.weathercode[index]) font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeLarge * scaling font.weight: Style.fontWeightBold color: Colors.textSecondary } @@ -83,8 +93,9 @@ NBox { } max = Math.round(max) min = Math.round(min) - return `${max}° / ${min}°` + return `${max}°/${min}°` } + font.pointSize: Style.fontSizeSmall * scaling color: Colors.textSecondary } } diff --git a/Services/Location.qml b/Services/Location.qml index 7692723..c33ca76 100644 --- a/Services/Location.qml +++ b/Services/Location.qml @@ -11,6 +11,7 @@ Singleton { property string locationFile: Quickshell.env("NOCTALIA_WEATHER_FILE") || (Settings.cacheDir + "location.json") property int weatherUpdateFrequency: 30 * 60 // 30 minutes expressed in seconds property var data: adapter // Used to access via Location.data.xxx.yyy + property bool isFetchingWeather: false FileView { path: locationFile @@ -63,6 +64,10 @@ Singleton { // -------------------------------- function updateWeather() { + if (isFetchingWeather) { + return + } + if ((data.weatherLastFetch === "") || (Time.timestamp >= data.weatherLastFetch + weatherUpdateFrequency)) { getFreshWeather() } @@ -70,10 +75,11 @@ Singleton { // -------------------------------- function getFreshWeather() { + isFetchingWeather = true if (data.latitude === "" || data.longitude === "") { console.log("Geocoding location") _geocodeLocation(Settings.data.location.name, function (lat, lon) { - console.log("Geocoded " + Settings.data.location.name + " to : " + lat + " / " + lon) + console.log("Geocoded " + Settings.data.location.name + " to: " + lat + " / " + lon) // Save GPS coordinates data.latitude = lat @@ -128,6 +134,7 @@ Singleton { // Save to json data.weather = weatherData data.weatherLastFetch = Time.timestamp + isFetchingWeather = false console.log("Cached weather to disk") } catch (e) { errorCallback("Failed to parse weather data.") @@ -144,9 +151,10 @@ Singleton { // -------------------------------- function errorCallback(message) { console.error(message) + isFetchingWeather = false } - // -------------------------------- + // -------------------------------- function weatherSymbolFromCode(code) { if (code === 0) return "sunny" From 03687f95f30e311ca0e02af5b6ee1deca16664bc Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 21:48:17 -0400 Subject: [PATCH 145/394] WeatherCard: better layout --- Modules/SidePanel/WeatherCard.qml | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/Modules/SidePanel/WeatherCard.qml b/Modules/SidePanel/WeatherCard.qml index 48965b7..fede32c 100644 --- a/Modules/SidePanel/WeatherCard.qml +++ b/Modules/SidePanel/WeatherCard.qml @@ -21,11 +21,10 @@ NBox { anchors.margins: Style.marginMedium * scaling spacing: Style.marginMedium * scaling - RowLayout { spacing: Style.marginSmall * scaling NText { - text: Location.weatherSymbolFromCode(Location.data.weather.current_weather.weathercode) + text: Location.weatherSymbolFromCode(Location.data.weather.current_weather.weathercode) font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXXL * 1.25 * scaling color: Colors.accentSecondary @@ -33,13 +32,13 @@ NBox { ColumnLayout { RowLayout { NText { - text: Settings.data.location.name + text: Settings.data.location.name font.weight: Style.fontWeightBold - font.pointSize: Style.fontSizeLarge * scaling + font.pointSize: Style.fontSizeXL * scaling } NText { text: `(${Location.data.weather.timezone_abbreviation})` - font.pointSize: Style.fontSizeTiny * scaling + font.pointSize: Style.fontSizeSmall * scaling visible: Location.data.weather } } @@ -48,39 +47,38 @@ NBox { text: { var temp = Location.data.weather.current_weather.temperature if (Settings.data.location.useFahrenheit) { - temp = Location.celsiusToFahrenheit(temp) + temp = Location.celsiusToFahrenheit(temp) } temp = Math.round(temp) - return `${temp}°` + return `${temp}°` } - font.pointSize: Style.fontSizeXL * scaling + font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold } } } - Rectangle { - height: 1 - width: parent.width - color: Colors.backgroundTertiary + NDivider { + Layout.fillWidth: true } RowLayout { Layout.fillWidth: true - spacing: Style.marginMedium * scaling + Layout.alignment: Qt.AlignHCenter + spacing: Style.marginLarge* scaling Repeater { model: Location.data.weather.daily.time delegate: ColumnLayout { + Layout.alignment: Qt.AlignHCenter spacing: Style.spacingSmall * scaling NText { text: Qt.formatDateTime(new Date(Location.data.weather.daily.time[index]), "ddd") - font.weight: Style.fontWeightBold + color: Colors.textPrimary } NText { text: Location.weatherSymbolFromCode(Location.data.weather.daily.weathercode[index]) font.family: "Material Symbols Outlined" - font.pointSize: Style.fontSizeLarge * scaling - font.weight: Style.fontWeightBold + font.pointSize: Style.fontSizeXL * scaling color: Colors.textSecondary } NText { From 4fc4c94a47627b2780218f7940cb19243eaf4f20 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 22:17:17 -0400 Subject: [PATCH 146/394] Weather: fixed polling + improved WeatherCard with a NBusyIndicator if not ready yet --- Modules/Bar/WiFiMenu.qml | 2 +- Modules/SidePanel/WeatherCard.qml | 24 ++++++++++++++++++++---- Services/Location.qml | 10 +++++++++- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/Modules/Bar/WiFiMenu.qml b/Modules/Bar/WiFiMenu.qml index 6775d18..34e463a 100644 --- a/Modules/Bar/WiFiMenu.qml +++ b/Modules/Bar/WiFiMenu.qml @@ -6,7 +6,7 @@ import Quickshell.Wayland import qs.Services import qs.Widgets -// LazyLoader for WiFi menu +// Loader for WiFi menu NLoader { id: root diff --git a/Modules/SidePanel/WeatherCard.qml b/Modules/SidePanel/WeatherCard.qml index fede32c..62f5120 100644 --- a/Modules/SidePanel/WeatherCard.qml +++ b/Modules/SidePanel/WeatherCard.qml @@ -1,5 +1,6 @@ import QtQuick import QtQuick.Layouts +import Quickshell import qs.Services import qs.Widgets @@ -8,6 +9,7 @@ NBox { id: root readonly property real scaling: Scaling.scale(screen) + readonly property bool weatherReady: (Location.data.weather !== null) Layout.fillWidth: true // Height driven by content @@ -24,11 +26,12 @@ NBox { RowLayout { spacing: Style.marginSmall * scaling NText { - text: Location.weatherSymbolFromCode(Location.data.weather.current_weather.weathercode) + text: weatherReady ? Location.weatherSymbolFromCode(Location.data.weather.current_weather.weathercode) : "" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXXL * 1.25 * scaling color: Colors.accentSecondary } + ColumnLayout { RowLayout { NText { @@ -37,14 +40,18 @@ NBox { font.pointSize: Style.fontSizeXL * scaling } NText { - text: `(${Location.data.weather.timezone_abbreviation})` + text: weatherReady ? `(${Location.data.weather.timezone_abbreviation})` : "" font.pointSize: Style.fontSizeSmall * scaling visible: Location.data.weather } } NText { + visible: weatherReady text: { + if (!weatherReady) { + return "" + } var temp = Location.data.weather.current_weather.temperature if (Settings.data.location.useFahrenheit) { temp = Location.celsiusToFahrenheit(temp) @@ -59,15 +66,17 @@ NBox { } NDivider { + visible: weatherReady Layout.fillWidth: true } RowLayout { + visible: weatherReady Layout.fillWidth: true Layout.alignment: Qt.AlignHCenter - spacing: Style.marginLarge* scaling + spacing: Style.marginLarge * scaling Repeater { - model: Location.data.weather.daily.time + model: weatherReady ? Location.data.weather.daily.time : [] delegate: ColumnLayout { Layout.alignment: Qt.AlignHCenter spacing: Style.spacingSmall * scaling @@ -99,5 +108,12 @@ NBox { } } } + + RowLayout { + visible: !weatherReady + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + NBusyIndicator {} + } } } diff --git a/Services/Location.qml b/Services/Location.qml index c33ca76..b5f91d2 100644 --- a/Services/Location.qml +++ b/Services/Location.qml @@ -41,9 +41,11 @@ Singleton { } } + // Every minute check if we need to fetch new weather Timer { id: updateTimer interval: 60 * 1000 + running: true repeat: true onTriggered: { updateWeather() @@ -51,7 +53,9 @@ Singleton { } // -------------------------------- - function init() {// does nothing but ensure the singleton is created + function init() { + // does nothing but ensure the singleton is created + // do not remove } // -------------------------------- @@ -60,11 +64,15 @@ Singleton { data.longitude = "" data.weatherLastFetch = 0 data.weather = null + + // Try to fetch immediately + updateWeather(); } // -------------------------------- function updateWeather() { if (isFetchingWeather) { + console.warn("Weather is still fetching") return } From 419634ea2a0a984d7e7a0cf9989f8e3b6d69e024 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 22:17:27 -0400 Subject: [PATCH 147/394] Formatting --- Services/Location.qml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Services/Location.qml b/Services/Location.qml index b5f91d2..e71c109 100644 --- a/Services/Location.qml +++ b/Services/Location.qml @@ -53,8 +53,7 @@ Singleton { } // -------------------------------- - function init() { - // does nothing but ensure the singleton is created + function init() {// does nothing but ensure the singleton is created // do not remove } @@ -66,7 +65,7 @@ Singleton { data.weather = null // Try to fetch immediately - updateWeather(); + updateWeather() } // -------------------------------- From f2ca670dada1a20f2e5b0a042885231fdc8093fb Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 22:33:52 -0400 Subject: [PATCH 148/394] NComboBox height was too thin --- Widgets/NComboBox.qml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Widgets/NComboBox.qml b/Widgets/NComboBox.qml index bf971c0..b557efd 100644 --- a/Widgets/NComboBox.qml +++ b/Widgets/NComboBox.qml @@ -8,6 +8,7 @@ ComboBox { id: root readonly property real scaling: Scaling.scale(screen) + readonly property real preferredHeight: Style.baseWidgetSize * 1.25 * scaling property list optionsKeys: [] property list optionsLabels: [] @@ -15,7 +16,7 @@ ComboBox { property var onSelected: function (string) {} Layout.fillWidth: true - Layout.preferredHeight: Style.baseWidgetSize * scaling + Layout.preferredHeight: height model: optionsKeys currentIndex: model.indexOf(currentKey) @@ -26,7 +27,7 @@ ComboBox { // Rounded background background: Rectangle { implicitWidth: 120 * scaling - implicitHeight: Style.baseWidgetSize * scaling + implicitHeight: preferredHeight color: Colors.surfaceVariant border.color: root.activeFocus ? Colors.hover : Colors.outline border.width: Math.max(1, Style.borderThin * scaling) From 5802e066d640e67701221ad933c1ed8b3998437f Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 22:34:23 -0400 Subject: [PATCH 149/394] NTextBox (input) should use the same border (color, thickness) as other widgets (ex: NComboxBox) --- Widgets/NTextBox.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Widgets/NTextBox.qml b/Widgets/NTextBox.qml index 6d6c8a4..bbd70f1 100644 --- a/Widgets/NTextBox.qml +++ b/Widgets/NTextBox.qml @@ -33,8 +33,8 @@ Item { anchors.fill: parent radius: frame.radius color: "transparent" - border.color: input.activeFocus ? Colors.accentPrimary : "transparent" - border.width: input.activeFocus ? Math.max(1, Style.borderMedium * scaling) : 0 + border.color: input.activeFocus ? Colors.hover : "transparent" + border.width: input.activeFocus ? Math.max(1, Style.borderThin * scaling) : 0 } RowLayout { From 402c626ff864942ba65d9bad3ae99ff77733cd93 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 22:34:27 -0400 Subject: [PATCH 150/394] Formatting --- Modules/Settings/Tabs/About.qml | 170 +++++++++++++++++++++++----- Modules/Settings/Tabs/Wallpaper.qml | 150 +++++++++++++++++++----- 2 files changed, 262 insertions(+), 58 deletions(-) diff --git a/Modules/Settings/Tabs/About.qml b/Modules/Settings/Tabs/About.qml index 5eb8af1..3f8abe3 100644 --- a/Modules/Settings/Tabs/About.qml +++ b/Modules/Settings/Tabs/About.qml @@ -23,12 +23,15 @@ Item { function loadFromFile() { const now = Date.now() const data = githubData - if (!data.timestamp || (now - data.timestamp > 3600 * 1000)) { // 1h cache + if (!data.timestamp || (now - data.timestamp > 3600 * 1000)) { + // 1h cache fetchFromGitHub() return } - if (data.version) root.latestVersion = data.version - if (data.contributors) root.contributors = data.contributors + if (data.version) + root.latestVersion = data.version + if (data.contributors) + root.contributors = data.contributors } function fetchFromGitHub() { @@ -38,7 +41,9 @@ Item { function saveData() { githubData.timestamp = Date.now() - Qt.callLater(function () { githubDataFile.writeAdapter() }) + Qt.callLater(function () { + githubDataFile.writeAdapter() + }) } ColumnLayout { @@ -47,27 +52,67 @@ Item { spacing: Style.marginMedium * scaling // Header - NText { text: "Noctalia: quiet by design"; font.weight: Style.fontWeightBold; color: Colors.textPrimary } - NText { text: "It may just be another quickshell setup but it won't get in your way."; color: Colors.textSecondary } + NText { + text: "Noctalia: quiet by design" + font.weight: Style.fontWeightBold + color: Colors.textPrimary + } + NText { + text: "It may just be another quickshell setup but it won't get in your way." + color: Colors.textSecondary + } // Versions grid RowLayout { spacing: Style.marginLarge * scaling - ColumnLayout { NText { text: "Latest Version:"; color: Colors.textSecondary }; NText { text: root.latestVersion; font.weight: Style.fontWeightBold; color: Colors.textPrimary } } - ColumnLayout { NText { text: "Installed Version:"; color: Colors.textSecondary }; NText { text: root.currentVersion; font.weight: Style.fontWeightBold; color: Colors.textPrimary } } - Item { Layout.fillWidth: true } + ColumnLayout { + NText { + text: "Latest Version:" + color: Colors.textSecondary + } + NText { + text: root.latestVersion + font.weight: Style.fontWeightBold + color: Colors.textPrimary + } + } + ColumnLayout { + NText { + text: "Installed Version:" + color: Colors.textSecondary + } + NText { + text: root.currentVersion + font.weight: Style.fontWeightBold + color: Colors.textPrimary + } + } + Item { + Layout.fillWidth: true + } NIconButton { icon: "system_update" tooltipText: "Open latest release" - onClicked: Quickshell.execDetached(["xdg-open", "https://github.com/Ly-sec/Noctalia/releases/latest"]) } + onClicked: Quickshell.execDetached(["xdg-open", "https://github.com/Ly-sec/Noctalia/releases/latest"]) + } } - NDivider { Layout.fillWidth: true } + NDivider { + Layout.fillWidth: true + } // Contributors - RowLayout { spacing: Style.marginSmall * scaling - NText { text: "Contributors"; font.weight: Style.fontWeightBold; color: Colors.textPrimary } - NText { text: "(" + root.contributors.length + ")"; color: Colors.textSecondary } + RowLayout { + spacing: Style.marginSmall * scaling + NText { + text: "Contributors" + font.weight: Style.fontWeightBold + color: Colors.textPrimary + } + NText { + text: "(" + root.contributors.length + ")" + color: Colors.textSecondary + } } GridView { @@ -89,21 +134,64 @@ Item { Item { Layout.preferredWidth: 40 * scaling Layout.preferredHeight: 40 * scaling - Image { id: avatarImage; anchors.fill: parent; source: modelData.avatar_url || ""; asynchronous: true; visible: false; fillMode: Image.PreserveAspectCrop } - MultiEffect { anchors.fill: parent; source: avatarImage; maskEnabled: true; maskSource: mask } - Item { id: mask; anchors.fill: parent; visible: false; Rectangle { anchors.fill: parent; radius: width / 2 } } - NText { anchors.centerIn: parent; text: "person"; font.family: "Material Symbols Outlined"; color: contributorArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary; visible: !avatarImage.source || avatarImage.status !== Image.Ready } + Image { + id: avatarImage + anchors.fill: parent + source: modelData.avatar_url || "" + asynchronous: true + visible: false + fillMode: Image.PreserveAspectCrop + } + MultiEffect { + anchors.fill: parent + source: avatarImage + maskEnabled: true + maskSource: mask + } + Item { + id: mask + anchors.fill: parent + visible: false + Rectangle { + anchors.fill: parent + radius: width / 2 + } + } + NText { + anchors.centerIn: parent + text: "person" + font.family: "Material Symbols Outlined" + color: contributorArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary + visible: !avatarImage.source || avatarImage.status !== Image.Ready + } } - ColumnLayout { Layout.fillWidth: true; spacing: 2 * scaling - NText { text: modelData.login || "Unknown"; color: contributorArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary } - NText { text: (modelData.contributions || 0) + " commits"; color: contributorArea.containsMouse ? Colors.backgroundPrimary : Colors.textSecondary } + ColumnLayout { + Layout.fillWidth: true + spacing: 2 * scaling + NText { + text: modelData.login || "Unknown" + color: contributorArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary + } + NText { + text: (modelData.contributions || 0) + " commits" + color: contributorArea.containsMouse ? Colors.backgroundPrimary : Colors.textSecondary + } } } - MouseArea { id: contributorArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onClicked: if (modelData.html_url) Quickshell.execDetached(["xdg-open", modelData.html_url]) } + MouseArea { + id: contributorArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: if (modelData.html_url) + Quickshell.execDetached(["xdg-open", modelData.html_url]) + } } } - Item { Layout.fillHeight: true } + Item { + Layout.fillHeight: true + } } // Processes and persistence @@ -117,7 +205,8 @@ Item { if (version && version !== "Unknown") { root.currentVersion = version } else { - currentVersionProcess.command = ["sh", "-c", "cd " + Quickshell.shellDir + " && cat package.json 2>/dev/null | grep '\"version\"' | cut -d'\"' -f4 || echo 'Unknown'"] + currentVersionProcess.command = ["sh", "-c", "cd " + Quickshell.shellDir + + " && cat package.json 2>/dev/null | grep '\"version\"' | cut -d'\"' -f4 || echo 'Unknown'"] currentVersionProcess.running = true } } @@ -133,10 +222,22 @@ Item { onFileChanged: githubDataFile.reload() onLoaded: loadFromFile() onLoadFailed: { - githubData.version = "Unknown"; githubData.contributors = []; githubData.timestamp = 0; githubDataFile.writeAdapter(); fetchFromGitHub() + githubData.version = "Unknown" + githubData.contributors = [] + githubData.timestamp = 0 + githubDataFile.writeAdapter() + fetchFromGitHub() + } + Component.onCompleted: { + if (path) + reload() + } + JsonAdapter { + id: githubData + property string version: "Unknown" + property var contributors: [] + property double timestamp: 0 } - Component.onCompleted: { if (path) reload() } - JsonAdapter { id: githubData; property string version: "Unknown"; property var contributors: []; property double timestamp: 0 } } Process { @@ -146,9 +247,15 @@ Item { onStreamFinished: { try { const data = JSON.parse(text) - if (data.tag_name) { const version = data.tag_name; githubData.version = version; root.latestVersion = version } + if (data.tag_name) { + const version = data.tag_name + githubData.version = version + root.latestVersion = version + } saveData() - } catch (e) { console.error("Failed to parse version:", e) } + } catch (e) { + console.error("Failed to parse version:", e) + } } } } @@ -163,7 +270,10 @@ Item { githubData.contributors = data || [] root.contributors = githubData.contributors saveData() - } catch (e) { console.error("Failed to parse contributors:", e); root.contributors = [] } + } catch (e) { + console.error("Failed to parse contributors:", e) + root.contributors = [] + } } } } diff --git a/Modules/Settings/Tabs/Wallpaper.qml b/Modules/Settings/Tabs/Wallpaper.qml index b426a83..923cc0f 100644 --- a/Modules/Settings/Tabs/Wallpaper.qml +++ b/Modules/Settings/Tabs/Wallpaper.qml @@ -14,59 +14,104 @@ Item { anchors.fill: parent spacing: Style.marginMedium * scaling - NText { text: "Wallpaper Settings"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + NText { + text: "Wallpaper Settings" + font.weight: Style.fontWeightBold + color: Colors.accentSecondary + } // Folder - NText { text: "Wallpaper Folder"; color: Colors.textPrimary; font.weight: Style.fontWeightBold } - NText { text: "Path to your wallpaper folder"; color: Colors.textSecondary; wrapMode: Text.WordWrap } + NText { + text: "Wallpaper Folder" + + font.weight: Style.fontWeightBold + } + NText { + text: "Path to your wallpaper folder" + color: Colors.textSecondary + wrapMode: Text.WordWrap + } NTextBox { text: Settings.data.wallpaper.directory Layout.fillWidth: true onEditingFinished: Settings.data.wallpaper.directory = text } - NDivider { Layout.fillWidth: true } + NDivider { + Layout.fillWidth: true + } - NText { text: "Automation"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + // ---------------------------- + NText { + text: "Automation" + font.weight: Style.fontWeightBold + color: Colors.accentSecondary + } NToggle { label: "Random Wallpaper" description: "Automatically select random wallpapers from the folder" value: Settings.data.wallpaper.isRandom - onToggled: function (newValue) { Settings.data.wallpaper.isRandom = newValue } + onToggled: function (newValue) { + Settings.data.wallpaper.isRandom = newValue + } } NToggle { label: "Use Wallpaper Theme" description: "Automatically adjust theme colors based on wallpaper" value: Settings.data.wallpaper.generateTheme - onToggled: function (newValue) { Settings.data.wallpaper.generateTheme = newValue } + onToggled: function (newValue) { + Settings.data.wallpaper.generateTheme = newValue + } } - NText { text: "Wallpaper Interval"; color: Colors.textPrimary; font.weight: Style.fontWeightBold } - NText { text: "How often to change wallpapers automatically (in seconds)"; color: Colors.textSecondary } + NText { + text: "Wallpaper Interval" + color: Colors.textPrimary + font.weight: Style.fontWeightBold + } + NText { + text: "How often to change wallpapers automatically (in seconds)" + color: Colors.textSecondary + } RowLayout { Layout.fillWidth: true - NText { text: Settings.data.wallpaper.randomInterval + " seconds"; color: Colors.textPrimary } - Item { Layout.fillWidth: true } + NText { + text: Settings.data.wallpaper.randomInterval + " seconds" + color: Colors.textPrimary + } + Item { + Layout.fillWidth: true + } } NSlider { Layout.fillWidth: true - from: 10; to: 900; stepSize: 10 + from: 10 + to: 900 + stepSize: 10 value: Settings.data.wallpaper.randomInterval onMoved: Settings.data.wallpaper.randomInterval = Math.round(value) cutoutColor: Colors.backgroundPrimary } - NDivider { Layout.fillWidth: true } + NDivider { + Layout.fillWidth: true + } - NText { text: "SWWW"; font.weight: Style.fontWeightBold; color: Colors.accentSecondary } + NText { + text: "SWWW" + font.weight: Style.fontWeightBold + color: Colors.accentSecondary + } NToggle { label: "Use SWWW" description: "Use SWWW daemon for advanced wallpaper management" value: Settings.data.wallpaper.swww.enabled - onToggled: function (newValue) { Settings.data.wallpaper.swww.enabled = newValue } + onToggled: function (newValue) { + Settings.data.wallpaper.swww.enabled = newValue + } } // SWWW settings @@ -74,45 +119,94 @@ Item { spacing: Style.marginSmall * scaling visible: Settings.data.wallpaper.swww.enabled - NText { text: "Resize Mode"; color: Colors.textPrimary; font.weight: Style.fontWeightBold } - NText { text: "How SWWW should resize wallpapers to fit the screen"; color: Colors.textSecondary; wrapMode: Text.WordWrap } + NText { + text: "Resize Mode" + font.weight: Style.fontWeightBold + } + NText { + text: "How SWWW should resize wallpapers to fit the screen" + color: Colors.textSecondary + wrapMode: Text.WordWrap + } NComboBox { optionsKeys: ["no", "crop", "fit", "stretch"] optionsLabels: ["No", "Crop", "Fit", "Stretch"] currentKey: Settings.data.wallpaper.swww.resizeMethod - onSelected: function (key) { Settings.data.wallpaper.swww.resizeMethod = key } + onSelected: function (key) { + Settings.data.wallpaper.swww.resizeMethod = key + } } - NText { text: "Transition Type"; color: Colors.textPrimary; font.weight: Style.fontWeightBold } - NText { text: "Animation type when switching between wallpapers"; color: Colors.textSecondary; wrapMode: Text.WordWrap } + NText { + text: "Transition Type" + font.weight: Style.fontWeightBold + } + NText { + text: "Animation type when switching between wallpapers" + color: Colors.textSecondary + wrapMode: Text.WordWrap + } NComboBox { optionsKeys: ["none", "simple", "fade", "left", "right", "top", "bottom", "wipe", "wave", "grow", "center", "any", "outer", "random"] optionsLabels: ["None", "Simple", "Fade", "Left", "Right", "Top", "Bottom", "Wipe", "Wave", "Grow", "Center", "Any", "Outer", "Random"] currentKey: Settings.data.wallpaper.swww.transitionType - onSelected: function (key) { Settings.data.wallpaper.swww.transitionType = key } + onSelected: function (key) { + Settings.data.wallpaper.swww.transitionType = key + } } - NText { text: "Transition FPS"; color: Colors.textPrimary; font.weight: Style.fontWeightBold } - RowLayout { Layout.fillWidth: true; NText { text: Settings.data.wallpaper.swww.transitionFps + " FPS"; color: Colors.textPrimary }; Item { Layout.fillWidth: true } } + NText { + text: "Transition FPS" + font.weight: Style.fontWeightBold + } + RowLayout { + Layout.fillWidth: true + NText { + text: Settings.data.wallpaper.swww.transitionFps + " FPS" + color: Colors.textPrimary + } + Item { + Layout.fillWidth: true + } + } NSlider { Layout.fillWidth: true - from: 30; to: 500; stepSize: 5 + from: 30 + to: 500 + stepSize: 5 value: Settings.data.wallpaper.swww.transitionFps onMoved: Settings.data.wallpaper.swww.transitionFps = Math.round(value) cutoutColor: Colors.backgroundPrimary } - NText { text: "Transition Duration"; color: Colors.textPrimary; font.weight: Style.fontWeightBold } - RowLayout { Layout.fillWidth: true; NText { text: Settings.data.wallpaper.swww.transitionDuration.toFixed(2) + " s"; color: Colors.textPrimary }; Item { Layout.fillWidth: true } } + NText { + text: "Transition Duration" + color: Colors.textPrimary + font.weight: Style.fontWeightBold + } + RowLayout { + Layout.fillWidth: true + NText { + text: Settings.data.wallpaper.swww.transitionDuration.toFixed(2) + " s" + color: Colors.textPrimary + } + Item { + Layout.fillWidth: true + } + } NSlider { Layout.fillWidth: true - from: 0.25; to: 10; stepSize: 0.05 + from: 0.25 + to: 10 + stepSize: 0.05 value: Settings.data.wallpaper.swww.transitionDuration onMoved: Settings.data.wallpaper.swww.transitionDuration = value cutoutColor: Colors.backgroundPrimary } } - Item { Layout.fillHeight: true } + Item { + Layout.fillHeight: true + } } } From 197bd586617d3fac24b8fe0f0ce3baa322939b05 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 23:03:01 -0400 Subject: [PATCH 151/394] Settings: minor adjustments --- Modules/Settings/SettingsWindow.qml | 27 +++++++++++++++------------ Modules/Settings/Tabs/General.qml | 1 + Modules/Settings/Tabs/TimeWeather.qml | 3 ++- Services/Style.qml | 1 + 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/Modules/Settings/SettingsWindow.qml b/Modules/Settings/SettingsWindow.qml index 405f7b5..d9dac3d 100644 --- a/Modules/Settings/SettingsWindow.qml +++ b/Modules/Settings/SettingsWindow.qml @@ -19,40 +19,40 @@ NLoader { readonly property real scaling: Scaling.scale(screen) property int currentTabIndex: 0 property var tabsModel: [{ - "icon": "tune", "label": "General", + "icon": "tune", "source": "Tabs/General.qml" }, { - "icon": "web_asset", "label": "Bar", + "icon": "web_asset", "source": "Tabs/Bar.qml" }, { - "icon": "schedule", "label": "Time & Weather", + "icon": "schedule", "source": "Tabs/TimeWeather.qml" }, { - "icon": "videocam", "label": "Screen Recorder", + "icon": "videocam", "source": "Tabs/ScreenRecorder.qml" }, { - "icon": "wifi", "label": "Network", + "icon": "wifi", "source": "Tabs/Network.qml" }, { - "icon": "monitor", "label": "Display", + "icon": "monitor", "source": "Tabs/Display.qml" }, { - "icon": "image", "label": "Wallpaper", + "icon": "image", "source": "Tabs/Wallpaper.qml" }, { - "icon": "more_horiz", "label": "Misc", + "icon": "more_horiz", "source": "Tabs/Misc.qml" }, { - "icon": "info", "label": "About", + "icon": "info", "source": "Tabs/About.qml" }] @@ -96,7 +96,7 @@ NLoader { Column { anchors.fill: parent anchors.margins: Style.marginSmall * scaling - spacing: 2 * scaling // Minimal spacing between tabs + spacing: Style.spacingTiny * 1.5 * scaling // Minimal spacing between tabs Repeater { id: sections @@ -119,18 +119,20 @@ NLoader { anchors.fill: parent anchors.leftMargin: Style.marginSmall * scaling anchors.rightMargin: Style.marginSmall * scaling - spacing: Style.marginTiny * scaling + spacing: Style.marginSmall * scaling NText { text: modelData.icon font.family: "Material Symbols Outlined" font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 } + font.pointSize: Style.fontSizeLarge * scaling color: selected ? Colors.onAccent : (tabItem.hovering ? Colors.onAccent : Colors.textSecondary) } NText { text: modelData.label color: selected ? Colors.onAccent : (tabItem.hovering ? Colors.onAccent : Colors.textPrimary) + font.pointSize: Style.fontSizeMediumLarge * scaling font.weight: Style.fontWeightBold Layout.fillWidth: true } @@ -172,8 +174,9 @@ NLoader { spacing: Style.marginSmall * scaling NText { text: settingsPanel.tabsModel[settingsPanel.currentTabIndex].label + font.pointSize: Style.fontSizeLarge * scaling font.weight: Style.fontWeightBold - color: Colors.textPrimary + color: Colors.accentPrimary Layout.fillWidth: true } NIconButton { diff --git a/Modules/Settings/Tabs/General.qml b/Modules/Settings/Tabs/General.qml index 8c7c35b..3d24a71 100644 --- a/Modules/Settings/Tabs/General.qml +++ b/Modules/Settings/Tabs/General.qml @@ -62,6 +62,7 @@ Item { NText { text: "Your profile picture displayed in various places throughout the shell" color: Colors.textSecondary + font.pointSize: Style.fontSizeSmall * scaling } NTextBox { text: Settings.data.general.avatarImage diff --git a/Modules/Settings/Tabs/TimeWeather.qml b/Modules/Settings/Tabs/TimeWeather.qml index b282157..bffb4a3 100644 --- a/Modules/Settings/Tabs/TimeWeather.qml +++ b/Modules/Settings/Tabs/TimeWeather.qml @@ -49,13 +49,14 @@ Item { } NText { - text: "City" + text: "Location" color: Colors.textPrimary font.weight: Style.fontWeightBold } NText { text: "Your city name for weather information" color: Colors.textSecondary + font.pointSize: Style.fontSizeSmall * scaling } NTextBox { text: Settings.data.location.name diff --git a/Services/Style.qml b/Services/Style.qml index 0b21d1d..4b9daf2 100644 --- a/Services/Style.qml +++ b/Services/Style.qml @@ -16,6 +16,7 @@ Singleton { property real fontSizeTiny: 7 property real fontSizeSmall: 9 property real fontSizeMedium: 11 + property real fontSizeMediumLarge: 12 property real fontSizeLarge: 13 property real fontSizeXL: 18 property real fontSizeXXL: 24 From 1e0057dcaf5df85e5f3b81576a0bb5be4447ac9c Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 23:08:24 -0400 Subject: [PATCH 152/394] Use Style.spacingXXXl for spacing AND margins (removed Style.marginXXX) --- Modules/Bar/TrayMenu.qml | 14 +++++++------- Modules/Bar/WiFiMenu.qml | 6 +++--- Modules/Settings/SettingsWindow.qml | 4 ++-- Modules/SidePanel/SidePanel.qml | 2 +- Modules/SidePanel/WeatherCard.qml | 2 +- Services/Style.qml | 19 ++++++------------- Widgets/NComboBox.qml | 12 ++++++------ 7 files changed, 26 insertions(+), 33 deletions(-) diff --git a/Modules/Bar/TrayMenu.qml b/Modules/Bar/TrayMenu.qml index 2d04dcb..6770a45 100644 --- a/Modules/Bar/TrayMenu.qml +++ b/Modules/Bar/TrayMenu.qml @@ -15,7 +15,7 @@ PopupWindow { property real anchorY implicitWidth: 180 * scaling - implicitHeight: Math.max(40 * scaling, listView.contentHeight + (Style.spacingMedium * 2 * scaling)) + implicitHeight: Math.max(40 * scaling, listView.contentHeight + (Style.marginMedium * 2 * scaling)) visible: false color: "transparent" @@ -83,7 +83,7 @@ PopupWindow { ListView { id: listView anchors.fill: parent - anchors.margins: Style.spacingMedium * scaling + anchors.margins: Style.marginMedium * scaling spacing: 0 interactive: false enabled: trayMenu.visible @@ -318,8 +318,8 @@ PopupWindow { ListView { id: listView anchors.fill: parent - anchors.margins: Style.spacingSmall * scaling - spacing: Style.spacingTiny * scaling + anchors.margins: Style.marginSmall * scaling + spacing: Style.marginTiny * scaling interactive: false enabled: subMenu.visible clip: true @@ -354,9 +354,9 @@ PopupWindow { RowLayout { anchors.fill: parent - anchors.leftMargin: Style.spacingMedium * scaling - anchors.rightMargin: Style.spacingMedium * scaling - spacing: Style.spacingSmall * scaling + anchors.leftMargin: Style.marginMedium * scaling + anchors.rightMargin: Style.marginMedium * scaling + spacing: Style.marginSmall * scaling NText { id: subText diff --git a/Modules/Bar/WiFiMenu.qml b/Modules/Bar/WiFiMenu.qml index 34e463a..370d16a 100644 --- a/Modules/Bar/WiFiMenu.qml +++ b/Modules/Bar/WiFiMenu.qml @@ -237,8 +237,8 @@ NLoader { RowLayout { anchors.fill: parent - anchors.margins: Style.spacingSmall * scaling - spacing: Style.spacingSmall * scaling + anchors.margins: Style.marginSmall * scaling + spacing: Style.marginSmall * scaling Item { Layout.fillWidth: true @@ -254,7 +254,7 @@ NLoader { TextInput { id: passwordInputField anchors.fill: parent - anchors.margins: Style.spacingMedium * scaling + anchors.margins: Style.marginMedium * scaling text: passwordInput font.pointSize: Style.fontSizeMedium * scaling color: Colors.textPrimary diff --git a/Modules/Settings/SettingsWindow.qml b/Modules/Settings/SettingsWindow.qml index d9dac3d..480eba9 100644 --- a/Modules/Settings/SettingsWindow.qml +++ b/Modules/Settings/SettingsWindow.qml @@ -96,7 +96,7 @@ NLoader { Column { anchors.fill: parent anchors.margins: Style.marginSmall * scaling - spacing: Style.spacingTiny * 1.5 * scaling // Minimal spacing between tabs + spacing: Style.marginTiny * 1.5 * scaling // Minimal spacing between tabs Repeater { id: sections @@ -132,7 +132,7 @@ NLoader { NText { text: modelData.label color: selected ? Colors.onAccent : (tabItem.hovering ? Colors.onAccent : Colors.textPrimary) - font.pointSize: Style.fontSizeMediumLarge * scaling + font.pointSize: Style.fontSizeInter * scaling font.weight: Style.fontWeightBold Layout.fillWidth: true } diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml index 9e155b6..10d911f 100644 --- a/Modules/SidePanel/SidePanel.qml +++ b/Modules/SidePanel/SidePanel.qml @@ -35,7 +35,7 @@ NLoader { readonly property real scaling: Scaling.scale(screen) // Single source of truth for spacing between cards (both axes) - property real cardSpacing: Style.spacingLarge * scaling + property real cardSpacing: Style.marginLarge * scaling // X coordinate from the bar to align this panel under property real anchorX: root.anchorX // Ensure this panel attaches to the intended screen diff --git a/Modules/SidePanel/WeatherCard.qml b/Modules/SidePanel/WeatherCard.qml index 62f5120..a3041d2 100644 --- a/Modules/SidePanel/WeatherCard.qml +++ b/Modules/SidePanel/WeatherCard.qml @@ -79,7 +79,7 @@ NBox { model: weatherReady ? Location.data.weather.daily.time : [] delegate: ColumnLayout { Layout.alignment: Qt.AlignHCenter - spacing: Style.spacingSmall * scaling + spacing: Style.marginSmall * scaling NText { text: Qt.formatDateTime(new Date(Location.data.weather.daily.time[index]), "ddd") color: Colors.textPrimary diff --git a/Services/Style.qml b/Services/Style.qml index 4b9daf2..85d07f4 100644 --- a/Services/Style.qml +++ b/Services/Style.qml @@ -16,7 +16,7 @@ Singleton { property real fontSizeTiny: 7 property real fontSizeSmall: 9 property real fontSizeMedium: 11 - property real fontSizeMediumLarge: 12 + property real fontSizeInter: 12 property real fontSizeLarge: 13 property real fontSizeXL: 18 property real fontSizeXXL: 24 @@ -27,23 +27,16 @@ Singleton { property int fontWeightBold: 700 // Radii - property int radiusLarge: 20 - property int radiusMedium: 16 - property int radiusSmall: 12 property int radiusTiny: 8 + property int radiusSmall: 12 + property int radiusMedium: 16 + property int radiusLarge: 20 // Border property int borderThin: 1 property int borderMedium: 2 property int borderThick: 3 - // Spacing - property int spacingExtraLarge: 20 - property int spacingLarge: 16 - property int spacingMedium: 12 - property int spacingSmall: 8 - property int spacingTiny: 4 - // Animation duration (ms) property int animationFast: 150 property int animationNormal: 300 @@ -59,12 +52,12 @@ Singleton { property int tooltipDelayLong: 1200 property int pillDelay: 500 - // Margins and spacing + // Margins (for margins and spacing) property int marginTiny: 4 property int marginSmall: 8 property int marginMedium: 12 property int marginLarge: 16 - property int marginXL: 20 + property int marginExtraLarge: 20 // Opacity property real opacityNone: 0.0 diff --git a/Widgets/NComboBox.qml b/Widgets/NComboBox.qml index b557efd..0eba662 100644 --- a/Widgets/NComboBox.qml +++ b/Widgets/NComboBox.qml @@ -36,8 +36,8 @@ ComboBox { // Label (currently selected) contentItem: NText { - leftPadding: Style.spacingLarge * scaling - rightPadding: root.indicator.width + Style.spacingLarge * scaling + leftPadding: Style.marginLarge * scaling + rightPadding: root.indicator.width + Style.marginLarge * scaling font.pointSize: Style.fontSizeMedium * scaling font.weight: Style.fontWeightBold verticalAlignment: Text.AlignVCenter @@ -49,7 +49,7 @@ ComboBox { // Drop down indicator indicator: NText { - x: root.width - width - Style.spacingMedium * scaling + x: root.width - width - Style.marginMedium * scaling y: root.topPadding + (root.availableHeight - height) / 2 text: "arrow_drop_down" font.family: "Material Symbols Outlined" @@ -59,8 +59,8 @@ ComboBox { popup: Popup { y: root.height width: root.width - implicitHeight: Math.min(160 * scaling, contentItem.implicitHeight + Style.spacingMedium * scaling * 2) - padding: Style.spacingMedium * scaling + implicitHeight: Math.min(160 * scaling, contentItem.implicitHeight + Style.marginMedium * scaling * 2) + padding: Style.marginMedium * scaling contentItem: ListView { clip: true @@ -93,7 +93,7 @@ ComboBox { } background: Rectangle { - width: root.width - Style.spacingMedium * scaling * 3 + width: root.width - Style.marginMedium * scaling * 3 color: highlighted ? Colors.hover : "transparent" radius: Style.radiusSmall * scaling } From 81382b213cd494a19a6d65436acb1648f4cf2762 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Mon, 11 Aug 2025 23:48:09 -0400 Subject: [PATCH 153/394] Attempt to round image with mask --- Modules/Settings/SettingsWindow.qml | 2 +- Modules/Settings/Tabs/General.qml | 19 +++++----- Services/Style.qml | 1 + Widgets/NImageRounded.qml | 55 +++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 12 deletions(-) create mode 100644 Widgets/NImageRounded.qml diff --git a/Modules/Settings/SettingsWindow.qml b/Modules/Settings/SettingsWindow.qml index 480eba9..2a2b01d 100644 --- a/Modules/Settings/SettingsWindow.qml +++ b/Modules/Settings/SettingsWindow.qml @@ -176,7 +176,7 @@ NLoader { text: settingsPanel.tabsModel[settingsPanel.currentTabIndex].label font.pointSize: Style.fontSizeLarge * scaling font.weight: Style.fontWeightBold - color: Colors.accentPrimary + color: Colors.textPrimary Layout.fillWidth: true } NIconButton { diff --git a/Modules/Settings/Tabs/General.qml b/Modules/Settings/Tabs/General.qml index 3d24a71..5a014b7 100644 --- a/Modules/Settings/Tabs/General.qml +++ b/Modules/Settings/Tabs/General.qml @@ -36,21 +36,18 @@ Item { // Avatar preview Rectangle { - width: 40 * scaling - height: 40 * scaling - radius: 20 * scaling - color: Colors.surfaceVariant + width: 64 * scaling + height: 64 * scaling + radius: width * 0.5 + color: Colors.accentPrimary border.color: Colors.outline border.width: Math.max(1, Style.borderThin * scaling) - Image { - anchors.fill: parent - anchors.margins: 2 * scaling - source: Settings.data.general.avatarImage - fillMode: Image.PreserveAspectCrop - asynchronous: true + + NImageRounded { + imagePath: Settings.data.general.avatarImage + fallbackIcon: "person" } } - ColumnLayout { Layout.fillWidth: true spacing: 2 * scaling diff --git a/Services/Style.qml b/Services/Style.qml index 85d07f4..7a3f6fc 100644 --- a/Services/Style.qml +++ b/Services/Style.qml @@ -53,6 +53,7 @@ Singleton { property int pillDelay: 500 // Margins (for margins and spacing) + property int marginTiniest: 2 property int marginTiny: 4 property int marginSmall: 8 property int marginMedium: 12 diff --git a/Widgets/NImageRounded.qml b/Widgets/NImageRounded.qml new file mode 100644 index 0000000..9133d3d --- /dev/null +++ b/Widgets/NImageRounded.qml @@ -0,0 +1,55 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import Quickshell.Widgets +import qs.Services + +Rectangle { + + readonly property real scaling: Scaling.scale(screen) + property string imagePath: "" + property string fallbackIcon: "" + + anchors.fill: parent + anchors.margins: Style.marginTiniest * scaling + + Image { + id: img + anchors.fill: parent + source: imagePath + visible: false + mipmap: true + smooth: true + asynchronous: true + fillMode: Image.PreserveAspectCrop + } + + MultiEffect { + anchors.fill: parent + source: img + maskEnabled: true + maskSource: mask + visible: imagePath !== "" + } + + Item { + id: mask + anchors.fill: parent + layer.enabled: true + visible: false + Rectangle { + anchors.fill: parent + radius: img.width * 0.5 + } + } + + // Fallback icon + NText { + anchors.centerIn: parent + text: fallbackIcon + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXL * scaling + visible: fallbackIcon !== undefined && fallbackIcon !== "" && (source === undefined || source === "") + z: 0 + } +} From 54b2673f9a3f32c0043102d4666a17b2ae274060 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Tue, 12 Aug 2025 12:47:20 +0200 Subject: [PATCH 154/394] Make avatar images rounded --- Modules/Settings/Tabs/General.qml | 15 +++++---------- Modules/SidePanel/ProfileCard.qml | 27 +++++---------------------- Widgets/NImageRounded.qml | 15 ++++++++++++++- 3 files changed, 24 insertions(+), 33 deletions(-) diff --git a/Modules/Settings/Tabs/General.qml b/Modules/Settings/Tabs/General.qml index 5a014b7..36484aa 100644 --- a/Modules/Settings/Tabs/General.qml +++ b/Modules/Settings/Tabs/General.qml @@ -35,18 +35,13 @@ Item { spacing: Style.marginMedium * scaling // Avatar preview - Rectangle { + NImageRounded { width: 64 * scaling height: 64 * scaling - radius: width * 0.5 - color: Colors.accentPrimary - border.color: Colors.outline - border.width: Math.max(1, Style.borderThin * scaling) - - NImageRounded { - imagePath: Settings.data.general.avatarImage - fallbackIcon: "person" - } + imagePath: Settings.data.general.avatarImage + fallbackIcon: "person" + borderColor: Colors.accentPrimary + borderWidth: Math.max(1, Style.borderMedium * scaling) } ColumnLayout { Layout.fillWidth: true diff --git a/Modules/SidePanel/ProfileCard.qml b/Modules/SidePanel/ProfileCard.qml index b715b7d..242a5a7 100644 --- a/Modules/SidePanel/ProfileCard.qml +++ b/Modules/SidePanel/ProfileCard.qml @@ -25,30 +25,13 @@ NBox { anchors.margins: Style.marginMedium * scaling spacing: Style.marginMedium * scaling - Item { - id: avatarBox + NImageRounded { width: Style.baseWidgetSize * 1.25 * scaling height: Style.baseWidgetSize * 1.25 * scaling - - Image { - id: avatarImage - anchors.fill: parent - source: Settings.data.general.avatarImage - fillMode: Image.PreserveAspectCrop - asynchronous: true - } - - // Ensure rounded corners consistently across renderers - MultiEffect { - anchors.fill: avatarImage - source: avatarImage - maskEnabled: true - maskSource: Rectangle { - anchors.fill: parent - color: "white" - radius: Style.radiusMedium * scaling - } - } + imagePath: Settings.data.general.avatarImage + fallbackIcon: "person" + borderColor: Colors.accentPrimary + borderWidth: Math.max(1, Style.borderMedium * scaling) } ColumnLayout { diff --git a/Widgets/NImageRounded.qml b/Widgets/NImageRounded.qml index 9133d3d..abef690 100644 --- a/Widgets/NImageRounded.qml +++ b/Widgets/NImageRounded.qml @@ -5,14 +5,27 @@ import Quickshell.Widgets import qs.Services Rectangle { + color: "transparent" + radius: width * 0.5 readonly property real scaling: Scaling.scale(screen) property string imagePath: "" property string fallbackIcon: "" + property color borderColor: "transparent" + property real borderWidth: 0 - anchors.fill: parent anchors.margins: Style.marginTiniest * scaling + // Border + Rectangle { + anchors.fill: parent + radius: parent.radius + color: "transparent" + border.color: parent.borderColor + border.width: parent.borderWidth + z: 10 + } + Image { id: img anchors.fill: parent From c616f5a46caa662e2bcd1696ca54a0f049aea194 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Tue, 12 Aug 2025 14:03:11 +0200 Subject: [PATCH 155/394] Finish SettingsWindow, still need Wallpaper Selector --- Modules/Settings/SettingsWindow.qml | 23 +- Modules/Settings/Tabs/About.qml | 661 ++++++++++++++--------- Modules/Settings/Tabs/Bar.qml | 3 +- Modules/Settings/Tabs/Display.qml | 3 +- Modules/Settings/Tabs/General.qml | 5 +- Modules/Settings/Tabs/Misc.qml | 3 +- Modules/Settings/Tabs/Network.qml | 3 +- Modules/Settings/Tabs/ScreenRecorder.qml | 3 +- Modules/Settings/Tabs/TimeWeather.qml | 43 +- Modules/Settings/Tabs/Wallpaper.qml | 3 +- Services/Settings.qml | 6 +- Services/Wallpapers.qml | 13 +- 12 files changed, 466 insertions(+), 303 deletions(-) diff --git a/Modules/Settings/SettingsWindow.qml b/Modules/Settings/SettingsWindow.qml index 2a2b01d..196e4ed 100644 --- a/Modules/Settings/SettingsWindow.qml +++ b/Modules/Settings/SettingsWindow.qml @@ -70,8 +70,8 @@ NLoader { border.color: Colors.backgroundTertiary border.width: Math.max(1, Style.borderMedium * scaling) layer.enabled: true - width: 1040 * scaling - height: 640 * scaling + width: (screen.width/2) * scaling + height: (screen.height/2) * scaling anchors.centerIn: parent MouseArea { @@ -151,7 +151,7 @@ NLoader { } } - // Content (unchanged) + // Content Rectangle { id: contentPane Layout.fillWidth: true @@ -200,14 +200,15 @@ NLoader { Layout.fillHeight: true currentIndex: settingsPanel.currentTabIndex - Repeater { - model: settingsPanel.tabsModel - delegate: Loader { - active: index === settingsPanel.currentTabIndex - visible: active - source: modelData.source - } - } + Tabs.General {} + Tabs.Bar {} + Tabs.TimeWeather {} + Tabs.ScreenRecorder {} + Tabs.Network {} + Tabs.Display {} + Tabs.Wallpaper {} + Tabs.Misc {} + Tabs.About {} } } } diff --git a/Modules/Settings/Tabs/About.qml b/Modules/Settings/Tabs/About.qml index 3f8abe3..53aa2df 100644 --- a/Modules/Settings/Tabs/About.qml +++ b/Modules/Settings/Tabs/About.qml @@ -1,280 +1,443 @@ import QtQuick import QtQuick.Controls -import QtQuick.Layouts import QtQuick.Effects +import QtQuick.Layouts import Quickshell import Quickshell.Io import qs.Services import qs.Widgets -Item { - id: root - property real scaling: 1 - readonly property string tabIcon: "info" - readonly property string tabLabel: "About" - readonly property int tabIndex: 8 - anchors.fill: parent +ColumnLayout { + id: root - property string latestVersion: "Unknown" - property string currentVersion: "Unknown" - property var contributors: [] - property string githubDataPath: Settings.configDir + "github_data.json" + property string latestVersion: "Unknown" + property string currentVersion: "v1.2.1" // Fallback version + property var contributors: [] + property string githubDataPath: Settings.configDir + "github_data.json" - function loadFromFile() { - const now = Date.now() - const data = githubData - if (!data.timestamp || (now - data.timestamp > 3600 * 1000)) { - // 1h cache - fetchFromGitHub() - return - } - if (data.version) - root.latestVersion = data.version - if (data.contributors) - root.contributors = data.contributors - } + function loadFromFile() { + const now = Date.now(); + const data = githubData; + if (!data.timestamp || (now - data.timestamp > 3.6e+06)) { + console.log("[About] Cache expired or missing, fetching new data from GitHub..."); + fetchFromGitHub(); + return ; + } + console.log("[About] Loading cached GitHub data (age: " + Math.round((now - data.timestamp) / 60000) + " minutes)"); + if (data.version) + root.latestVersion = data.version; - function fetchFromGitHub() { - versionProcess.running = true - contributorsProcess.running = true - } + if (data.contributors) { + root.contributors = data.contributors; + } - function saveData() { - githubData.timestamp = Date.now() - Qt.callLater(function () { - githubDataFile.writeAdapter() - }) - } - - ColumnLayout { - anchors.fill: parent - anchors.margins: Style.marginLarge * scaling - spacing: Style.marginMedium * scaling - - // Header - NText { - text: "Noctalia: quiet by design" - font.weight: Style.fontWeightBold - color: Colors.textPrimary - } - NText { - text: "It may just be another quickshell setup but it won't get in your way." - color: Colors.textSecondary } - // Versions grid - RowLayout { - spacing: Style.marginLarge * scaling - ColumnLayout { - NText { - text: "Latest Version:" - color: Colors.textSecondary + function fetchFromGitHub() { + versionProcess.running = true; + contributorsProcess.running = true; + } + + function saveData() { + githubData.timestamp = Date.now(); + Qt.callLater(() => { + githubDataFile.writeAdapter(); + }); + } + + spacing: 0 + Layout.fillWidth: true + Layout.fillHeight: true + + Process { + id: currentVersionProcess + + command: ["sh", "-c", "cd " + Quickshell.shellDir + " && git describe --tags --abbrev=0 2>/dev/null || echo 'Unknown'"] + Component.onCompleted: { + running = true; } - NText { - text: root.latestVersion - font.weight: Style.fontWeightBold - color: Colors.textPrimary + + stdout: StdioCollector { + onStreamFinished: { + const version = text.trim(); + if (version && version !== "Unknown") { + root.currentVersion = version; + } else { + currentVersionProcess.command = ["sh", "-c", "cd " + Quickshell.shellDir + " && cat package.json 2>/dev/null | grep '\"version\"' | cut -d'\"' -f4 || echo 'Unknown'"]; + currentVersionProcess.running = true; + } + } } - } - ColumnLayout { - NText { - text: "Installed Version:" - color: Colors.textSecondary + + } + + FileView { + id: githubDataFile + + path: root.githubDataPath + blockLoading: true + printErrors: true + watchChanges: true + onFileChanged: githubDataFile.reload() + onLoaded: loadFromFile() + onLoadFailed: function(error) { + console.log("GitHub data file doesn't exist yet, creating it..."); + githubData.version = "Unknown"; + githubData.contributors = []; + githubData.timestamp = 0; + githubDataFile.writeAdapter(); + fetchFromGitHub(); } - NText { - text: root.currentVersion - font.weight: Style.fontWeightBold - color: Colors.textPrimary + Component.onCompleted: { + if (path) + reload(); + } - } - Item { + + JsonAdapter { + id: githubData + + property string version: "Unknown" + property var contributors: [] + property double timestamp: 0 + } + + } + + Process { + id: versionProcess + + command: ["curl", "-s", "https://api.github.com/repos/Ly-sec/Noctalia/releases/latest"] + + stdout: StdioCollector { + onStreamFinished: { + try { + const data = JSON.parse(text); + if (data.tag_name) { + const version = data.tag_name; + githubData.version = version; + root.latestVersion = version; + console.log("[About] Latest version fetched from GitHub:", version); + } else { + console.log("No tag_name in GitHub response"); + } + saveData(); + } catch (e) { + console.error("Failed to parse version:", e); + } + } + } + + } + + Process { + id: contributorsProcess + + command: ["curl", "-s", "https://api.github.com/repos/Ly-sec/Noctalia/contributors?per_page=100"] + + stdout: StdioCollector { + onStreamFinished: { + try { + const data = JSON.parse(text); + githubData.contributors = data || []; + root.contributors = githubData.contributors; + saveData(); + } catch (e) { + console.error("Failed to parse contributors:", e); + root.contributors = []; + } + } + } + + } + + ScrollView { + id: scrollView + Layout.fillWidth: true - } - NIconButton { - icon: "system_update" - tooltipText: "Open latest release" - onClicked: Quickshell.execDetached(["xdg-open", "https://github.com/Ly-sec/Noctalia/releases/latest"]) - } - } + Layout.fillHeight: true + padding: 16 + rightPadding: 12 + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded - NDivider { - Layout.fillWidth: true - } + ColumnLayout { + width: scrollView.availableWidth + spacing: 0 - // Contributors - RowLayout { - spacing: Style.marginSmall * scaling - NText { - text: "Contributors" - font.weight: Style.fontWeightBold - color: Colors.textPrimary - } - NText { - text: "(" + root.contributors.length + ")" - color: Colors.textSecondary - } - } - - GridView { - id: contributorsGrid - Layout.fillWidth: true - Layout.fillHeight: true - cellWidth: 200 * scaling - cellHeight: 100 * scaling - model: root.contributors - delegate: Rectangle { - width: contributorsGrid.cellWidth - 8 * scaling - height: contributorsGrid.cellHeight - 4 * scaling - radius: Style.radiusLarge * scaling - color: contributorArea.containsMouse ? Colors.highlight : "transparent" - RowLayout { - anchors.fill: parent - anchors.margins: Style.marginSmall * scaling - spacing: Style.marginSmall * scaling - Item { - Layout.preferredWidth: 40 * scaling - Layout.preferredHeight: 40 * scaling - Image { - id: avatarImage - anchors.fill: parent - source: modelData.avatar_url || "" - asynchronous: true - visible: false - fillMode: Image.PreserveAspectCrop - } - MultiEffect { - anchors.fill: parent - source: avatarImage - maskEnabled: true - maskSource: mask - } - Item { - id: mask - anchors.fill: parent - visible: false - Rectangle { - anchors.fill: parent - radius: width / 2 - } - } NText { - anchors.centerIn: parent - text: "person" - font.family: "Material Symbols Outlined" - color: contributorArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary - visible: !avatarImage.source || avatarImage.status !== Image.Ready + text: "Noctalia: quiet by design" + font.pointSize: 24 * Scaling.scale(screen) + font.weight: Style.fontWeightBold + color: Colors.textPrimary + Layout.alignment: Qt.AlignCenter + Layout.bottomMargin: 8 * Scaling.scale(screen) } - } - ColumnLayout { - Layout.fillWidth: true - spacing: 2 * scaling + NText { - text: modelData.login || "Unknown" - color: contributorArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary + text: "It may just be another quickshell setup but it won't get in your way." + font.pointSize: 14 * Scaling.scale(screen) + color: Colors.textSecondary + Layout.alignment: Qt.AlignCenter + Layout.bottomMargin: 16 * Scaling.scale(screen) } + + GridLayout { + Layout.alignment: Qt.AlignCenter + columns: 2 + rowSpacing: 4 + columnSpacing: 8 + + NText { + text: "Latest Version:" + font.pointSize: 16 * Scaling.scale(screen) + color: Colors.textSecondary + Layout.alignment: Qt.AlignRight + } + + NText { + text: root.latestVersion + font.pointSize: 16 * Scaling.scale(screen) + color: Colors.textPrimary + font.weight: Style.fontWeightBold + } + + NText { + text: "Installed Version:" + font.pointSize: 16 * Scaling.scale(screen) + color: Colors.textSecondary + Layout.alignment: Qt.AlignRight + } + + NText { + text: root.currentVersion + font.pointSize: 16 * Scaling.scale(screen) + color: Colors.textPrimary + font.weight: Style.fontWeightBold + } + + } + + Rectangle { + Layout.alignment: Qt.AlignCenter + Layout.topMargin: 8 + Layout.preferredWidth: updateText.implicitWidth + 46 + Layout.preferredHeight: 32 + radius: 20 + color: updateArea.containsMouse ? Colors.accentPrimary : "transparent" + border.color: Colors.accentPrimary + border.width: 1 + visible: { + if (root.currentVersion === "Unknown" || root.latestVersion === "Unknown") + return false; + + const latest = root.latestVersion.replace("v", "").split("."); + const current = root.currentVersion.replace("v", "").split("."); + for (let i = 0; i < Math.max(latest.length, current.length); i++) { + const l = parseInt(latest[i] || "0"); + const c = parseInt(current[i] || "0"); + if (l > c) + return true; + + if (l < c) + return false; + + } + return false; + } + + RowLayout { + anchors.centerIn: parent + spacing: 8 + + NText { + text: "system_update" + font.family: "Material Symbols Outlined" + font.pointSize: 18 * Scaling.scale(screen) + color: updateArea.containsMouse ? Colors.backgroundPrimary : Colors.accentPrimary + } + + NText { + id: updateText + + text: "Download latest release" + font.pointSize: 14 * Scaling.scale(screen) + color: updateArea.containsMouse ? Colors.backgroundPrimary : Colors.accentPrimary + } + + } + + MouseArea { + id: updateArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + Quickshell.execDetached(["xdg-open", "https://github.com/Ly-sec/Noctalia/releases/latest"]); + } + } + + } + + // Separator + Rectangle { + Layout.fillWidth: true + Layout.topMargin: 26 + Layout.bottomMargin: 18 + height: 1 + color: Colors.outline + opacity: 0.3 + } + NText { - text: (modelData.contributions || 0) + " commits" - color: contributorArea.containsMouse ? Colors.backgroundPrimary : Colors.textSecondary + text: "Contributors" + font.pointSize: 18 * Scaling.scale(screen) + font.weight: Style.fontWeightBold + color: Colors.textPrimary + Layout.alignment: Qt.AlignCenter + Layout.topMargin: 32 } - } + + NText { + text: "(" + root.contributors.length + ")" + font.pointSize: 14 * Scaling.scale(screen) + color: Colors.textSecondary + Layout.alignment: Qt.AlignCenter + Layout.topMargin: 4 + } + + ScrollView { + Layout.alignment: Qt.AlignCenter + Layout.preferredWidth: 200 * 4 + Layout.fillHeight: true + Layout.topMargin: 16 + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + + GridView { + id: contributorsGrid + + anchors.fill: parent + width: 200 * 4 + height: Math.ceil(root.contributors.length / 4) * 100 + cellWidth: 200 + cellHeight: 100 + model: root.contributors + + delegate: Rectangle { + width: contributorsGrid.cellWidth - 16 + height: contributorsGrid.cellHeight - 4 + radius: 20 + color: contributorArea.containsMouse ? Colors.hover : "transparent" + + RowLayout { + anchors.fill: parent + anchors.margins: 8 + spacing: 12 + + Item { + Layout.alignment: Qt.AlignVCenter + Layout.preferredWidth: 40 + Layout.preferredHeight: 40 + + Image { + id: avatarImage + + anchors.fill: parent + source: modelData.avatar_url || "" + sourceSize: Qt.size(80, 80) + visible: false + mipmap: true + smooth: true + asynchronous: true + fillMode: Image.PreserveAspectCrop + cache: true + + onStatusChanged: { + if (status === Image.Error) { + console.log("[About] Failed to load avatar for", modelData.login, "URL:", modelData.avatar_url); + } + } + } + + MultiEffect { + anchors.fill: parent + source: avatarImage + maskEnabled: true + maskSource: mask + } + + Item { + id: mask + + anchors.fill: parent + layer.enabled: true + visible: false + + Rectangle { + anchors.fill: parent + radius: parent.width / 2 + } + } + + NText { + anchors.centerIn: parent + text: "person" + font.family: "Material Symbols Outlined" + font.pointSize: 24 * Scaling.scale(screen) + color: contributorArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary + visible: !avatarImage.source || avatarImage.status !== Image.Ready + } + + } + + ColumnLayout { + spacing: 4 + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true + + NText { + text: modelData.login || "Unknown" + font.pointSize: 13 * Scaling.scale(screen) + color: contributorArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary + elide: Text.ElideRight + Layout.fillWidth: true + } + + NText { + text: (modelData.contributions || 0) + " commits" + font.pointSize: 11 * Scaling.scale(screen) + color: contributorArea.containsMouse ? Colors.backgroundPrimary : Colors.textSecondary + } + + } + + } + + MouseArea { + id: contributorArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (modelData.html_url) + Quickshell.execDetached(["xdg-open", modelData.html_url]); + + } + } + + } + + } + + } + } - MouseArea { - id: contributorArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: if (modelData.html_url) - Quickshell.execDetached(["xdg-open", modelData.html_url]) - } - } + } - Item { - Layout.fillHeight: true - } - } - - // Processes and persistence - Process { - id: currentVersionProcess - command: ["sh", "-c", "cd " + Quickshell.shellDir + " && git describe --tags --abbrev=0 2>/dev/null || echo 'Unknown'"] - Component.onCompleted: running = true - stdout: StdioCollector { - onStreamFinished: { - const version = text.trim() - if (version && version !== "Unknown") { - root.currentVersion = version - } else { - currentVersionProcess.command = ["sh", "-c", "cd " + Quickshell.shellDir - + " && cat package.json 2>/dev/null | grep '\"version\"' | cut -d'\"' -f4 || echo 'Unknown'"] - currentVersionProcess.running = true - } - } - } - } - - FileView { - id: githubDataFile - path: root.githubDataPath - blockLoading: true - printErrors: true - watchChanges: true - onFileChanged: githubDataFile.reload() - onLoaded: loadFromFile() - onLoadFailed: { - githubData.version = "Unknown" - githubData.contributors = [] - githubData.timestamp = 0 - githubDataFile.writeAdapter() - fetchFromGitHub() - } - Component.onCompleted: { - if (path) - reload() - } - JsonAdapter { - id: githubData - property string version: "Unknown" - property var contributors: [] - property double timestamp: 0 - } - } - - Process { - id: versionProcess - command: ["curl", "-s", "https://api.github.com/repos/Ly-sec/Noctalia/releases/latest"] - stdout: StdioCollector { - onStreamFinished: { - try { - const data = JSON.parse(text) - if (data.tag_name) { - const version = data.tag_name - githubData.version = version - root.latestVersion = version - } - saveData() - } catch (e) { - console.error("Failed to parse version:", e) - } - } - } - } - - Process { - id: contributorsProcess - command: ["curl", "-s", "https://api.github.com/repos/Ly-sec/Noctalia/contributors?per_page=100"] - stdout: StdioCollector { - onStreamFinished: { - try { - const data = JSON.parse(text) - githubData.contributors = data || [] - root.contributors = githubData.contributors - saveData() - } catch (e) { - console.error("Failed to parse contributors:", e) - root.contributors = [] - } - } - } - } } diff --git a/Modules/Settings/Tabs/Bar.qml b/Modules/Settings/Tabs/Bar.qml index 6a6ce6f..a44d0fd 100644 --- a/Modules/Settings/Tabs/Bar.qml +++ b/Modules/Settings/Tabs/Bar.qml @@ -10,7 +10,8 @@ Item { readonly property string tabIcon: "web_asset" readonly property string tabLabel: "Bar" readonly property int tabIndex: 1 - anchors.fill: parent + Layout.fillWidth: true + Layout.fillHeight: true ColumnLayout { anchors.fill: parent diff --git a/Modules/Settings/Tabs/Display.qml b/Modules/Settings/Tabs/Display.qml index f7295bc..ea587d7 100644 --- a/Modules/Settings/Tabs/Display.qml +++ b/Modules/Settings/Tabs/Display.qml @@ -9,7 +9,8 @@ Item { readonly property string tabIcon: "monitor" readonly property string tabLabel: "Display" readonly property int tabIndex: 5 - anchors.fill: parent + Layout.fillWidth: true + Layout.fillHeight: true // Helper functions to update arrays immutably function addMonitor(list, name) { diff --git a/Modules/Settings/Tabs/General.qml b/Modules/Settings/Tabs/General.qml index 36484aa..1e72603 100644 --- a/Modules/Settings/Tabs/General.qml +++ b/Modules/Settings/Tabs/General.qml @@ -14,9 +14,8 @@ Item { readonly property string tabLabel: "General" readonly property int tabIndex: 0 - anchors.fill: parent - implicitWidth: parent ? parent.width : 0 - implicitHeight: parent ? parent.height : 0 + Layout.fillWidth: true + Layout.fillHeight: true ColumnLayout { anchors.fill: parent diff --git a/Modules/Settings/Tabs/Misc.qml b/Modules/Settings/Tabs/Misc.qml index d64f3dc..2a6dc6a 100644 --- a/Modules/Settings/Tabs/Misc.qml +++ b/Modules/Settings/Tabs/Misc.qml @@ -8,7 +8,8 @@ Item { readonly property string tabIcon: "more_horiz" readonly property string tabLabel: "Misc" readonly property int tabIndex: 7 - anchors.fill: parent + Layout.fillWidth: true + Layout.fillHeight: true ColumnLayout { anchors.fill: parent diff --git a/Modules/Settings/Tabs/Network.qml b/Modules/Settings/Tabs/Network.qml index fd16ed7..ad9be23 100644 --- a/Modules/Settings/Tabs/Network.qml +++ b/Modules/Settings/Tabs/Network.qml @@ -10,7 +10,8 @@ Item { readonly property string tabIcon: "wifi" readonly property string tabLabel: "Network" readonly property int tabIndex: 4 - anchors.fill: parent + Layout.fillWidth: true + Layout.fillHeight: true ColumnLayout { anchors.fill: parent diff --git a/Modules/Settings/Tabs/ScreenRecorder.qml b/Modules/Settings/Tabs/ScreenRecorder.qml index b414ea2..045acf3 100644 --- a/Modules/Settings/Tabs/ScreenRecorder.qml +++ b/Modules/Settings/Tabs/ScreenRecorder.qml @@ -8,7 +8,8 @@ Item { readonly property string tabIcon: "videocam" readonly property string tabLabel: "Screen Recorder" readonly property int tabIndex: 3 - anchors.fill: parent + Layout.fillWidth: true + Layout.fillHeight: true ColumnLayout { anchors.fill: parent diff --git a/Modules/Settings/Tabs/TimeWeather.qml b/Modules/Settings/Tabs/TimeWeather.qml index bffb4a3..fd56728 100644 --- a/Modules/Settings/Tabs/TimeWeather.qml +++ b/Modules/Settings/Tabs/TimeWeather.qml @@ -8,7 +8,8 @@ Item { readonly property string tabIcon: "schedule" readonly property string tabLabel: "Time & Weather" readonly property int tabIndex: 2 - anchors.fill: parent + Layout.fillWidth: true + Layout.fillHeight: true ColumnLayout { anchors.fill: parent @@ -64,30 +65,22 @@ Item { onEditingFinished: Settings.data.location.name = text } - RowLayout { - Layout.fillWidth: true - spacing: Style.marginSmall * scaling - ColumnLayout { - Layout.fillWidth: true - spacing: 2 * scaling - NText { - text: "Temperature Unit" - color: Colors.textPrimary - font.weight: Style.fontWeightBold - } - NText { - text: "Choose between Celsius and Fahrenheit" - color: Colors.textSecondary - wrapMode: Text.WordWrap - } - } - NComboBox { - optionsKeys: ["c", "f"] - optionsLabels: ["Celsius", "Fahrenheit"] - currentKey: Settings.data.location.useFahrenheit ? "f" : "c" - onSelected: function (key) { - Settings.data.location.useFahrenheit = (key === "f") - } + NText { + text: "Temperature Unit" + color: Colors.textPrimary + font.weight: Style.fontWeightBold + } + NText { + text: "Choose between Celsius and Fahrenheit" + color: Colors.textSecondary + font.pointSize: Style.fontSizeSmall * scaling + } + NComboBox { + optionsKeys: ["c", "f"] + optionsLabels: ["Celsius", "Fahrenheit"] + currentKey: Settings.data.location.useFahrenheit ? "f" : "c" + onSelected: function (key) { + Settings.data.location.useFahrenheit = (key === "f") } } diff --git a/Modules/Settings/Tabs/Wallpaper.qml b/Modules/Settings/Tabs/Wallpaper.qml index 923cc0f..8c6e5cc 100644 --- a/Modules/Settings/Tabs/Wallpaper.qml +++ b/Modules/Settings/Tabs/Wallpaper.qml @@ -8,7 +8,8 @@ Item { readonly property string tabIcon: "image" readonly property string tabLabel: "Wallpaper" readonly property int tabIndex: 6 - anchors.fill: parent + Layout.fillWidth: true + Layout.fillHeight: true ColumnLayout { anchors.fill: parent diff --git a/Services/Settings.qml b/Services/Settings.qml index 4f1a444..3d31e06 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -119,9 +119,9 @@ Singleton { property bool generateTheme: false property JsonObject swww - onDirectoryChanged: WallpaperManager.loadWallpapers() - onIsRandomChanged: WallpaperManager.toggleRandomWallpaper() - onRandomIntervalChanged: WallpaperManager.restartRandomWallpaperTimer() + onDirectoryChanged: Wallpapers.loadWallpapers() + onIsRandomChanged: Wallpapers.toggleRandomWallpaper() + onRandomIntervalChanged: Wallpapers.restartRandomWallpaperTimer() swww: JsonObject { property bool enabled: false diff --git a/Services/Wallpapers.qml b/Services/Wallpapers.qml index d73d5cc..bd78cf6 100644 --- a/Services/Wallpapers.qml +++ b/Services/Wallpapers.qml @@ -22,10 +22,11 @@ Singleton { property string transitionType: Settings.data.wallpaper.swww.transitionType property var randomChoices: ["fade", "left", "right", "top", "bottom", "wipe", "wave", "grow", "center", "any", "outer"] + + function loadWallpapers() { scanning = true wallpaperList = [] - folderModel.folder = "" folderModel.folder = "file://" + (Settings.data.wallpaper.directory !== undefined ? Settings.data.wallpaper.directory : "") } @@ -38,11 +39,11 @@ Singleton { if (!isInitial) { Settings.data.wallpaper.current = path } - if (Settings.data.swww.enabled) { - if (Settings.data.swww.transitionType === "random") { + if (Settings.data.wallpaper.swww.enabled) { + if (Settings.data.wallpaper.swww.transitionType === "random") { transitionType = randomChoices[Math.floor(Math.random() * randomChoices.length)] } else { - transitionType = Settings.data.swww.transitionType + transitionType = Settings.data.wallpaper.swww.transitionType } changeWallpaperProcess.running = true } @@ -105,7 +106,7 @@ Singleton { var files = [] var filesSwww = [] for (var i = 0; i < count; i++) { - var filepath = (Settings.data.wallpaper.folder !== undefined ? Settings.data.wallpaper.folder : "") + "/" + get( + var filepath = (Settings.data.wallpaper.directory !== undefined ? Settings.data.wallpaper.directory : "") + "/" + get( i, "fileName") files.push(filepath) } @@ -118,7 +119,7 @@ Singleton { Process { id: changeWallpaperProcess command: ["swww", "img", "--resize", Settings.data.wallpaper.swww.resizeMethod, "--transition-fps", Settings.data.wallpaper.swww.transitionFps.toString( - ), "--transition-type", transitionType, "--transition-duration", Settings.data.wallpaper.transitionDuration.toString( + ), "--transition-type", transitionType, "--transition-duration", Settings.data.wallpaper.swww.transitionDuration.toString( ), currentWallpaper] running: false } From 049ea7c4e69558ba5cce027dd4cf9be5c38f71b4 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 08:10:06 -0400 Subject: [PATCH 156/394] Rename NTextBox To NTextInput for better clarity --- Modules/Settings/Tabs/General.qml | 2 +- Modules/Settings/Tabs/ScreenRecorder.qml | 2 +- Modules/Settings/Tabs/TimeWeather.qml | 2 +- Modules/Settings/Tabs/Wallpaper.qml | 2 +- Widgets/{NTextBox.qml => NTextInput.qml} | 0 5 files changed, 4 insertions(+), 4 deletions(-) rename Widgets/{NTextBox.qml => NTextInput.qml} (100%) diff --git a/Modules/Settings/Tabs/General.qml b/Modules/Settings/Tabs/General.qml index 1e72603..9693f6f 100644 --- a/Modules/Settings/Tabs/General.qml +++ b/Modules/Settings/Tabs/General.qml @@ -55,7 +55,7 @@ Item { color: Colors.textSecondary font.pointSize: Style.fontSizeSmall * scaling } - NTextBox { + NTextInput { text: Settings.data.general.avatarImage placeholderText: "/home/user/.face" Layout.fillWidth: true diff --git a/Modules/Settings/Tabs/ScreenRecorder.qml b/Modules/Settings/Tabs/ScreenRecorder.qml index 045acf3..ae278be 100644 --- a/Modules/Settings/Tabs/ScreenRecorder.qml +++ b/Modules/Settings/Tabs/ScreenRecorder.qml @@ -31,7 +31,7 @@ Item { text: "Directory where screen recordings will be saved" color: Colors.textSecondary } - NTextBox { + NTextInput { text: Settings.data.screenRecorder.directory Layout.fillWidth: true onEditingFinished: Settings.data.screenRecorder.directory = text diff --git a/Modules/Settings/Tabs/TimeWeather.qml b/Modules/Settings/Tabs/TimeWeather.qml index fd56728..cf420f7 100644 --- a/Modules/Settings/Tabs/TimeWeather.qml +++ b/Modules/Settings/Tabs/TimeWeather.qml @@ -59,7 +59,7 @@ Item { color: Colors.textSecondary font.pointSize: Style.fontSizeSmall * scaling } - NTextBox { + NTextInput { text: Settings.data.location.name Layout.fillWidth: true onEditingFinished: Settings.data.location.name = text diff --git a/Modules/Settings/Tabs/Wallpaper.qml b/Modules/Settings/Tabs/Wallpaper.qml index 8c6e5cc..88259e0 100644 --- a/Modules/Settings/Tabs/Wallpaper.qml +++ b/Modules/Settings/Tabs/Wallpaper.qml @@ -32,7 +32,7 @@ Item { color: Colors.textSecondary wrapMode: Text.WordWrap } - NTextBox { + NTextInput { text: Settings.data.wallpaper.directory Layout.fillWidth: true onEditingFinished: Settings.data.wallpaper.directory = text diff --git a/Widgets/NTextBox.qml b/Widgets/NTextInput.qml similarity index 100% rename from Widgets/NTextBox.qml rename to Widgets/NTextInput.qml From 663e820d260731a0e7ffda72c6283f164597ce20 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Tue, 12 Aug 2025 14:11:52 +0200 Subject: [PATCH 157/394] Create Github Service --- Modules/Settings/Tabs/About.qml | 116 +------------------- Services/Github.qml | 182 ++++++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+), 111 deletions(-) create mode 100644 Services/Github.qml diff --git a/Modules/Settings/Tabs/About.qml b/Modules/Settings/Tabs/About.qml index 53aa2df..3c97970 100644 --- a/Modules/Settings/Tabs/About.qml +++ b/Modules/Settings/Tabs/About.qml @@ -10,39 +10,13 @@ import qs.Widgets ColumnLayout { id: root - property string latestVersion: "Unknown" + property string latestVersion: Github.latestVersion property string currentVersion: "v1.2.1" // Fallback version - property var contributors: [] - property string githubDataPath: Settings.configDir + "github_data.json" + property var contributors: Github.contributors - function loadFromFile() { - const now = Date.now(); - const data = githubData; - if (!data.timestamp || (now - data.timestamp > 3.6e+06)) { - console.log("[About] Cache expired or missing, fetching new data from GitHub..."); - fetchFromGitHub(); - return ; - } - console.log("[About] Loading cached GitHub data (age: " + Math.round((now - data.timestamp) / 60000) + " minutes)"); - if (data.version) - root.latestVersion = data.version; - - if (data.contributors) { - root.contributors = data.contributors; - } - - } - - function fetchFromGitHub() { - versionProcess.running = true; - contributorsProcess.running = true; - } - - function saveData() { - githubData.timestamp = Date.now(); - Qt.callLater(() => { - githubDataFile.writeAdapter(); - }); + Component.onCompleted: { + // Initialize the Github service + Github.init(); } spacing: 0 @@ -71,86 +45,6 @@ ColumnLayout { } - FileView { - id: githubDataFile - - path: root.githubDataPath - blockLoading: true - printErrors: true - watchChanges: true - onFileChanged: githubDataFile.reload() - onLoaded: loadFromFile() - onLoadFailed: function(error) { - console.log("GitHub data file doesn't exist yet, creating it..."); - githubData.version = "Unknown"; - githubData.contributors = []; - githubData.timestamp = 0; - githubDataFile.writeAdapter(); - fetchFromGitHub(); - } - Component.onCompleted: { - if (path) - reload(); - - } - - JsonAdapter { - id: githubData - - property string version: "Unknown" - property var contributors: [] - property double timestamp: 0 - } - - } - - Process { - id: versionProcess - - command: ["curl", "-s", "https://api.github.com/repos/Ly-sec/Noctalia/releases/latest"] - - stdout: StdioCollector { - onStreamFinished: { - try { - const data = JSON.parse(text); - if (data.tag_name) { - const version = data.tag_name; - githubData.version = version; - root.latestVersion = version; - console.log("[About] Latest version fetched from GitHub:", version); - } else { - console.log("No tag_name in GitHub response"); - } - saveData(); - } catch (e) { - console.error("Failed to parse version:", e); - } - } - } - - } - - Process { - id: contributorsProcess - - command: ["curl", "-s", "https://api.github.com/repos/Ly-sec/Noctalia/contributors?per_page=100"] - - stdout: StdioCollector { - onStreamFinished: { - try { - const data = JSON.parse(text); - githubData.contributors = data || []; - root.contributors = githubData.contributors; - saveData(); - } catch (e) { - console.error("Failed to parse contributors:", e); - root.contributors = []; - } - } - } - - } - ScrollView { id: scrollView diff --git a/Services/Github.qml b/Services/Github.qml new file mode 100644 index 0000000..4d6e8f2 --- /dev/null +++ b/Services/Github.qml @@ -0,0 +1,182 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Services +pragma Singleton + +// GitHub API logic and caching +Singleton { + id: root + + property string githubDataFile: Quickshell.env("NOCTALIA_GITHUB_FILE") || (Settings.cacheDir + "github.json") + property int githubUpdateFrequency: 60 * 60 // 1 hour expressed in seconds + property var data: adapter // Used to access via Github.data.xxx.yyy + property bool isFetchingData: false + + // Public properties for easy access + property string latestVersion: "Unknown" + property var contributors: [] + + FileView { + objectName: "githubDataFileView" + path: githubDataFile + watchChanges: true + onFileChanged: reload() + onAdapterUpdated: writeAdapter() + Component.onCompleted: function () { + reload() + } + onLoaded: function () { + loadFromCache() + } + onLoadFailed: function (error) { + if (error.toString().includes("No such file") || error === 2) { + // File doesn't exist, create it with default values + console.log("[Github] Creating new cache file..."); + writeAdapter() + // Fetch data after a short delay to ensure file is created + Qt.callLater(() => { + fetchFromGitHub() + }) + } + } + + JsonAdapter { + id: adapter + + property string version: "Unknown" + property var contributors: [] + property double timestamp: 0 + } + } + + // -------------------------------- + function init() { + // does nothing but ensure the singleton is created + // do not remove + } + + // -------------------------------- + function loadFromCache() { + const now = Date.now(); + if (!data.timestamp || (now - data.timestamp > githubUpdateFrequency * 1000)) { + console.log("[Github] Cache expired or missing, fetching new data from GitHub..."); + fetchFromGitHub(); + return; + } + console.log("[Github] Loading cached GitHub data (age: " + Math.round((now - data.timestamp) / 60000) + " minutes)"); + + if (data.version) { + root.latestVersion = data.version; + } + if (data.contributors) { + root.contributors = data.contributors; + } + } + + // -------------------------------- + function fetchFromGitHub() { + if (isFetchingData) { + console.warn("[Github] GitHub data is still fetching") + return + } + + isFetchingData = true + versionProcess.running = true; + contributorsProcess.running = true; + } + + // -------------------------------- + function saveData() { + data.timestamp = Date.now(); + Qt.callLater(() => { + // Access the FileView's writeAdapter method + var fileView = root.children.find(child => child.objectName === "githubDataFileView"); + if (fileView) { + fileView.writeAdapter(); + } + }); + } + + // -------------------------------- + function resetCache() { + data.version = "Unknown" + data.contributors = [] + data.timestamp = 0 + + // Try to fetch immediately + fetchFromGitHub() + } + + Process { + id: versionProcess + + command: ["curl", "-s", "https://api.github.com/repos/Ly-sec/Noctalia/releases/latest"] + + stdout: StdioCollector { + onStreamFinished: { + try { + const response = text; + if (response && response.trim()) { + const data = JSON.parse(response); + if (data.tag_name) { + const version = data.tag_name; + root.data.version = version; + root.latestVersion = version; + console.log("[Github] Latest version fetched from GitHub:", version); + } else { + console.log("[Github] No tag_name in GitHub response"); + } + } else { + console.log("[Github] Empty response from GitHub API"); + } + } catch (e) { + console.error("[Github] Failed to parse version:", e); + } + + // Check if both processes are done + checkAndSaveData(); + } + } + } + + Process { + id: contributorsProcess + + command: ["curl", "-s", "https://api.github.com/repos/Ly-sec/Noctalia/contributors?per_page=100"] + + stdout: StdioCollector { + onStreamFinished: { + try { + const response = text; + if (response && response.trim()) { + const data = JSON.parse(response); + root.data.contributors = data || []; + root.contributors = root.data.contributors; + console.log("[Github] Contributors fetched from GitHub:", root.contributors.length); + } else { + console.log("[Github] Empty response from GitHub API for contributors"); + root.data.contributors = []; + root.contributors = []; + } + } catch (e) { + console.error("[Github] Failed to parse contributors:", e); + root.data.contributors = []; + root.contributors = []; + } + + // Check if both processes are done + checkAndSaveData(); + } + } + } + + // -------------------------------- + function checkAndSaveData() { + // Only save when both processes are finished + if (!versionProcess.running && !contributorsProcess.running) { + root.isFetchingData = false; + root.saveData(); + } + } +} \ No newline at end of file From d0762bea3117853e14c7201b6c8dea18ad1912ed Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 08:13:54 -0400 Subject: [PATCH 158/394] NTextInput: fixing caller onEditingFinished --- Modules/Settings/SettingsWindow.qml | 4 ++-- Modules/Settings/Tabs/General.qml | 4 +++- Modules/Settings/Tabs/ScreenRecorder.qml | 4 +++- Modules/Settings/Tabs/TimeWeather.qml | 4 +++- Modules/Settings/Tabs/Wallpaper.qml | 4 +++- Services/Wallpapers.qml | 2 -- 6 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Modules/Settings/SettingsWindow.qml b/Modules/Settings/SettingsWindow.qml index 196e4ed..359d221 100644 --- a/Modules/Settings/SettingsWindow.qml +++ b/Modules/Settings/SettingsWindow.qml @@ -70,8 +70,8 @@ NLoader { border.color: Colors.backgroundTertiary border.width: Math.max(1, Style.borderMedium * scaling) layer.enabled: true - width: (screen.width/2) * scaling - height: (screen.height/2) * scaling + width: (screen.width / 2) * scaling + height: (screen.height / 2) * scaling anchors.centerIn: parent MouseArea { diff --git a/Modules/Settings/Tabs/General.qml b/Modules/Settings/Tabs/General.qml index 9693f6f..0202277 100644 --- a/Modules/Settings/Tabs/General.qml +++ b/Modules/Settings/Tabs/General.qml @@ -59,7 +59,9 @@ Item { text: Settings.data.general.avatarImage placeholderText: "/home/user/.face" Layout.fillWidth: true - onEditingFinished: Settings.data.general.avatarImage = text + onEditingFinished: function () { + Settings.data.general.avatarImage = text + } } } } diff --git a/Modules/Settings/Tabs/ScreenRecorder.qml b/Modules/Settings/Tabs/ScreenRecorder.qml index ae278be..39e1541 100644 --- a/Modules/Settings/Tabs/ScreenRecorder.qml +++ b/Modules/Settings/Tabs/ScreenRecorder.qml @@ -34,7 +34,9 @@ Item { NTextInput { text: Settings.data.screenRecorder.directory Layout.fillWidth: true - onEditingFinished: Settings.data.screenRecorder.directory = text + onEditingFinished: function () { + Settings.data.screenRecorder.directory = text + } } // Frame Rate diff --git a/Modules/Settings/Tabs/TimeWeather.qml b/Modules/Settings/Tabs/TimeWeather.qml index cf420f7..577fbbd 100644 --- a/Modules/Settings/Tabs/TimeWeather.qml +++ b/Modules/Settings/Tabs/TimeWeather.qml @@ -62,7 +62,9 @@ Item { NTextInput { text: Settings.data.location.name Layout.fillWidth: true - onEditingFinished: Settings.data.location.name = text + onEditingFinished: function () { + Settings.data.location.name = text + } } NText { diff --git a/Modules/Settings/Tabs/Wallpaper.qml b/Modules/Settings/Tabs/Wallpaper.qml index 88259e0..0a718b5 100644 --- a/Modules/Settings/Tabs/Wallpaper.qml +++ b/Modules/Settings/Tabs/Wallpaper.qml @@ -35,7 +35,9 @@ Item { NTextInput { text: Settings.data.wallpaper.directory Layout.fillWidth: true - onEditingFinished: Settings.data.wallpaper.directory = text + onEditingFinished: function () { + Settings.data.wallpaper.directory = text + } } NDivider { diff --git a/Services/Wallpapers.qml b/Services/Wallpapers.qml index bd78cf6..d18f3a5 100644 --- a/Services/Wallpapers.qml +++ b/Services/Wallpapers.qml @@ -22,8 +22,6 @@ Singleton { property string transitionType: Settings.data.wallpaper.swww.transitionType property var randomChoices: ["fade", "left", "right", "top", "bottom", "wipe", "wave", "grow", "center", "any", "outer"] - - function loadWallpapers() { scanning = true wallpaperList = [] From 934d4cc9333d005f1999a89f574bae6ddc026407 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 08:14:04 -0400 Subject: [PATCH 159/394] Formatting --- Modules/Settings/Tabs/About.qml | 546 ++++++++++++++++---------------- Services/Github.qml | 97 +++--- 2 files changed, 314 insertions(+), 329 deletions(-) diff --git a/Modules/Settings/Tabs/About.qml b/Modules/Settings/Tabs/About.qml index 3c97970..58e5eb1 100644 --- a/Modules/Settings/Tabs/About.qml +++ b/Modules/Settings/Tabs/About.qml @@ -8,330 +8,316 @@ import qs.Services import qs.Widgets ColumnLayout { - id: root + id: root - property string latestVersion: Github.latestVersion - property string currentVersion: "v1.2.1" // Fallback version - property var contributors: Github.contributors + property string latestVersion: Github.latestVersion + property string currentVersion: "v1.2.1" // Fallback version + property var contributors: Github.contributors + Component.onCompleted: { + // Initialize the Github service + Github.init() + } + + spacing: 0 + Layout.fillWidth: true + Layout.fillHeight: true + + Process { + id: currentVersionProcess + + command: ["sh", "-c", "cd " + Quickshell.shellDir + " && git describe --tags --abbrev=0 2>/dev/null || echo 'Unknown'"] Component.onCompleted: { - // Initialize the Github service - Github.init(); + running = true } - spacing: 0 + stdout: StdioCollector { + onStreamFinished: { + const version = text.trim() + if (version && version !== "Unknown") { + root.currentVersion = version + } else { + currentVersionProcess.command = ["sh", "-c", "cd " + Quickshell.shellDir + + " && cat package.json 2>/dev/null | grep '\"version\"' | cut -d'\"' -f4 || echo 'Unknown'"] + currentVersionProcess.running = true + } + } + } + } + + ScrollView { + id: scrollView + Layout.fillWidth: true Layout.fillHeight: true + padding: 16 + rightPadding: 12 + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded - Process { - id: currentVersionProcess + ColumnLayout { + width: scrollView.availableWidth + spacing: 0 - command: ["sh", "-c", "cd " + Quickshell.shellDir + " && git describe --tags --abbrev=0 2>/dev/null || echo 'Unknown'"] - Component.onCompleted: { - running = true; + NText { + text: "Noctalia: quiet by design" + font.pointSize: 24 * Scaling.scale(screen) + font.weight: Style.fontWeightBold + color: Colors.textPrimary + Layout.alignment: Qt.AlignCenter + Layout.bottomMargin: 8 * Scaling.scale(screen) + } + + NText { + text: "It may just be another quickshell setup but it won't get in your way." + font.pointSize: 14 * Scaling.scale(screen) + color: Colors.textSecondary + Layout.alignment: Qt.AlignCenter + Layout.bottomMargin: 16 * Scaling.scale(screen) + } + + GridLayout { + Layout.alignment: Qt.AlignCenter + columns: 2 + rowSpacing: 4 + columnSpacing: 8 + + NText { + text: "Latest Version:" + font.pointSize: 16 * Scaling.scale(screen) + color: Colors.textSecondary + Layout.alignment: Qt.AlignRight } - stdout: StdioCollector { - onStreamFinished: { - const version = text.trim(); - if (version && version !== "Unknown") { - root.currentVersion = version; - } else { - currentVersionProcess.command = ["sh", "-c", "cd " + Quickshell.shellDir + " && cat package.json 2>/dev/null | grep '\"version\"' | cut -d'\"' -f4 || echo 'Unknown'"]; - currentVersionProcess.running = true; - } - } + NText { + text: root.latestVersion + font.pointSize: 16 * Scaling.scale(screen) + color: Colors.textPrimary + font.weight: Style.fontWeightBold } - } + NText { + text: "Installed Version:" + font.pointSize: 16 * Scaling.scale(screen) + color: Colors.textSecondary + Layout.alignment: Qt.AlignRight + } - ScrollView { - id: scrollView + NText { + text: root.currentVersion + font.pointSize: 16 * Scaling.scale(screen) + color: Colors.textPrimary + font.weight: Style.fontWeightBold + } + } + Rectangle { + Layout.alignment: Qt.AlignCenter + Layout.topMargin: 8 + Layout.preferredWidth: updateText.implicitWidth + 46 + Layout.preferredHeight: 32 + radius: 20 + color: updateArea.containsMouse ? Colors.accentPrimary : "transparent" + border.color: Colors.accentPrimary + border.width: 1 + visible: { + if (root.currentVersion === "Unknown" || root.latestVersion === "Unknown") + return false + + const latest = root.latestVersion.replace("v", "").split(".") + const current = root.currentVersion.replace("v", "").split(".") + for (var i = 0; i < Math.max(latest.length, current.length); i++) { + const l = parseInt(latest[i] || "0") + const c = parseInt(current[i] || "0") + if (l > c) + return true + + if (l < c) + return false + } + return false + } + + RowLayout { + anchors.centerIn: parent + spacing: 8 + + NText { + text: "system_update" + font.family: "Material Symbols Outlined" + font.pointSize: 18 * Scaling.scale(screen) + color: updateArea.containsMouse ? Colors.backgroundPrimary : Colors.accentPrimary + } + + NText { + id: updateText + + text: "Download latest release" + font.pointSize: 14 * Scaling.scale(screen) + color: updateArea.containsMouse ? Colors.backgroundPrimary : Colors.accentPrimary + } + } + + MouseArea { + id: updateArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + Quickshell.execDetached(["xdg-open", "https://github.com/Ly-sec/Noctalia/releases/latest"]) + } + } + } + + // Separator + Rectangle { Layout.fillWidth: true + Layout.topMargin: 26 + Layout.bottomMargin: 18 + height: 1 + color: Colors.outline + opacity: 0.3 + } + + NText { + text: "Contributors" + font.pointSize: 18 * Scaling.scale(screen) + font.weight: Style.fontWeightBold + color: Colors.textPrimary + Layout.alignment: Qt.AlignCenter + Layout.topMargin: 32 + } + + NText { + text: "(" + root.contributors.length + ")" + font.pointSize: 14 * Scaling.scale(screen) + color: Colors.textSecondary + Layout.alignment: Qt.AlignCenter + Layout.topMargin: 4 + } + + ScrollView { + Layout.alignment: Qt.AlignCenter + Layout.preferredWidth: 200 * 4 Layout.fillHeight: true - padding: 16 - rightPadding: 12 + Layout.topMargin: 16 clip: true ScrollBar.horizontal.policy: ScrollBar.AlwaysOff ScrollBar.vertical.policy: ScrollBar.AsNeeded - ColumnLayout { - width: scrollView.availableWidth - spacing: 0 + GridView { + id: contributorsGrid - NText { - text: "Noctalia: quiet by design" - font.pointSize: 24 * Scaling.scale(screen) - font.weight: Style.fontWeightBold - color: Colors.textPrimary - Layout.alignment: Qt.AlignCenter - Layout.bottomMargin: 8 * Scaling.scale(screen) - } + anchors.fill: parent + width: 200 * 4 + height: Math.ceil(root.contributors.length / 4) * 100 + cellWidth: 200 + cellHeight: 100 + model: root.contributors - NText { - text: "It may just be another quickshell setup but it won't get in your way." - font.pointSize: 14 * Scaling.scale(screen) - color: Colors.textSecondary - Layout.alignment: Qt.AlignCenter - Layout.bottomMargin: 16 * Scaling.scale(screen) - } + delegate: Rectangle { + width: contributorsGrid.cellWidth - 16 + height: contributorsGrid.cellHeight - 4 + radius: 20 + color: contributorArea.containsMouse ? Colors.hover : "transparent" - GridLayout { - Layout.alignment: Qt.AlignCenter - columns: 2 - rowSpacing: 4 - columnSpacing: 8 + RowLayout { + anchors.fill: parent + anchors.margins: 8 + spacing: 12 - NText { - text: "Latest Version:" - font.pointSize: 16 * Scaling.scale(screen) - color: Colors.textSecondary - Layout.alignment: Qt.AlignRight - } + Item { + Layout.alignment: Qt.AlignVCenter + Layout.preferredWidth: 40 + Layout.preferredHeight: 40 - NText { - text: root.latestVersion - font.pointSize: 16 * Scaling.scale(screen) - color: Colors.textPrimary - font.weight: Style.fontWeightBold - } + Image { + id: avatarImage - NText { - text: "Installed Version:" - font.pointSize: 16 * Scaling.scale(screen) - color: Colors.textSecondary - Layout.alignment: Qt.AlignRight - } - - NText { - text: root.currentVersion - font.pointSize: 16 * Scaling.scale(screen) - color: Colors.textPrimary - font.weight: Style.fontWeightBold - } - - } - - Rectangle { - Layout.alignment: Qt.AlignCenter - Layout.topMargin: 8 - Layout.preferredWidth: updateText.implicitWidth + 46 - Layout.preferredHeight: 32 - radius: 20 - color: updateArea.containsMouse ? Colors.accentPrimary : "transparent" - border.color: Colors.accentPrimary - border.width: 1 - visible: { - if (root.currentVersion === "Unknown" || root.latestVersion === "Unknown") - return false; - - const latest = root.latestVersion.replace("v", "").split("."); - const current = root.currentVersion.replace("v", "").split("."); - for (let i = 0; i < Math.max(latest.length, current.length); i++) { - const l = parseInt(latest[i] || "0"); - const c = parseInt(current[i] || "0"); - if (l > c) - return true; - - if (l < c) - return false; + anchors.fill: parent + source: modelData.avatar_url || "" + sourceSize: Qt.size(80, 80) + visible: false + mipmap: true + smooth: true + asynchronous: true + fillMode: Image.PreserveAspectCrop + cache: true + onStatusChanged: { + if (status === Image.Error) { + console.log("[About] Failed to load avatar for", modelData.login, "URL:", modelData.avatar_url) } - return false; + } } - RowLayout { - anchors.centerIn: parent - spacing: 8 - - NText { - text: "system_update" - font.family: "Material Symbols Outlined" - font.pointSize: 18 * Scaling.scale(screen) - color: updateArea.containsMouse ? Colors.backgroundPrimary : Colors.accentPrimary - } - - NText { - id: updateText - - text: "Download latest release" - font.pointSize: 14 * Scaling.scale(screen) - color: updateArea.containsMouse ? Colors.backgroundPrimary : Colors.accentPrimary - } - + MultiEffect { + anchors.fill: parent + source: avatarImage + maskEnabled: true + maskSource: mask } - MouseArea { - id: updateArea + Item { + id: mask + anchors.fill: parent + layer.enabled: true + visible: false + + Rectangle { anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - Quickshell.execDetached(["xdg-open", "https://github.com/Ly-sec/Noctalia/releases/latest"]); - } + radius: parent.width / 2 + } } - } + NText { + anchors.centerIn: parent + text: "person" + font.family: "Material Symbols Outlined" + font.pointSize: 24 * Scaling.scale(screen) + color: contributorArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary + visible: !avatarImage.source || avatarImage.status !== Image.Ready + } + } - // Separator - Rectangle { + ColumnLayout { + spacing: 4 + Layout.alignment: Qt.AlignVCenter Layout.fillWidth: true - Layout.topMargin: 26 - Layout.bottomMargin: 18 - height: 1 - color: Colors.outline - opacity: 0.3 - } - - NText { - text: "Contributors" - font.pointSize: 18 * Scaling.scale(screen) - font.weight: Style.fontWeightBold - color: Colors.textPrimary - Layout.alignment: Qt.AlignCenter - Layout.topMargin: 32 - } - - NText { - text: "(" + root.contributors.length + ")" - font.pointSize: 14 * Scaling.scale(screen) - color: Colors.textSecondary - Layout.alignment: Qt.AlignCenter - Layout.topMargin: 4 - } - - ScrollView { - Layout.alignment: Qt.AlignCenter - Layout.preferredWidth: 200 * 4 - Layout.fillHeight: true - Layout.topMargin: 16 - clip: true - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - ScrollBar.vertical.policy: ScrollBar.AsNeeded - - GridView { - id: contributorsGrid - - anchors.fill: parent - width: 200 * 4 - height: Math.ceil(root.contributors.length / 4) * 100 - cellWidth: 200 - cellHeight: 100 - model: root.contributors - - delegate: Rectangle { - width: contributorsGrid.cellWidth - 16 - height: contributorsGrid.cellHeight - 4 - radius: 20 - color: contributorArea.containsMouse ? Colors.hover : "transparent" - - RowLayout { - anchors.fill: parent - anchors.margins: 8 - spacing: 12 - - Item { - Layout.alignment: Qt.AlignVCenter - Layout.preferredWidth: 40 - Layout.preferredHeight: 40 - - Image { - id: avatarImage - - anchors.fill: parent - source: modelData.avatar_url || "" - sourceSize: Qt.size(80, 80) - visible: false - mipmap: true - smooth: true - asynchronous: true - fillMode: Image.PreserveAspectCrop - cache: true - - onStatusChanged: { - if (status === Image.Error) { - console.log("[About] Failed to load avatar for", modelData.login, "URL:", modelData.avatar_url); - } - } - } - - MultiEffect { - anchors.fill: parent - source: avatarImage - maskEnabled: true - maskSource: mask - } - - Item { - id: mask - - anchors.fill: parent - layer.enabled: true - visible: false - - Rectangle { - anchors.fill: parent - radius: parent.width / 2 - } - } - - NText { - anchors.centerIn: parent - text: "person" - font.family: "Material Symbols Outlined" - font.pointSize: 24 * Scaling.scale(screen) - color: contributorArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary - visible: !avatarImage.source || avatarImage.status !== Image.Ready - } - - } - - ColumnLayout { - spacing: 4 - Layout.alignment: Qt.AlignVCenter - Layout.fillWidth: true - - NText { - text: modelData.login || "Unknown" - font.pointSize: 13 * Scaling.scale(screen) - color: contributorArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary - elide: Text.ElideRight - Layout.fillWidth: true - } - - NText { - text: (modelData.contributions || 0) + " commits" - font.pointSize: 11 * Scaling.scale(screen) - color: contributorArea.containsMouse ? Colors.backgroundPrimary : Colors.textSecondary - } - - } - - } - - MouseArea { - id: contributorArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (modelData.html_url) - Quickshell.execDetached(["xdg-open", modelData.html_url]); - - } - } - - } + NText { + text: modelData.login || "Unknown" + font.pointSize: 13 * Scaling.scale(screen) + color: contributorArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary + elide: Text.ElideRight + Layout.fillWidth: true } + NText { + text: (modelData.contributions || 0) + " commits" + font.pointSize: 11 * Scaling.scale(screen) + color: contributorArea.containsMouse ? Colors.backgroundPrimary : Colors.textSecondary + } + } } + MouseArea { + id: contributorArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (modelData.html_url) + Quickshell.execDetached(["xdg-open", modelData.html_url]) + } + } + } } - + } } - + } } diff --git a/Services/Github.qml b/Services/Github.qml index 4d6e8f2..a1409a6 100644 --- a/Services/Github.qml +++ b/Services/Github.qml @@ -32,12 +32,12 @@ Singleton { onLoadFailed: function (error) { if (error.toString().includes("No such file") || error === 2) { // File doesn't exist, create it with default values - console.log("[Github] Creating new cache file..."); + console.log("[Github] Creating new cache file...") writeAdapter() // Fetch data after a short delay to ensure file is created Qt.callLater(() => { - fetchFromGitHub() - }) + fetchFromGitHub() + }) } } @@ -51,26 +51,25 @@ Singleton { } // -------------------------------- - function init() { - // does nothing but ensure the singleton is created + function init() {// does nothing but ensure the singleton is created // do not remove } // -------------------------------- function loadFromCache() { - const now = Date.now(); + const now = Date.now() if (!data.timestamp || (now - data.timestamp > githubUpdateFrequency * 1000)) { - console.log("[Github] Cache expired or missing, fetching new data from GitHub..."); - fetchFromGitHub(); - return; + console.log("[Github] Cache expired or missing, fetching new data from GitHub...") + fetchFromGitHub() + return } - console.log("[Github] Loading cached GitHub data (age: " + Math.round((now - data.timestamp) / 60000) + " minutes)"); - + console.log("[Github] Loading cached GitHub data (age: " + Math.round((now - data.timestamp) / 60000) + " minutes)") + if (data.version) { - root.latestVersion = data.version; + root.latestVersion = data.version } if (data.contributors) { - root.contributors = data.contributors; + root.contributors = data.contributors } } @@ -82,20 +81,20 @@ Singleton { } isFetchingData = true - versionProcess.running = true; - contributorsProcess.running = true; + versionProcess.running = true + contributorsProcess.running = true } // -------------------------------- function saveData() { - data.timestamp = Date.now(); + data.timestamp = Date.now() Qt.callLater(() => { - // Access the FileView's writeAdapter method - var fileView = root.children.find(child => child.objectName === "githubDataFileView"); - if (fileView) { - fileView.writeAdapter(); - } - }); + // Access the FileView's writeAdapter method + var fileView = root.children.find(child => child.objectName === "githubDataFileView") + if (fileView) { + fileView.writeAdapter() + } + }) } // -------------------------------- @@ -116,26 +115,26 @@ Singleton { stdout: StdioCollector { onStreamFinished: { try { - const response = text; + const response = text if (response && response.trim()) { - const data = JSON.parse(response); + const data = JSON.parse(response) if (data.tag_name) { - const version = data.tag_name; - root.data.version = version; - root.latestVersion = version; - console.log("[Github] Latest version fetched from GitHub:", version); + const version = data.tag_name + root.data.version = version + root.latestVersion = version + console.log("[Github] Latest version fetched from GitHub:", version) } else { - console.log("[Github] No tag_name in GitHub response"); + console.log("[Github] No tag_name in GitHub response") } } else { - console.log("[Github] Empty response from GitHub API"); + console.log("[Github] Empty response from GitHub API") } } catch (e) { - console.error("[Github] Failed to parse version:", e); + console.error("[Github] Failed to parse version:", e) } - + // Check if both processes are done - checkAndSaveData(); + checkAndSaveData() } } } @@ -148,25 +147,25 @@ Singleton { stdout: StdioCollector { onStreamFinished: { try { - const response = text; + const response = text if (response && response.trim()) { - const data = JSON.parse(response); - root.data.contributors = data || []; - root.contributors = root.data.contributors; - console.log("[Github] Contributors fetched from GitHub:", root.contributors.length); + const data = JSON.parse(response) + root.data.contributors = data || [] + root.contributors = root.data.contributors + console.log("[Github] Contributors fetched from GitHub:", root.contributors.length) } else { - console.log("[Github] Empty response from GitHub API for contributors"); - root.data.contributors = []; - root.contributors = []; + console.log("[Github] Empty response from GitHub API for contributors") + root.data.contributors = [] + root.contributors = [] } } catch (e) { - console.error("[Github] Failed to parse contributors:", e); - root.data.contributors = []; - root.contributors = []; + console.error("[Github] Failed to parse contributors:", e) + root.data.contributors = [] + root.contributors = [] } - + // Check if both processes are done - checkAndSaveData(); + checkAndSaveData() } } } @@ -175,8 +174,8 @@ Singleton { function checkAndSaveData() { // Only save when both processes are finished if (!versionProcess.running && !contributorsProcess.running) { - root.isFetchingData = false; - root.saveData(); + root.isFetchingData = false + root.saveData() } } -} \ No newline at end of file +} From a127696f3545393b7ffe490059f74521ef39a0c6 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Tue, 12 Aug 2025 14:28:48 +0200 Subject: [PATCH 160/394] Fix NPill scaling --- Modules/DemoPanel/DemoPanel.qml | 28 +++++++++++++++++++++------- Widgets/NPill.qml | 4 ++-- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/Modules/DemoPanel/DemoPanel.qml b/Modules/DemoPanel/DemoPanel.qml index e66541d..183291d 100644 --- a/Modules/DemoPanel/DemoPanel.qml +++ b/Modules/DemoPanel/DemoPanel.qml @@ -16,16 +16,19 @@ NLoader { readonly property real scaling: Scaling.scale(screen) // Ensure panel shows itself once created - Component.onCompleted: show() + Component.onCompleted: { + console.log("[DemoPanel] Component completed, showing panel...") + show() + } Rectangle { id: bgRect color: Colors.backgroundPrimary radius: Style.radiusMedium * scaling - border.color: Colors.backgroundTertiary - border.width: Math.max(1, Style.borderMedium * scaling) - width: 600 * scaling - height: 600 * scaling + border.color: Colors.accentPrimary + border.width: 2 + width: 500 * scaling + height: 700 * scaling anchors.centerIn: parent // Prevent closing when clicking in the panel bg @@ -33,10 +36,21 @@ NLoader { anchors.fill: parent } + // Debug: Add a simple text to see if content is visible + NText { + text: "DemoPanel is working!" + color: Colors.accentPrimary + font.pointSize: Style.fontSizeLarge * scaling + anchors.top: parent.top + anchors.horizontalCenter: parent.horizontalCenter + anchors.topMargin: 20 * scaling + } + ColumnLayout { anchors.fill: parent - anchors.margins: Style.marginXL * scaling - spacing: Style.marginSmall * scaling + anchors.margins: Style.marginMedium * scaling + anchors.topMargin: (Style.marginMedium + 40) * scaling + spacing: Style.marginMedium * scaling // NSlider ColumnLayout { diff --git a/Widgets/NPill.qml b/Widgets/NPill.qml index ff819f3..3a6ac0e 100644 --- a/Widgets/NPill.qml +++ b/Widgets/NPill.qml @@ -56,7 +56,7 @@ Item { id: textItem anchors.centerIn: parent text: root.text - font.pointSize: Colors.fontSizeSmall * scaling + font.pointSize: Style.fontSizeSmall * scaling font.weight: Style.fontWeightBold color: textColor visible: showPill @@ -97,7 +97,7 @@ Item { Text { anchors.centerIn: parent font.family: showPill ? "Material Symbols Rounded" : "Material Symbols Outlined" - font.pointSize: Colors.fontSizeSmall * scaling + font.pointSize: Style.fontSizeMedium * scaling text: root.icon color: showPill ? iconTextColor : collapsedIconColor } From 1dcc42ef19319bf65b62b755d3e6d44c202380d1 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Tue, 12 Aug 2025 14:30:54 +0200 Subject: [PATCH 161/394] Fix Notification scaling --- Modules/Notification/Notification.qml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index 324387a..1a67b03 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -143,7 +143,7 @@ PanelWindow { NText { text: (model.appName || model.desktopEntry) || "Unknown App" color: Colors.accentSecondary - font.pointSize: Style.fontSizeSmall + font.pointSize: Style.fontSizeSmall * scaling } Rectangle { width: 6 * scaling @@ -158,13 +158,13 @@ PanelWindow { NText { text: notificationService.formatTimestamp(model.timestamp) color: Colors.textSecondary - font.pointSize: Style.fontSizeSmall + font.pointSize: Style.fontSizeSmall * scaling } } NText { text: model.summary || "No summary" - font.pointSize: Style.fontSizeLarge + font.pointSize: Style.fontSizeLarge * scaling font.weight: Style.fontWeightBold color: Colors.textPrimary wrapMode: Text.Wrap @@ -175,7 +175,7 @@ PanelWindow { NText { text: model.body || "" - font.pointSize: Style.fontSizeSmall + font.pointSize: Style.fontSizeSmall * scaling color: Colors.textSecondary wrapMode: Text.Wrap width: 300 * scaling From 618bee311b745cbffce4d5ec042cbd8ed2d61feb Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 08:33:42 -0400 Subject: [PATCH 162/394] Location/Weather: Improved logic and proper refresh of data when changing location name in the settings UI --- Modules/Settings/Tabs/TimeWeather.qml | 1 + Services/Location.qml | 28 ++++++++++++++------------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/Modules/Settings/Tabs/TimeWeather.qml b/Modules/Settings/Tabs/TimeWeather.qml index 577fbbd..b78ce02 100644 --- a/Modules/Settings/Tabs/TimeWeather.qml +++ b/Modules/Settings/Tabs/TimeWeather.qml @@ -64,6 +64,7 @@ Item { Layout.fillWidth: true onEditingFinished: function () { Settings.data.location.name = text + Location.resetWeather(); } } diff --git a/Services/Location.qml b/Services/Location.qml index e71c109..49dac02 100644 --- a/Services/Location.qml +++ b/Services/Location.qml @@ -41,10 +41,10 @@ Singleton { } } - // Every minute check if we need to fetch new weather + // Every 20s check if we need to fetch new weather Timer { id: updateTimer - interval: 60 * 1000 + interval: 20 * 1000 running: true repeat: true onTriggered: { @@ -55,6 +55,7 @@ Singleton { // -------------------------------- function init() {// does nothing but ensure the singleton is created // do not remove + console.log("[Location] Service started") } // -------------------------------- @@ -71,11 +72,11 @@ Singleton { // -------------------------------- function updateWeather() { if (isFetchingWeather) { - console.warn("Weather is still fetching") + console.warn("[Location] Weather is still fetching") return } - if ((data.weatherLastFetch === "") || (Time.timestamp >= data.weatherLastFetch + weatherUpdateFrequency)) { + if ((data.weatherLastFetch === "") || (data.weather === null) || (Time.timestamp >= data.weatherLastFetch + weatherUpdateFrequency)) { getFreshWeather() } } @@ -84,9 +85,9 @@ Singleton { function getFreshWeather() { isFetchingWeather = true if (data.latitude === "" || data.longitude === "") { - console.log("Geocoding location") + _geocodeLocation(Settings.data.location.name, function (lat, lon) { - console.log("Geocoded " + Settings.data.location.name + " to: " + lat + " / " + lon) + console.log("[Location] Geocoded " + Settings.data.location.name + " to: " + lat + " / " + lon) // Save GPS coordinates data.latitude = lat @@ -101,6 +102,7 @@ Singleton { // -------------------------------- function _geocodeLocation(locationName, callback, errorCallback) { + console.log("[Location] Geocoding from api.open-meteo.com") var geoUrl = "https://geocoding-api.open-meteo.com/v1/search?name=" + encodeURIComponent( locationName) + "&language=en&format=json" var xhr = new XMLHttpRequest() @@ -112,13 +114,13 @@ Singleton { if (geoData.results && geoData.results.length > 0) { callback(geoData.results[0].latitude, geoData.results[0].longitude) } else { - errorCallback("Location not found.") + errorCallback("[Location] could not resolve location name") } } catch (e) { - errorCallback("Failed to parse geocoding data.") + errorCallback("[Location] Failed to parse geocoding data: " + e) } } else { - errorCallback("Geocoding error: " + xhr.status) + errorCallback("[Location] Geocoding error: " + xhr.status) } } } @@ -128,7 +130,7 @@ Singleton { // -------------------------------- function _fetchWeather(latitude, longitude, errorCallback) { - console.log("Getting weather") + console.log("[Location] Fetching weather from api.open-meteo.com") var url = "https://api.open-meteo.com/v1/forecast?latitude=" + latitude + "&longitude=" + longitude + "¤t_weather=true¤t=relativehumidity_2m,surface_pressure&daily=temperature_2m_max,temperature_2m_min,weathercode&timezone=auto" var xhr = new XMLHttpRequest() @@ -142,12 +144,12 @@ Singleton { data.weather = weatherData data.weatherLastFetch = Time.timestamp isFetchingWeather = false - console.log("Cached weather to disk") + console.log("[Location] Cached weather to disk") } catch (e) { - errorCallback("Failed to parse weather data.") + errorCallback("[Location] Failed to parse weather data") } } else { - errorCallback("Weather fetch error: " + xhr.status) + errorCallback("[Location] Weather fetch error: " + xhr.status) } } } From ca8523ec1a5581cca2435b61614b24bac32cd58e Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 09:40:10 -0400 Subject: [PATCH 163/394] Location: having issue with FileView sometimes serializing 0 instead of latitude --- Modules/Settings/Tabs/TimeWeather.qml | 2 +- Services/Location.qml | 54 ++++++++++++++++----------- 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/Modules/Settings/Tabs/TimeWeather.qml b/Modules/Settings/Tabs/TimeWeather.qml index b78ce02..f329def 100644 --- a/Modules/Settings/Tabs/TimeWeather.qml +++ b/Modules/Settings/Tabs/TimeWeather.qml @@ -64,7 +64,7 @@ Item { Layout.fillWidth: true onEditingFinished: function () { Settings.data.location.name = text - Location.resetWeather(); + Location.resetWeather() } } diff --git a/Services/Location.qml b/Services/Location.qml index 49dac02..d6df1e2 100644 --- a/Services/Location.qml +++ b/Services/Location.qml @@ -15,27 +15,20 @@ Singleton { FileView { path: locationFile - watchChanges: true - onFileChanged: reload() onAdapterUpdated: writeAdapter() - Component.onCompleted: function () { - reload() - } onLoaded: function () { updateWeather() } onLoadFailed: function (error) { - if (error.toString().includes("No such file") || error === 2) { - // File doesn't exist, create it with default values - writeAdapter() - } + updateWeather() } JsonAdapter { id: adapter - property string latitude: "" - property string longitude: "" + property string lastLocationName: "" + property real latitude: 0 + property real longitude: 0 property int weatherLastFetch: 0 property var weather: null } @@ -53,15 +46,19 @@ Singleton { } // -------------------------------- - function init() {// does nothing but ensure the singleton is created + function init() { + // does nothing but ensure the singleton is created // do not remove console.log("[Location] Service started") } // -------------------------------- function resetWeather() { - data.latitude = "" - data.longitude = "" + console.log("[Location] Resetting weather data") + + data.lastLocationName = "" + data.latitude = 0 + data.longitude = 0 data.weatherLastFetch = 0 data.weather = null @@ -76,7 +73,13 @@ Singleton { return } - if ((data.weatherLastFetch === "") || (data.weather === null) || (Time.timestamp >= data.weatherLastFetch + weatherUpdateFrequency)) { + if (data.latitude === 0) { + console.warn("[Location] Why is my latitude zero") + } + + if ((data.weatherLastFetch === "") || (data.weather === null) || data.latitude === 0 || data.longitude === 0 + || (data.lastLocationName !== Settings.data.location.name) + || (Time.timestamp >= data.weatherLastFetch + weatherUpdateFrequency)) { getFreshWeather() } } @@ -84,16 +87,19 @@ Singleton { // -------------------------------- function getFreshWeather() { isFetchingWeather = true - if (data.latitude === "" || data.longitude === "") { + if (data.latitude === 0 || data.longitude === 0 || (data.lastLocationName !== Settings.data.location.name)) { - _geocodeLocation(Settings.data.location.name, function (lat, lon) { - console.log("[Location] Geocoded " + Settings.data.location.name + " to: " + lat + " / " + lon) + _geocodeLocation(Settings.data.location.name, function (latitude, longitude) { + console.log("[Location] Geocoded " + Settings.data.location.name + " to: " + latitude + " / " + longitude) + + // Save location name + data.lastLocationName = Settings.data.location.name // Save GPS coordinates - data.latitude = lat - data.longitude = lon + data.latitude = latitude + data.longitude = longitude - _fetchWeather(data.latitude, data.longitude, errorCallback) + _fetchWeather(latitude, longitude, errorCallback) }, errorCallback) } else { _fetchWeather(data.latitude, data.longitude, errorCallback) @@ -111,6 +117,7 @@ Singleton { if (xhr.status === 200) { try { var geoData = JSON.parse(xhr.responseText) + // console.log(JSON.stringify(geoData)) if (geoData.results && geoData.results.length > 0) { callback(geoData.results[0].latitude, geoData.results[0].longitude) } else { @@ -140,9 +147,12 @@ Singleton { try { var weatherData = JSON.parse(xhr.responseText) - // Save to json + // Save data data.weather = weatherData data.weatherLastFetch = Time.timestamp + data.latitude = weatherData.latitude + data.longitude = weatherData.longitude + isFetchingWeather = false console.log("[Location] Cached weather to disk") } catch (e) { From f398f277417b788607052b60693ccf187651b854 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 09:51:34 -0400 Subject: [PATCH 164/394] Location: fighting with a FileView issue? --- Services/Location.qml | 47 ++++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/Services/Location.qml b/Services/Location.qml index d6df1e2..aaf0d40 100644 --- a/Services/Location.qml +++ b/Services/Location.qml @@ -10,7 +10,7 @@ Singleton { property string locationFile: Quickshell.env("NOCTALIA_WEATHER_FILE") || (Settings.cacheDir + "location.json") property int weatherUpdateFrequency: 30 * 60 // 30 minutes expressed in seconds - property var data: adapter // Used to access via Location.data.xxx.yyy + property var data: adapter // Used to access via Location.data.xxx property bool isFetchingWeather: false FileView { @@ -26,9 +26,9 @@ Singleton { JsonAdapter { id: adapter - property string lastLocationName: "" - property real latitude: 0 - property real longitude: 0 + property string latitude: "" + property string longitude: "" + property string name: "" property int weatherLastFetch: 0 property var weather: null } @@ -56,9 +56,9 @@ Singleton { function resetWeather() { console.log("[Location] Resetting weather data") - data.lastLocationName = "" - data.latitude = 0 - data.longitude = 0 + data.latitude = "" + data.longitude = "" + data.name = "" data.weatherLastFetch = 0 data.weather = null @@ -73,13 +73,17 @@ Singleton { return } - if (data.latitude === 0) { - console.warn("[Location] Why is my latitude zero") + if (data.latitude === "") { + console.warn("[Location] Why is my latitude empty") } - if ((data.weatherLastFetch === "") || (data.weather === null) || data.latitude === 0 || data.longitude === 0 - || (data.lastLocationName !== Settings.data.location.name) - || (Time.timestamp >= data.weatherLastFetch + weatherUpdateFrequency)) { + if ( + (data.weatherLastFetch === "") || + (data.weather === null) || + (data.latitude === "") || + (data.longitude === "") || + (data.name !== Settings.data.location.name) + || (Time.timestamp >= data.weatherLastFetch + weatherUpdateFrequency)) { getFreshWeather() } } @@ -87,17 +91,22 @@ Singleton { // -------------------------------- function getFreshWeather() { isFetchingWeather = true - if (data.latitude === 0 || data.longitude === 0 || (data.lastLocationName !== Settings.data.location.name)) { + if ( + (data.latitude === "") || + (data.longitude === "") || + (data.name !== Settings.data.location.name)) { _geocodeLocation(Settings.data.location.name, function (latitude, longitude) { console.log("[Location] Geocoded " + Settings.data.location.name + " to: " + latitude + " / " + longitude) - // Save location name - data.lastLocationName = Settings.data.location.name + // Save location name + data.name = Settings.data.location.name // Save GPS coordinates - data.latitude = latitude - data.longitude = longitude + data.latitude = latitude.toString() + data.longitude = longitude.toString() + + _fetchWeather(latitude, longitude, errorCallback) }, errorCallback) @@ -150,8 +159,8 @@ Singleton { // Save data data.weather = weatherData data.weatherLastFetch = Time.timestamp - data.latitude = weatherData.latitude - data.longitude = weatherData.longitude + data.latitude = weatherData.latitude.toString() + data.longitude = weatherData.longitude.toString() isFetchingWeather = false console.log("[Location] Cached weather to disk") From 091d7bf5a375d4ae14c5fcbb9adccabf64b83acd Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 10:05:57 -0400 Subject: [PATCH 165/394] Formatting Location --- Services/Location.qml | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/Services/Location.qml b/Services/Location.qml index aaf0d40..1c6e3cd 100644 --- a/Services/Location.qml +++ b/Services/Location.qml @@ -77,13 +77,9 @@ Singleton { console.warn("[Location] Why is my latitude empty") } - if ( - (data.weatherLastFetch === "") || - (data.weather === null) || - (data.latitude === "") || - (data.longitude === "") || - (data.name !== Settings.data.location.name) - || (Time.timestamp >= data.weatherLastFetch + weatherUpdateFrequency)) { + if ((data.weatherLastFetch === "") || (data.weather === null) || (data.latitude === "") || (data.longitude === "") + || (data.name !== Settings.data.location.name) + || (Time.timestamp >= data.weatherLastFetch + weatherUpdateFrequency)) { getFreshWeather() } } @@ -91,23 +87,18 @@ Singleton { // -------------------------------- function getFreshWeather() { isFetchingWeather = true - if ( - (data.latitude === "") || - (data.longitude === "") || - (data.name !== Settings.data.location.name)) { + if ((data.latitude === "") || (data.longitude === "") || (data.name !== Settings.data.location.name)) { _geocodeLocation(Settings.data.location.name, function (latitude, longitude) { console.log("[Location] Geocoded " + Settings.data.location.name + " to: " + latitude + " / " + longitude) - // Save location name + // Save location name data.name = Settings.data.location.name // Save GPS coordinates data.latitude = latitude.toString() data.longitude = longitude.toString() - - _fetchWeather(latitude, longitude, errorCallback) }, errorCallback) } else { From 0202965f65d6f28311ba81bfcd86f33eaf95fb33 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 10:08:33 -0400 Subject: [PATCH 166/394] Nicer weather card --- Modules/SidePanel/WeatherCard.qml | 40 ++++++++++++++++--------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/Modules/SidePanel/WeatherCard.qml b/Modules/SidePanel/WeatherCard.qml index a3041d2..c3de73f 100644 --- a/Modules/SidePanel/WeatherCard.qml +++ b/Modules/SidePanel/WeatherCard.qml @@ -33,35 +33,37 @@ NBox { } ColumnLayout { + + NText { + text: Settings.data.location.name + font.weight: Style.fontWeightBold + font.pointSize: Style.fontSizeXL * scaling + } + RowLayout { NText { - text: Settings.data.location.name - font.weight: Style.fontWeightBold + visible: weatherReady + text: { + if (!weatherReady) { + return "" + } + var temp = Location.data.weather.current_weather.temperature + if (Settings.data.location.useFahrenheit) { + temp = Location.celsiusToFahrenheit(temp) + } + temp = Math.round(temp) + return `${temp}°` + } font.pointSize: Style.fontSizeXL * scaling + font.weight: Style.fontWeightBold } + NText { text: weatherReady ? `(${Location.data.weather.timezone_abbreviation})` : "" font.pointSize: Style.fontSizeSmall * scaling visible: Location.data.weather } } - - NText { - visible: weatherReady - text: { - if (!weatherReady) { - return "" - } - var temp = Location.data.weather.current_weather.temperature - if (Settings.data.location.useFahrenheit) { - temp = Location.celsiusToFahrenheit(temp) - } - temp = Math.round(temp) - return `${temp}°` - } - font.pointSize: Style.fontSizeXL * scaling - font.weight: Style.fontWeightBold - } } } From dab1b9b3897e682e13cf247bf5b43345bbe416ab Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 10:17:39 -0400 Subject: [PATCH 167/394] ProfileCard: uptime is back --- Modules/SidePanel/ProfileCard.qml | 43 +++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/Modules/SidePanel/ProfileCard.qml b/Modules/SidePanel/ProfileCard.qml index 242a5a7..dac0715 100644 --- a/Modules/SidePanel/ProfileCard.qml +++ b/Modules/SidePanel/ProfileCard.qml @@ -1,7 +1,9 @@ import QtQuick +import QtQuick.Effects import QtQuick.Layouts import Quickshell -import QtQuick.Effects +import Quickshell.Io +import Quickshell.Widgets import qs.Services import qs.Widgets @@ -13,6 +15,8 @@ NBox { // Hold a single instance of the Settings window (root is NLoader) property var settingsWindow: null + property string uptimeText: "--" + Layout.fillWidth: true // Height driven by content implicitHeight: content.implicitHeight + Style.marginMedium * 2 * scaling @@ -42,7 +46,7 @@ NBox { font.weight: Style.fontWeightBold } NText { - text: "System Uptime: —" + text: `System Uptime: ${uptimeText}` color: Colors.textSecondary } } @@ -80,4 +84,39 @@ NBox { } } } + + + Timer { + interval: 60000 + repeat: true + running: true + onTriggered: uptimeProcess.running = true + } + + Process { + id: uptimeProcess + command: ["cat", "/proc/uptime"] + running: true + + stdout: StdioCollector { + onStreamFinished: { + var uptimeSeconds = parseFloat(this.text.trim().split(' ')[0]) + + var minutes = Math.floor(uptimeSeconds / 60) % 60 + var hours = Math.floor(uptimeSeconds / 3600) % 24 + var days = Math.floor(uptimeSeconds / 86400) + + // Format the output + if (days > 0) { + uptimeText = days + "d " + hours + "h" + } else if (hours > 0) { + uptimeText = hours + "h" + minutes + "m" + } else { + uptimeText = minutes + "m" + } + + uptimeProcess.running = false + } + } + } } From 7fe87c063365b12ab5a0358cf26c44bffaaaa235 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 10:24:20 -0400 Subject: [PATCH 168/394] Fix illegal Colors.fontXXXX --- Modules/Bar/TrayMenu.qml | 8 ++++---- Widgets/NTextInput.qml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Modules/Bar/TrayMenu.qml b/Modules/Bar/TrayMenu.qml index 6770a45..aaf42e4 100644 --- a/Modules/Bar/TrayMenu.qml +++ b/Modules/Bar/TrayMenu.qml @@ -128,7 +128,7 @@ PopupWindow { color: (modelData?.enabled ?? true) ? (mouseArea.containsMouse ? Colors.onAccent : Colors.textPrimary) : Colors.textDisabled text: modelData?.text ?? "" - font.pointSize: Colors.fontSizeSmall * scaling + font.pointSize: Style.fontSizeSmall * scaling verticalAlignment: Text.AlignVCenter elide: Text.ElideRight } @@ -145,7 +145,7 @@ PopupWindow { Text { text: modelData?.hasChildren ? "menu" : "" font.family: "Material Symbols Outlined" - font.pointSize: Colors.fontSizeSmall * scaling + font.pointSize: Style.fontSizeSmall * scaling verticalAlignment: Text.AlignVCenter visible: modelData?.hasChildren ?? false color: Colors.textPrimary @@ -363,7 +363,7 @@ PopupWindow { Layout.fillWidth: true color: (modelData?.enabled ?? true) ? bg.hoverTextColor : Colors.textDisabled text: modelData?.text ?? "" - font.pointSize: Colors.fontSizeSmall * scaling + font.pointSize: Style.fontSizeSmall * scaling verticalAlignment: Text.AlignVCenter elide: Text.ElideRight } @@ -380,7 +380,7 @@ PopupWindow { NText { text: modelData?.hasChildren ? "\uE5CC" : "" font.family: "Material Symbols Outlined" - font.pointSize: Colors.fontSizeSmall * scaling + font.pointSize: Style.fontSizeSmall * scaling verticalAlignment: Text.AlignVCenter visible: modelData?.hasChildren ?? false } diff --git a/Widgets/NTextInput.qml b/Widgets/NTextInput.qml index bbd70f1..b2e00bf 100644 --- a/Widgets/NTextInput.qml +++ b/Widgets/NTextInput.qml @@ -54,7 +54,7 @@ Item { color: Colors.textPrimary placeholderTextColor: Colors.textSecondary background: null - font.pointSize: Colors.fontSizeSmall * scaling + font.pointSize: Style.fontSizeSmall * scaling onEditingFinished: root.onEditingFinished() // 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. From f8e436fb2dab5bfac89f11875cfd7263233358d7 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 10:40:24 -0400 Subject: [PATCH 169/394] NIconButton: default scale is 1.0, default show border --- Modules/Bar/Bar.qml | 4 ++++ Modules/Bar/WiFi.qml | 3 ++- Modules/Bar/WiFiMenu.qml | 2 ++ Modules/DemoPanel/DemoPanel.qml | 2 -- Modules/Notification/Notification.qml | 2 ++ Modules/SidePanel/ProfileCard.qml | 12 ++++-------- Modules/SidePanel/SidePanel.qml | 5 ----- 7 files changed, 14 insertions(+), 16 deletions(-) diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 31942c1..4db35a7 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -106,6 +106,8 @@ Variants { id: demoPanelToggle icon: "experiment" tooltipText: "Open demo panel" + sizeMultiplier: 0.8 + showBorder: false anchors.verticalCenter: parent.verticalCenter onClicked: function () { demoPanel.isLoaded = !demoPanel.isLoaded @@ -116,6 +118,8 @@ Variants { id: sidePanelToggle icon: "widgets" tooltipText: "Open side panel" + sizeMultiplier: 0.8 + showBorder: false anchors.verticalCenter: parent.verticalCenter onClicked: function () { // Map this button's center to the screen and open the side panel below it diff --git a/Modules/Bar/WiFi.qml b/Modules/Bar/WiFi.qml index b343cd8..e768aa0 100644 --- a/Modules/Bar/WiFi.qml +++ b/Modules/Bar/WiFi.qml @@ -11,7 +11,8 @@ NIconButton { readonly property real scaling: Scaling.scale(screen) readonly property bool wifiEnabled: Settings.data.network.wifiEnabled - + sizeMultiplier: 0.8 + showBorder: false icon: { let connected = false for (const net in network.networks) { diff --git a/Modules/Bar/WiFiMenu.qml b/Modules/Bar/WiFiMenu.qml index 370d16a..dad6c9d 100644 --- a/Modules/Bar/WiFiMenu.qml +++ b/Modules/Bar/WiFiMenu.qml @@ -77,6 +77,7 @@ NLoader { NIconButton { icon: "refresh" + sizeMultiplier: 0.8 onClicked: function () { network.refreshNetworks() } @@ -84,6 +85,7 @@ NLoader { NIconButton { icon: "close" + sizeMultiplier: 0.8 onClicked: function () { wifiPanel.visible = false network.onMenuClosed() diff --git a/Modules/DemoPanel/DemoPanel.qml b/Modules/DemoPanel/DemoPanel.qml index 183291d..af1dbc1 100644 --- a/Modules/DemoPanel/DemoPanel.qml +++ b/Modules/DemoPanel/DemoPanel.qml @@ -82,7 +82,6 @@ NLoader { } NIconButton { icon: "refresh" - sizeMultiplier: 1.0 fontPointSize: Style.fontSizeXL * scaling onClicked: function () { Scaling.overrideEnabled = false @@ -107,7 +106,6 @@ NLoader { NIconButton { id: myIconButton icon: "celebration" - sizeMultiplier: 1.0 fontPointSize: Style.fontSizeXL * scaling } diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index 1a67b03..0cf6017 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -185,6 +185,8 @@ PanelWindow { } NIconButton { + sizeMultiplier: 0.8 + showBorder: false anchors.top: parent.top anchors.right: parent.right anchors.margins: Style.marginSmall * scaling diff --git a/Modules/SidePanel/ProfileCard.qml b/Modules/SidePanel/ProfileCard.qml index dac0715..ee4b1c6 100644 --- a/Modules/SidePanel/ProfileCard.qml +++ b/Modules/SidePanel/ProfileCard.qml @@ -59,7 +59,6 @@ NBox { } NIconButton { icon: "settings" - sizeMultiplier: 0.9 onClicked: function () { if (!root.settingsWindow) { const comp = Qt.createComponent("../Settings/SettingsWindow.qml") @@ -80,17 +79,15 @@ NBox { } NIconButton { icon: "power_settings_new" - sizeMultiplier: 0.9 } } } - Timer { - interval: 60000 - repeat: true - running: true - onTriggered: uptimeProcess.running = true + interval: 60000 + repeat: true + running: true + onTriggered: uptimeProcess.running = true } Process { @@ -101,7 +98,6 @@ NBox { stdout: StdioCollector { onStreamFinished: { var uptimeSeconds = parseFloat(this.text.trim().split(' ')[0]) - var minutes = Math.floor(uptimeSeconds / 60) % 60 var hours = Math.floor(uptimeSeconds / 3600) % 24 var days = Math.floor(uptimeSeconds / 86400) diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml index 10d911f..3a25894 100644 --- a/Modules/SidePanel/SidePanel.qml +++ b/Modules/SidePanel/SidePanel.qml @@ -138,19 +138,16 @@ NLoader { // Performance NIconButton { icon: "speed" - sizeMultiplier: 1.0 onClicked: function () {/* TODO: hook to power profile */ } } // Balanced NIconButton { icon: "balance" - sizeMultiplier: 1.0 onClicked: function () {/* TODO: hook to power profile */ } } // Eco NIconButton { icon: "eco" - sizeMultiplier: 1.0 onClicked: function () {/* TODO: hook to power profile */ } } Item { @@ -175,12 +172,10 @@ NLoader { // Record NIconButton { icon: "fiber_manual_record" - sizeMultiplier: 1.0 } // Wallpaper NIconButton { icon: "image" - sizeMultiplier: 1.0 } Item { Layout.fillWidth: true From 3c333c5d8963cfe5992506c4d0183526c9e01a00 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 10:40:32 -0400 Subject: [PATCH 170/394] NIconButton leftover --- Widgets/NIconButton.qml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml index 2f3b964..d5939a9 100644 --- a/Widgets/NIconButton.qml +++ b/Widgets/NIconButton.qml @@ -8,10 +8,11 @@ Rectangle { readonly property real scaling: Scaling.scale(screen) // Multiplier to control how large the button container is relative to Style.baseWidgetSize - property real sizeMultiplier: 0.8 + property real sizeMultiplier: 1.0 property real size: Style.baseWidgetSize * sizeMultiplier * scaling property string icon property string tooltipText + property bool showBorder: true property bool enabled: true property bool hovering: false property var onEntered: function () {} @@ -20,20 +21,24 @@ Rectangle { property real fontPointSize: Style.fontSizeMedium property string fontFamily: "Material Symbols Outlined" + implicitWidth: size implicitHeight: size - radius: width * 0.5 color: root.hovering ? Colors.accentPrimary : "transparent" + radius: width * 0.5 + border.color: showBorder ? Colors.accentPrimary : "transparent" + border.width: Math.max(1, Style.borderThin * scaling) NText { anchors.centerIn: parent - anchors.horizontalCenterOffset: 0 + // Little hack to keep things centered at high scaling + anchors.horizontalCenterOffset: -1 * (scaling - 1.0) anchors.verticalCenterOffset: 0 text: root.icon font.family: fontFamily font.pointSize: root.fontPointSize * scaling - color: root.hovering ? Colors.onAccent : Colors.textPrimary + color: root.hovering ? Colors.onAccent : showBorder ? Colors.accentPrimary : Colors.textPrimary horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter opacity: root.enabled ? Style.opacityFull : Style.opacityMedium From 986eac179efe986aad7c8f41acf2c35c88857f77 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 10:43:46 -0400 Subject: [PATCH 171/394] added NTextInput into DemoPanel - minor display issues with scaling in there --- Modules/DemoPanel/DemoPanel.qml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Modules/DemoPanel/DemoPanel.qml b/Modules/DemoPanel/DemoPanel.qml index af1dbc1..66b55fc 100644 --- a/Modules/DemoPanel/DemoPanel.qml +++ b/Modules/DemoPanel/DemoPanel.qml @@ -159,6 +159,28 @@ NLoader { } } + // NTextInput + ColumnLayout { + spacing: Style.marginMedium * scaling + NText { + text: "NTextInput" + color: Colors.accentSecondary + font.weight: Style.fontWeightBold + } + + NTextInput { + text: "Type anything" + Layout.fillWidth: true + onEditingFinished: function () { + } + + NDivider { + Layout.fillWidth: true + } + } + + } + // NBusyIndicator ColumnLayout { spacing: Style.marginMedium * scaling From a11e310580938272ffdbe299bea8d93e5b90de59 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 10:53:34 -0400 Subject: [PATCH 172/394] SidePanel cards in a Cards directory --- Modules/SidePanel/{ => Cards}/MediaCard.qml | 0 Modules/SidePanel/{ => Cards}/ProfileCard.qml | 0 Modules/SidePanel/{ => Cards}/SystemCard.qml | 0 Modules/SidePanel/{ => Cards}/WeatherCard.qml | 0 Modules/SidePanel/SidePanel.qml | 1 + 5 files changed, 1 insertion(+) rename Modules/SidePanel/{ => Cards}/MediaCard.qml (100%) rename Modules/SidePanel/{ => Cards}/ProfileCard.qml (100%) rename Modules/SidePanel/{ => Cards}/SystemCard.qml (100%) rename Modules/SidePanel/{ => Cards}/WeatherCard.qml (100%) diff --git a/Modules/SidePanel/MediaCard.qml b/Modules/SidePanel/Cards/MediaCard.qml similarity index 100% rename from Modules/SidePanel/MediaCard.qml rename to Modules/SidePanel/Cards/MediaCard.qml diff --git a/Modules/SidePanel/ProfileCard.qml b/Modules/SidePanel/Cards/ProfileCard.qml similarity index 100% rename from Modules/SidePanel/ProfileCard.qml rename to Modules/SidePanel/Cards/ProfileCard.qml diff --git a/Modules/SidePanel/SystemCard.qml b/Modules/SidePanel/Cards/SystemCard.qml similarity index 100% rename from Modules/SidePanel/SystemCard.qml rename to Modules/SidePanel/Cards/SystemCard.qml diff --git a/Modules/SidePanel/WeatherCard.qml b/Modules/SidePanel/Cards/WeatherCard.qml similarity index 100% rename from Modules/SidePanel/WeatherCard.qml rename to Modules/SidePanel/Cards/WeatherCard.qml diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml index 3a25894..092c700 100644 --- a/Modules/SidePanel/SidePanel.qml +++ b/Modules/SidePanel/SidePanel.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import Quickshell import Quickshell.Wayland +import qs.Modules.SidePanel.Cards import qs.Services import qs.Widgets From bfe70e8f46fe059a3d2514f8b288b7510072a49d Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 10:57:57 -0400 Subject: [PATCH 173/394] SidePanel: added PowerProfileCard and UtilitiesCard --- Modules/Bar/WiFi.qml | 4 +- Modules/DemoPanel/DemoPanel.qml | 12 ++-- Modules/Notification/Notification.qml | 2 +- Modules/SidePanel/Cards/PowerProfileCard.qml | 40 +++++++++++++ Modules/SidePanel/Cards/UtilitiesCard.qml | 34 +++++++++++ Modules/SidePanel/SidePanel.qml | 63 +------------------- 6 files changed, 85 insertions(+), 70 deletions(-) create mode 100644 Modules/SidePanel/Cards/PowerProfileCard.qml create mode 100644 Modules/SidePanel/Cards/UtilitiesCard.qml diff --git a/Modules/Bar/WiFi.qml b/Modules/Bar/WiFi.qml index e768aa0..059736f 100644 --- a/Modules/Bar/WiFi.qml +++ b/Modules/Bar/WiFi.qml @@ -11,8 +11,8 @@ NIconButton { readonly property real scaling: Scaling.scale(screen) readonly property bool wifiEnabled: Settings.data.network.wifiEnabled - sizeMultiplier: 0.8 - showBorder: false + sizeMultiplier: 0.8 + showBorder: false icon: { let connected = false for (const net in network.networks) { diff --git a/Modules/DemoPanel/DemoPanel.qml b/Modules/DemoPanel/DemoPanel.qml index 66b55fc..634ebda 100644 --- a/Modules/DemoPanel/DemoPanel.qml +++ b/Modules/DemoPanel/DemoPanel.qml @@ -168,19 +168,17 @@ NLoader { font.weight: Style.fontWeightBold } - NTextInput { + NTextInput { text: "Type anything" Layout.fillWidth: true - onEditingFinished: function () { - } + onEditingFinished: function () {} - NDivider { - Layout.fillWidth: true + NDivider { + Layout.fillWidth: true + } } } - } - // NBusyIndicator ColumnLayout { spacing: Style.marginMedium * scaling diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index 0cf6017..7adaf9c 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -185,7 +185,7 @@ PanelWindow { } NIconButton { - sizeMultiplier: 0.8 + sizeMultiplier: 0.8 showBorder: false anchors.top: parent.top anchors.right: parent.right diff --git a/Modules/SidePanel/Cards/PowerProfileCard.qml b/Modules/SidePanel/Cards/PowerProfileCard.qml new file mode 100644 index 0000000..6fd612d --- /dev/null +++ b/Modules/SidePanel/Cards/PowerProfileCard.qml @@ -0,0 +1,40 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Services +import qs.Widgets + +// Power Profiles: performance, balanced, eco +NBox { + Layout.fillWidth: true + Layout.preferredWidth: 1 + implicitHeight: powerRow.implicitHeight + Style.marginMedium * 2 * scaling + RowLayout { + id: powerRow + anchors.fill: parent + anchors.margins: Style.marginSmall * scaling + spacing: sidePanel.cardSpacing + Item { + Layout.fillWidth: true + } + // Performance + NIconButton { + icon: "speed" + onClicked: function () {/* TODO: hook to power profile */ } + } + // Balanced + NIconButton { + icon: "balance" + onClicked: function () {/* TODO: hook to power profile */ } + } + // Eco + NIconButton { + icon: "eco" + onClicked: function () {/* TODO: hook to power profile */ } + } + Item { + Layout.fillWidth: true + } + } +} diff --git a/Modules/SidePanel/Cards/UtilitiesCard.qml b/Modules/SidePanel/Cards/UtilitiesCard.qml new file mode 100644 index 0000000..6800432 --- /dev/null +++ b/Modules/SidePanel/Cards/UtilitiesCard.qml @@ -0,0 +1,34 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Services +import qs.Widgets + +// Utilities: record & wallpaper + NBox { + Layout.fillWidth: true + Layout.preferredWidth: 1 + implicitHeight: utilRow.implicitHeight + Style.marginMedium * 2 * scaling + RowLayout { + id: utilRow + anchors.fill: parent + anchors.margins: Style.marginSmall * scaling + spacing: sidePanel.cardSpacing + Item { + Layout.fillWidth: true + } + // Record + NIconButton { + icon: "fiber_manual_record" + } + // Wallpaper + NIconButton { + icon: "image" + } + Item { + Layout.fillWidth: true + } + } + } + \ No newline at end of file diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml index 092c700..70a6eff 100644 --- a/Modules/SidePanel/SidePanel.qml +++ b/Modules/SidePanel/SidePanel.qml @@ -2,7 +2,6 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell -import Quickshell.Wayland import qs.Modules.SidePanel.Cards import qs.Services import qs.Widgets @@ -123,66 +122,10 @@ NLoader { Layout.bottomMargin: 0 spacing: sidePanel.cardSpacing - // Power Profiles: performance, balanced, eco - NBox { - Layout.fillWidth: true - Layout.preferredWidth: 1 - implicitHeight: powerRow.implicitHeight + Style.marginSmall * 2 * scaling - RowLayout { - id: powerRow - anchors.fill: parent - anchors.margins: Style.marginSmall * scaling - spacing: sidePanel.cardSpacing - Item { - Layout.fillWidth: true - } - // Performance - NIconButton { - icon: "speed" - onClicked: function () {/* TODO: hook to power profile */ } - } - // Balanced - NIconButton { - icon: "balance" - onClicked: function () {/* TODO: hook to power profile */ } - } - // Eco - NIconButton { - icon: "eco" - onClicked: function () {/* TODO: hook to power profile */ } - } - Item { - Layout.fillWidth: true - } - } - } + PowerProfileCard {} + + UtilitiesCard {} - // Utilities: record & wallpaper - NBox { - Layout.fillWidth: true - Layout.preferredWidth: 1 - implicitHeight: utilRow.implicitHeight + Style.marginSmall * 2 * scaling - RowLayout { - id: utilRow - anchors.fill: parent - anchors.margins: Style.marginSmall * scaling - spacing: sidePanel.cardSpacing - Item { - Layout.fillWidth: true - } - // Record - NIconButton { - icon: "fiber_manual_record" - } - // Wallpaper - NIconButton { - icon: "image" - } - Item { - Layout.fillWidth: true - } - } - } } } } From 0987fdee8e601a7afc342b86cad87d3592029702 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 10:58:01 -0400 Subject: [PATCH 174/394] Formatting --- Widgets/NIconButton.qml | 1 - 1 file changed, 1 deletion(-) diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml index d5939a9..9db3f1e 100644 --- a/Widgets/NIconButton.qml +++ b/Widgets/NIconButton.qml @@ -21,7 +21,6 @@ Rectangle { property real fontPointSize: Style.fontSizeMedium property string fontFamily: "Material Symbols Outlined" - implicitWidth: size implicitHeight: size From 82d7e29213b3ecddc53b0ba453441d5a34c1b721 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 10:58:13 -0400 Subject: [PATCH 175/394] Formatting --- Modules/SidePanel/Cards/UtilitiesCard.qml | 51 +++++++++++------------ Modules/SidePanel/SidePanel.qml | 1 - 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/Modules/SidePanel/Cards/UtilitiesCard.qml b/Modules/SidePanel/Cards/UtilitiesCard.qml index 6800432..6f04815 100644 --- a/Modules/SidePanel/Cards/UtilitiesCard.qml +++ b/Modules/SidePanel/Cards/UtilitiesCard.qml @@ -6,29 +6,28 @@ import qs.Services import qs.Widgets // Utilities: record & wallpaper - NBox { - Layout.fillWidth: true - Layout.preferredWidth: 1 - implicitHeight: utilRow.implicitHeight + Style.marginMedium * 2 * scaling - RowLayout { - id: utilRow - anchors.fill: parent - anchors.margins: Style.marginSmall * scaling - spacing: sidePanel.cardSpacing - Item { - Layout.fillWidth: true - } - // Record - NIconButton { - icon: "fiber_manual_record" - } - // Wallpaper - NIconButton { - icon: "image" - } - Item { - Layout.fillWidth: true - } - } - } - \ No newline at end of file +NBox { + Layout.fillWidth: true + Layout.preferredWidth: 1 + implicitHeight: utilRow.implicitHeight + Style.marginMedium * 2 * scaling + RowLayout { + id: utilRow + anchors.fill: parent + anchors.margins: Style.marginSmall * scaling + spacing: sidePanel.cardSpacing + Item { + Layout.fillWidth: true + } + // Record + NIconButton { + icon: "fiber_manual_record" + } + // Wallpaper + NIconButton { + icon: "image" + } + Item { + Layout.fillWidth: true + } + } +} diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml index 70a6eff..c8936d8 100644 --- a/Modules/SidePanel/SidePanel.qml +++ b/Modules/SidePanel/SidePanel.qml @@ -125,7 +125,6 @@ NLoader { PowerProfileCard {} UtilitiesCard {} - } } } From 6cbc74facad3bb6d9c9ec05694487b46bbff9e89 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 11:13:38 -0400 Subject: [PATCH 176/394] Fix settings not opening --- Modules/SidePanel/Cards/ProfileCard.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/SidePanel/Cards/ProfileCard.qml b/Modules/SidePanel/Cards/ProfileCard.qml index ee4b1c6..9d7f638 100644 --- a/Modules/SidePanel/Cards/ProfileCard.qml +++ b/Modules/SidePanel/Cards/ProfileCard.qml @@ -61,7 +61,7 @@ NBox { icon: "settings" onClicked: function () { if (!root.settingsWindow) { - const comp = Qt.createComponent("../Settings/SettingsWindow.qml") + const comp = Qt.createComponent("../../Settings/SettingsWindow.qml") if (comp.status === Component.Ready) { root.settingsWindow = comp.createObject(root) } else { From 1c77eb7afc9e6ce81ade8bbdcfa12df9d5566424 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 11:13:52 -0400 Subject: [PATCH 177/394] Fix laggy sliders --- Modules/Settings/Tabs/ScreenRecorder.qml | 2 +- Modules/Settings/Tabs/Wallpaper.qml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Modules/Settings/Tabs/ScreenRecorder.qml b/Modules/Settings/Tabs/ScreenRecorder.qml index 39e1541..e2017ea 100644 --- a/Modules/Settings/Tabs/ScreenRecorder.qml +++ b/Modules/Settings/Tabs/ScreenRecorder.qml @@ -65,7 +65,7 @@ Item { to: 144 stepSize: 1 value: Settings.data.screenRecorder.frameRate - onMoved: Settings.data.screenRecorder.frameRate = Math.round(value) + onPressedChanged: Settings.data.screenRecorder.frameRate = Math.round(value) cutoutColor: Colors.surface } diff --git a/Modules/Settings/Tabs/Wallpaper.qml b/Modules/Settings/Tabs/Wallpaper.qml index 0a718b5..aa474b8 100644 --- a/Modules/Settings/Tabs/Wallpaper.qml +++ b/Modules/Settings/Tabs/Wallpaper.qml @@ -94,7 +94,7 @@ Item { to: 900 stepSize: 10 value: Settings.data.wallpaper.randomInterval - onMoved: Settings.data.wallpaper.randomInterval = Math.round(value) + onPressedChanged: Settings.data.wallpaper.randomInterval = Math.round(value) cutoutColor: Colors.backgroundPrimary } @@ -178,7 +178,7 @@ Item { to: 500 stepSize: 5 value: Settings.data.wallpaper.swww.transitionFps - onMoved: Settings.data.wallpaper.swww.transitionFps = Math.round(value) + onPressedChanged: Settings.data.wallpaper.swww.transitionFps = Math.round(value) cutoutColor: Colors.backgroundPrimary } @@ -203,7 +203,7 @@ Item { to: 10 stepSize: 0.05 value: Settings.data.wallpaper.swww.transitionDuration - onMoved: Settings.data.wallpaper.swww.transitionDuration = value + onPressedChanged: Settings.data.wallpaper.swww.transitionDuration = value cutoutColor: Colors.backgroundPrimary } } From 4753ed4d247a021df2f9d1aecba165e8dbc08254 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 11:14:04 -0400 Subject: [PATCH 178/394] semi bold icons --- Widgets/NIconButton.qml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml index 9db3f1e..825eadb 100644 --- a/Widgets/NIconButton.qml +++ b/Widgets/NIconButton.qml @@ -37,10 +37,14 @@ Rectangle { text: root.icon font.family: fontFamily font.pointSize: root.fontPointSize * scaling + font.variableAxes: { + "wght": (Font.Normal + Font.Bold) / 2.0 + } color: root.hovering ? Colors.onAccent : showBorder ? Colors.accentPrimary : Colors.textPrimary horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter opacity: root.enabled ? Style.opacityFull : Style.opacityMedium + } NTooltip { From eff47f504fc5290e4d19ebfd1c7c34d1dba8eadd Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 11:14:12 -0400 Subject: [PATCH 179/394] Better video recorder icon (matches settings) --- Modules/SidePanel/Cards/UtilitiesCard.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/SidePanel/Cards/UtilitiesCard.qml b/Modules/SidePanel/Cards/UtilitiesCard.qml index 6f04815..ec1126f 100644 --- a/Modules/SidePanel/Cards/UtilitiesCard.qml +++ b/Modules/SidePanel/Cards/UtilitiesCard.qml @@ -20,7 +20,7 @@ NBox { } // Record NIconButton { - icon: "fiber_manual_record" + icon: "videocam" } // Wallpaper NIconButton { From 8155ef20eb913fce26d353be97ebb12f9f9b43fc Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Tue, 12 Aug 2025 17:26:23 +0200 Subject: [PATCH 180/394] Add WallpaperSelector, add some scrolling in SettingsWindow --- Modules/Background/Background.qml | 13 +- Modules/Background/Overview.qml | 4 +- Modules/Settings/SettingsWindow.qml | 9 +- Modules/Settings/Tabs/Bar.qml | 152 +++--- Modules/Settings/Tabs/Display.qml | 2 +- Modules/Settings/Tabs/General.qml | 214 ++++---- Modules/Settings/Tabs/Misc.qml | 89 ++-- Modules/Settings/Tabs/Network.qml | 106 ++-- Modules/Settings/Tabs/ScreenRecorder.qml | 475 +++++++++++------- Modules/Settings/Tabs/TimeWeather.qml | 202 +++++--- Modules/Settings/Tabs/Wallpaper.qml | 512 +++++++++++++------- Modules/Settings/Tabs/WallpaperSelector.qml | 270 +++++++++++ Services/Settings.qml | 18 +- Services/Wallpapers.qml | 59 ++- 14 files changed, 1451 insertions(+), 674 deletions(-) create mode 100644 Modules/Settings/Tabs/WallpaperSelector.qml diff --git a/Modules/Background/Background.qml b/Modules/Background/Background.qml index a48f660..20752c9 100644 --- a/Modules/Background/Background.qml +++ b/Modules/Background/Background.qml @@ -8,9 +8,18 @@ Variants { delegate: PanelWindow { required property ShellScreen modelData - property string wallpaperSource: Settings.data.wallpaper.current + property string wallpaperSource: Wallpapers.currentWallpaper !== "" && !Settings.data.wallpaper.swww.enabled ? Wallpapers.currentWallpaper : "" - visible: wallpaperSource !== "" + visible: wallpaperSource !== "" && !Settings.data.wallpaper.swww.enabled + + // Force update when SWWW setting changes + onVisibleChanged: { + if (visible) { + console.log("Background: Showing wallpaper:", wallpaperSource) + } else { + console.log("Background: Hiding wallpaper (SWWW enabled)") + } + } color: "transparent" screen: modelData WlrLayershell.layer: WlrLayer.Background diff --git a/Modules/Background/Overview.qml b/Modules/Background/Overview.qml index 9dc5f6f..d2a7aad 100644 --- a/Modules/Background/Overview.qml +++ b/Modules/Background/Overview.qml @@ -9,9 +9,9 @@ Variants { delegate: PanelWindow { required property ShellScreen modelData - property string wallpaperSource: Qt.resolvedUrl("../../Assets/Tests/wallpaper.png") + property string wallpaperSource: Wallpapers.currentWallpaper !== "" && !Settings.data.wallpaper.swww.enabled ? Wallpapers.currentWallpaper : "" - visible: wallpaperSource !== "" + visible: wallpaperSource !== "" && !Settings.data.wallpaper.swww.enabled color: "transparent" screen: modelData WlrLayershell.layer: WlrLayer.Background diff --git a/Modules/Settings/SettingsWindow.qml b/Modules/Settings/SettingsWindow.qml index 359d221..c05e32b 100644 --- a/Modules/Settings/SettingsWindow.qml +++ b/Modules/Settings/SettingsWindow.qml @@ -46,7 +46,13 @@ NLoader { "label": "Wallpaper", "icon": "image", "source": "Tabs/Wallpaper.qml" - }, { + }, + { + "label": "Wallpaper Selector", + "icon": "wallpaper_slideshow", + "source": "Tabs/WallpaperSelector.qml" + }, + { "label": "Misc", "icon": "more_horiz", "source": "Tabs/Misc.qml" @@ -207,6 +213,7 @@ NLoader { Tabs.Network {} Tabs.Display {} Tabs.Wallpaper {} + Tabs.WallpaperSelector {} Tabs.Misc {} Tabs.About {} } diff --git a/Modules/Settings/Tabs/Bar.qml b/Modules/Settings/Tabs/Bar.qml index a44d0fd..b9b4406 100644 --- a/Modules/Settings/Tabs/Bar.qml +++ b/Modules/Settings/Tabs/Bar.qml @@ -1,75 +1,105 @@ import QtQuick +import QtQuick.Controls import QtQuick.Layouts import qs.Services import qs.Widgets -Item { - // Optional scaling prop to match other tabs - property real scaling: 1 - // Tab metadata - readonly property string tabIcon: "web_asset" - readonly property string tabLabel: "Bar" - readonly property int tabIndex: 1 - Layout.fillWidth: true - Layout.fillHeight: true +ColumnLayout { + id: root - ColumnLayout { - anchors.fill: parent - spacing: Style.marginMedium * scaling + spacing: 0 - NText { - text: "Elements" - font.weight: Style.fontWeightBold - color: Colors.accentSecondary - } + ScrollView { + id: scrollView - NToggle { - label: "Show Active Window" - description: "Display the title of the currently focused window below the bar" - value: Settings.data.bar.showActiveWindow - onToggled: function (newValue) { - Settings.data.bar.showActiveWindow = newValue + Layout.fillWidth: true + Layout.fillHeight: true + padding: 16 + rightPadding: 12 + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + + ColumnLayout { + width: scrollView.availableWidth + spacing: 0 + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 0 } - } - NToggle { - label: "Show Active Window Icon" - description: "Display the icon of the currently focused window" - value: Settings.data.bar.showActiveWindowIcon - onToggled: function (newValue) { - Settings.data.bar.showActiveWindowIcon = newValue + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + NText { + text: "Bar Settings" + font.pointSize: 18 + font.weight: Style.fontWeightBold + color: Colors.textPrimary + Layout.bottomMargin: 8 + } + + // Elements section + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + NText { + text: "Elements" + font.pointSize: 13 + font.weight: Style.fontWeightBold + color: Colors.textPrimary + } + + NToggle { + label: "Show Active Window" + description: "Display the title of the currently focused window below the bar" + value: Settings.data.bar.showActiveWindow + onToggled: function (newValue) { + Settings.data.bar.showActiveWindow = newValue + } + } + + NToggle { + label: "Show Active Window Icon" + description: "Display the icon of the currently focused window" + value: Settings.data.bar.showActiveWindowIcon + onToggled: function (newValue) { + Settings.data.bar.showActiveWindowIcon = newValue + } + } + + NToggle { + label: "Show System Info" + description: "Display system information (CPU, RAM, Temperature)" + value: Settings.data.bar.showSystemInfo + onToggled: function (newValue) { + Settings.data.bar.showSystemInfo = newValue + } + } + + NToggle { + label: "Show Taskbar" + description: "Display a taskbar showing currently open windows" + value: Settings.data.bar.showTaskbar + onToggled: function (newValue) { + Settings.data.bar.showTaskbar = newValue + } + } + + NToggle { + label: "Show Media" + description: "Display media controls and information" + value: Settings.data.bar.showMedia + onToggled: function (newValue) { + Settings.data.bar.showMedia = newValue + } + } + } } } - - NToggle { - label: "Show System Info" - description: "Display system information (CPU, RAM, Temperature)" - value: Settings.data.bar.showSystemInfo - onToggled: function (newValue) { - Settings.data.bar.showSystemInfo = newValue - } - } - - NToggle { - label: "Show Taskbar" - description: "Display a taskbar showing currently open windows" - value: Settings.data.bar.showTaskbar - onToggled: function (newValue) { - Settings.data.bar.showTaskbar = newValue - } - } - - NToggle { - label: "Show Media" - description: "Display media controls and information" - value: Settings.data.bar.showMedia - onToggled: function (newValue) { - Settings.data.bar.showMedia = newValue - } - } - - Item { - Layout.fillHeight: true - } } } diff --git a/Modules/Settings/Tabs/Display.qml b/Modules/Settings/Tabs/Display.qml index ea587d7..86dea3d 100644 --- a/Modules/Settings/Tabs/Display.qml +++ b/Modules/Settings/Tabs/Display.qml @@ -116,4 +116,4 @@ Item { Layout.fillHeight: true } } -} +} \ No newline at end of file diff --git a/Modules/Settings/Tabs/General.qml b/Modules/Settings/Tabs/General.qml index 0202277..2d71032 100644 --- a/Modules/Settings/Tabs/General.qml +++ b/Modules/Settings/Tabs/General.qml @@ -1,113 +1,143 @@ import QtQuick +import QtQuick.Controls import QtQuick.Layouts import qs.Services import qs.Widgets -Item { - id: generalPage +ColumnLayout { + id: root - // Public API - // Scaling factor provided by the parent settings window - property real scaling: 1 - // Tab metadata - readonly property string tabIcon: "tune" - readonly property string tabLabel: "General" - readonly property int tabIndex: 0 + spacing: 0 - Layout.fillWidth: true - Layout.fillHeight: true + ScrollView { + id: scrollView - ColumnLayout { - anchors.fill: parent - anchors.margins: 0 - spacing: Style.marginMedium * scaling + Layout.fillWidth: true + Layout.fillHeight: true + padding: 16 + rightPadding: 12 + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded - // Profile section - NText { - text: "Profile" - font.weight: Style.fontWeightBold - color: Colors.accentSecondary - } + ColumnLayout { + width: scrollView.availableWidth + spacing: 0 - RowLayout { - Layout.fillWidth: true - spacing: Style.marginMedium * scaling - - // Avatar preview - NImageRounded { - width: 64 * scaling - height: 64 * scaling - imagePath: Settings.data.general.avatarImage - fallbackIcon: "person" - borderColor: Colors.accentPrimary - borderWidth: Math.max(1, Style.borderMedium * scaling) - } - ColumnLayout { + Item { Layout.fillWidth: true - spacing: 2 * scaling + Layout.preferredHeight: 0 + } + + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + NText { - text: "Profile Image" - color: Colors.textPrimary + text: "General Settings" + font.pointSize: 18 font.weight: Style.fontWeightBold + color: Colors.textPrimary + Layout.bottomMargin: 8 } - NText { - text: "Your profile picture displayed in various places throughout the shell" - color: Colors.textSecondary - font.pointSize: Style.fontSizeSmall * scaling - } - NTextInput { - text: Settings.data.general.avatarImage - placeholderText: "/home/user/.face" + + // Profile section + ColumnLayout { + spacing: 8 Layout.fillWidth: true - onEditingFinished: function () { - Settings.data.general.avatarImage = text + Layout.topMargin: 8 + + NText { + text: "Profile" + font.pointSize: 13 + font.weight: Style.fontWeightBold + color: Colors.textPrimary + } + + RowLayout { + Layout.fillWidth: true + spacing: 16 + + // Avatar preview + NImageRounded { + width: 64 + height: 64 + imagePath: Settings.data.general.avatarImage + fallbackIcon: "person" + borderColor: Colors.accentPrimary + borderWidth: Math.max(1, Style.borderMedium) + } + ColumnLayout { + Layout.fillWidth: true + spacing: 4 + NText { + text: "Profile Image" + color: Colors.textPrimary + font.weight: Style.fontWeightBold + } + NText { + text: "Your profile picture displayed in various places throughout the shell" + color: Colors.textSecondary + font.pointSize: 12 + } + NTextInput { + text: Settings.data.general.avatarImage + placeholderText: "/home/user/.face" + Layout.fillWidth: true + onEditingFinished: function () { + Settings.data.general.avatarImage = text + } + } + } + } + } + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: 26 + Layout.bottomMargin: 18 + } + + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + NText { + text: "User Interface" + font.pointSize: 18 + font.weight: Style.fontWeightBold + color: Colors.textPrimary + Layout.bottomMargin: 8 + } + + NToggle { + label: "Show Corners" + description: "Display rounded corners on the edge of the screen" + value: Settings.data.general.showScreenCorners + onToggled: function (v) { + Settings.data.general.showScreenCorners = v + } + } + + NToggle { + label: "Show Dock" + description: "Display a dock at the bottom of the screen for quick access to applications" + value: Settings.data.general.showDock + onToggled: function (v) { + Settings.data.general.showDock = v + } + } + + NToggle { + label: "Dim Desktop" + description: "Dim the desktop when panels or menus are open" + value: Settings.data.general.dimDesktop + onToggled: function (v) { + Settings.data.general.dimDesktop = v } } } } - - NDivider { - Layout.fillWidth: true - Layout.topMargin: Style.marginSmall * scaling - Layout.bottomMargin: Style.marginSmall * scaling - } - - // UI section - NText { - text: "User Interface" - font.weight: Style.fontWeightBold - color: Colors.accentSecondary - } - - NToggle { - label: "Show Corners" - description: "Display rounded corners on the edge of the screen" - value: Settings.data.general.showScreenCorners - onToggled: function (v) { - Settings.data.general.showScreenCorners = v - } - } - - NToggle { - label: "Show Dock" - description: "Display a dock at the bottom of the screen for quick access to applications" - value: Settings.data.general.showDock - onToggled: function (v) { - Settings.data.general.showDock = v - } - } - - NToggle { - label: "Dim Desktop" - description: "Dim the desktop when panels or menus are open" - value: Settings.data.general.dimDesktop - onToggled: function (v) { - Settings.data.general.dimDesktop = v - } - } - - Item { - Layout.fillHeight: true - } } } diff --git a/Modules/Settings/Tabs/Misc.qml b/Modules/Settings/Tabs/Misc.qml index 2a6dc6a..f99cdfa 100644 --- a/Modules/Settings/Tabs/Misc.qml +++ b/Modules/Settings/Tabs/Misc.qml @@ -1,48 +1,69 @@ import QtQuick +import QtQuick.Controls import QtQuick.Layouts import qs.Services import qs.Widgets -Item { - property real scaling: 1 - readonly property string tabIcon: "more_horiz" - readonly property string tabLabel: "Misc" - readonly property int tabIndex: 7 - Layout.fillWidth: true - Layout.fillHeight: true +ColumnLayout { + id: root - ColumnLayout { - anchors.fill: parent - spacing: Style.marginMedium * scaling + spacing: 0 - NText { - text: "Media" - font.weight: Style.fontWeightBold - color: Colors.accentSecondary - } + ScrollView { + id: scrollView - NText { - text: "Visualizer Type" - color: Colors.textPrimary - font.weight: Style.fontWeightBold - } - NText { - text: "Choose the style of the audio visualizer" - color: Colors.textSecondary - } + Layout.fillWidth: true + Layout.fillHeight: true + padding: 16 + rightPadding: 12 + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded - NComboBox { - id: visualizerTypeComboBox - optionsKeys: ["radial", "fire", "diamond"] - optionsLabels: ["Radial", "Fire", "Diamond"] - currentKey: Settings.data.audioVisualizer.type - onSelected: function (key) { - Settings.data.audioVisualizer.type = key + ColumnLayout { + width: scrollView.availableWidth + spacing: 0 + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 0 } - } - Item { - Layout.fillHeight: true + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + NText { + text: "Miscellaneous Settings" + font.pointSize: 18 + font.weight: Style.fontWeightBold + color: Colors.textPrimary + Layout.bottomMargin: 8 + } + + // Audio Visualizer section + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + NText { + text: "Audio Visualizer" + font.pointSize: 13 + font.weight: Style.fontWeightBold + color: Colors.textPrimary + } + + NComboBox { + optionsKeys: ["radial", "bars", "wave"] + optionsLabels: ["Radial", "Bars", "Wave"] + currentKey: Settings.data.audioVisualizer.type + onSelected: function (key) { + Settings.data.audioVisualizer.type = key + } + } + } + } } } } diff --git a/Modules/Settings/Tabs/Network.qml b/Modules/Settings/Tabs/Network.qml index ad9be23..3c31d74 100644 --- a/Modules/Settings/Tabs/Network.qml +++ b/Modules/Settings/Tabs/Network.qml @@ -1,64 +1,80 @@ import QtQuick +import QtQuick.Controls import QtQuick.Layouts import Quickshell import Quickshell.Bluetooth import qs.Services import qs.Widgets -Item { - property real scaling: 1 - readonly property string tabIcon: "wifi" - readonly property string tabLabel: "Network" - readonly property int tabIndex: 4 - Layout.fillWidth: true - Layout.fillHeight: true +ColumnLayout { + id: root - ColumnLayout { - anchors.fill: parent - spacing: Style.marginMedium * scaling + spacing: 0 - NText { - text: "Wi‑Fi" - font.weight: Style.fontWeightBold - color: Colors.accentSecondary - } + ScrollView { + id: scrollView - NToggle { - label: "Enable Wi‑Fi" - description: "Turn Wi‑Fi radio on or off" - value: Settings.data.network.wifiEnabled - onToggled: function (newValue) { - Settings.data.network.wifiEnabled = newValue - Quickshell.execDetached(["nmcli", "radio", "wifi", newValue ? "on" : "off"]) + Layout.fillWidth: true + Layout.fillHeight: true + padding: 16 + rightPadding: 12 + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + + ColumnLayout { + width: scrollView.availableWidth + spacing: 0 + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 0 } - } - NDivider { - Layout.fillWidth: true - } + ColumnLayout { + spacing: 4 + Layout.fillWidth: true - NText { - text: "Bluetooth" - font.weight: Style.fontWeightBold - color: Colors.accentSecondary - } + NText { + text: "Network Settings" + font.pointSize: 18 + font.weight: Style.fontWeightBold + color: Colors.textPrimary + Layout.bottomMargin: 8 + } - NToggle { - label: "Enable Bluetooth" - description: "Turn Bluetooth radio on or off" - value: Settings.data.network.bluetoothEnabled - onToggled: function (newValue) { - Settings.data.network.bluetoothEnabled = newValue - if (Bluetooth.defaultAdapter) { - Bluetooth.defaultAdapter.enabled = newValue - if (Bluetooth.defaultAdapter.enabled) - Bluetooth.defaultAdapter.discovering = true + // Network interfaces section + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + NText { + text: "Network Interfaces" + font.pointSize: 13 + font.weight: Style.fontWeightBold + color: Colors.textPrimary + } + + NToggle { + label: "WiFi Enabled" + description: "Enable WiFi connectivity" + value: Settings.data.network.wifiEnabled + onToggled: function (newValue) { + Settings.data.network.wifiEnabled = newValue + } + } + + NToggle { + label: "Bluetooth Enabled" + description: "Enable Bluetooth connectivity" + value: Settings.data.network.bluetoothEnabled + onToggled: function (newValue) { + Settings.data.network.bluetoothEnabled = newValue + } + } } } } - - Item { - Layout.fillHeight: true - } } } diff --git a/Modules/Settings/Tabs/ScreenRecorder.qml b/Modules/Settings/Tabs/ScreenRecorder.qml index e2017ea..d102ce9 100644 --- a/Modules/Settings/Tabs/ScreenRecorder.qml +++ b/Modules/Settings/Tabs/ScreenRecorder.qml @@ -1,180 +1,327 @@ import QtQuick +import QtQuick.Controls import QtQuick.Layouts import qs.Services import qs.Widgets -Item { - property real scaling: 1 - readonly property string tabIcon: "videocam" - readonly property string tabLabel: "Screen Recorder" - readonly property int tabIndex: 3 - Layout.fillWidth: true - Layout.fillHeight: true +ColumnLayout { + id: root - ColumnLayout { - anchors.fill: parent - spacing: Style.marginMedium * scaling + spacing: 0 - NText { - text: "Screen Recording" - font.weight: Style.fontWeightBold - color: Colors.accentSecondary - } + ScrollView { + id: scrollView - // Output Directory - NText { - text: "Output Directory" - color: Colors.textPrimary - font.weight: Style.fontWeightBold - } - NText { - text: "Directory where screen recordings will be saved" - color: Colors.textSecondary - } - NTextInput { - text: Settings.data.screenRecorder.directory - Layout.fillWidth: true - onEditingFinished: function () { - Settings.data.screenRecorder.directory = text - } - } + Layout.fillWidth: true + Layout.fillHeight: true + padding: 16 + rightPadding: 12 + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + + ColumnLayout { + width: scrollView.availableWidth + spacing: 0 - // Frame Rate - NText { - text: "Frame Rate" - color: Colors.textPrimary - font.weight: Style.fontWeightBold - } - NText { - text: "Target frame rate for screen recordings (default: 60)" - color: Colors.textSecondary - } - RowLayout { - Layout.fillWidth: true - NText { - text: Settings.data.screenRecorder.frameRate + " FPS" - color: Colors.textPrimary - } Item { Layout.fillWidth: true + Layout.preferredHeight: 0 } - } - NSlider { - Layout.fillWidth: true - from: 24 - to: 144 - stepSize: 1 - value: Settings.data.screenRecorder.frameRate - onPressedChanged: Settings.data.screenRecorder.frameRate = Math.round(value) - cutoutColor: Colors.surface - } - // Audio Source - NText { - text: "Audio Source" - color: Colors.textPrimary - font.weight: Style.fontWeightBold - } - NText { - text: "Audio source to capture during recording" - color: Colors.textSecondary - } - NComboBox { - optionsKeys: ["default_output", "default_input", "both"] - optionsLabels: ["System Audio", "Microphone", "System Audio + Microphone"] - currentKey: Settings.data.screenRecorder.audioSource - onSelected: function (key) { - Settings.data.screenRecorder.audioSource = key + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + NText { + text: "Screen Recording" + font.pointSize: 18 + font.weight: Style.fontWeightBold + color: Colors.textPrimary + Layout.bottomMargin: 8 + } + + // Output Directory + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + NText { + text: "Output Directory" + font.pointSize: 13 + font.weight: Style.fontWeightBold + color: Colors.textPrimary + } + + NText { + text: "Directory where screen recordings will be saved" + font.pointSize: 12 + color: Colors.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + NTextInput { + text: Settings.data.screenRecorder.directory + Layout.fillWidth: true + onEditingFinished: function () { + Settings.data.screenRecorder.directory = text + } + } + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: 26 + Layout.bottomMargin: 18 + } + + // Video Settings + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + NText { + text: "Video Settings" + font.pointSize: 18 + font.weight: Style.fontWeightBold + color: Colors.textPrimary + Layout.bottomMargin: 8 + } + + // Frame Rate + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + NText { + text: "Frame Rate" + font.pointSize: 13 + font.weight: Style.fontWeightBold + color: Colors.textPrimary + } + + NText { + text: "Target frame rate for screen recordings (default: 60)" + font.pointSize: 12 + color: Colors.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + RowLayout { + Layout.fillWidth: true + + NText { + text: Settings.data.screenRecorder.frameRate + " FPS" + font.pointSize: 13 + color: Colors.textPrimary + } + + Item { + Layout.fillWidth: true + } + } + + NSlider { + Layout.fillWidth: true + from: 24 + to: 144 + stepSize: 1 + value: Settings.data.screenRecorder.frameRate + onMoved: Settings.data.screenRecorder.frameRate = Math.round(value) + cutoutColor: Colors.backgroundPrimary + } + } + + // Video Quality + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + NText { + text: "Video Quality" + font.pointSize: 13 + font.weight: Style.fontWeightBold + color: Colors.textPrimary + } + + NText { + text: "Higher quality results in larger file sizes" + font.pointSize: 12 + color: Colors.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + NComboBox { + optionsKeys: ["medium", "high", "very_high", "ultra"] + optionsLabels: ["Medium", "High", "Very High", "Ultra"] + currentKey: Settings.data.screenRecorder.quality + onSelected: function (key) { + Settings.data.screenRecorder.quality = key + } + } + } + + // Video Codec + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + NText { + text: "Video Codec" + font.pointSize: 13 + font.weight: Style.fontWeightBold + color: Colors.textPrimary + } + + NText { + text: "Different codecs offer different compression and compatibility" + font.pointSize: 12 + color: Colors.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + NComboBox { + optionsKeys: ["h264", "hevc", "av1", "vp8", "vp9"] + optionsLabels: ["H264", "HEVC", "AV1", "VP8", "VP9"] + currentKey: Settings.data.screenRecorder.videoCodec + onSelected: function (key) { + Settings.data.screenRecorder.videoCodec = key + } + } + } + + // Color Range + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + NText { + text: "Color Range" + font.pointSize: 13 + font.weight: Style.fontWeightBold + color: Colors.textPrimary + } + + NText { + text: "Limited is recommended for better compatibility" + font.pointSize: 12 + color: Colors.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + NComboBox { + optionsKeys: ["limited", "full"] + optionsLabels: ["Limited", "Full"] + currentKey: Settings.data.screenRecorder.colorRange + onSelected: function (key) { + Settings.data.screenRecorder.colorRange = key + } + } + } + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: 26 + Layout.bottomMargin: 18 + } + + // Audio Settings + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + NText { + text: "Audio Settings" + font.pointSize: 18 + font.weight: Style.fontWeightBold + color: Colors.textPrimary + Layout.bottomMargin: 8 + } + + // Audio Source + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + NText { + text: "Audio Source" + font.pointSize: 13 + font.weight: Style.fontWeightBold + color: Colors.textPrimary + } + + NText { + text: "Audio source to capture during recording" + font.pointSize: 12 + color: Colors.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + NComboBox { + optionsKeys: ["default_output", "default_input", "both"] + optionsLabels: ["System Audio", "Microphone", "System Audio + Microphone"] + currentKey: Settings.data.screenRecorder.audioSource + onSelected: function (key) { + Settings.data.screenRecorder.audioSource = key + } + } + } + + // Audio Codec + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + NText { + text: "Audio Codec" + font.pointSize: 13 + font.weight: Style.fontWeightBold + color: Colors.textPrimary + } + + NText { + text: "Opus is recommended for best performance and smallest audio size" + font.pointSize: 12 + color: Colors.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + NComboBox { + optionsKeys: ["opus", "aac"] + optionsLabels: ["OPUS", "AAC"] + currentKey: Settings.data.screenRecorder.audioCodec + onSelected: function (key) { + Settings.data.screenRecorder.audioCodec = key + } + } + } + + // Show Cursor + NToggle { + label: "Show Cursor" + description: "Record mouse cursor in the video" + value: Settings.data.screenRecorder.showCursor + onToggled: function (newValue) { + Settings.data.screenRecorder.showCursor = newValue + } + } + } } } - - // Video Quality - NText { - text: "Video Quality" - color: Colors.textPrimary - font.weight: Style.fontWeightBold - } - NText { - text: "Higher quality results in larger file sizes" - color: Colors.textSecondary - } - NComboBox { - optionsKeys: ["medium", "high", "very_high", "ultra"] - optionsLabels: ["Medium", "High", "Very High", "Ultra"] - currentKey: Settings.data.screenRecorder.quality - onSelected: function (key) { - Settings.data.screenRecorder.quality = key - } - } - - // Video Codec - NText { - text: "Video Codec" - color: Colors.textPrimary - font.weight: Style.fontWeightBold - } - NText { - text: "Different codecs offer different compression and compatibility" - color: Colors.textSecondary - } - NComboBox { - optionsKeys: ["h264", "hevc", "av1", "vp8", "vp9"] - optionsLabels: ["H264", "HEVC", "AV1", "VP8", "VP9"] - currentKey: Settings.data.screenRecorder.videoCodec - onSelected: function (key) { - Settings.data.screenRecorder.videoCodec = key - } - } - - // Audio Codec - NText { - text: "Audio Codec" - color: Colors.textPrimary - font.weight: Style.fontWeightBold - } - NText { - text: "Opus is recommended for best performance and smallest audio size" - color: Colors.textSecondary - } - NComboBox { - optionsKeys: ["opus", "aac"] - optionsLabels: ["OPUS", "AAC"] - currentKey: Settings.data.screenRecorder.audioCodec - onSelected: function (key) { - Settings.data.screenRecorder.audioCodec = key - } - } - - // Color Range - NText { - text: "Color Range" - color: Colors.textPrimary - font.weight: Style.fontWeightBold - } - NText { - text: "Limited is recommended for better compatibility" - color: Colors.textSecondary - } - NComboBox { - optionsKeys: ["limited", "full"] - optionsLabels: ["Limited", "Full"] - currentKey: Settings.data.screenRecorder.colorRange - onSelected: function (key) { - Settings.data.screenRecorder.colorRange = key - } - } - - NToggle { - label: "Show Cursor" - description: "Record mouse cursor in the video" - value: Settings.data.screenRecorder.showCursor - onToggled: function (newValue) { - Settings.data.screenRecorder.showCursor = newValue - } - } - - Item { - Layout.fillHeight: true - } } -} +} \ No newline at end of file diff --git a/Modules/Settings/Tabs/TimeWeather.qml b/Modules/Settings/Tabs/TimeWeather.qml index f329def..a5f1b1f 100644 --- a/Modules/Settings/Tabs/TimeWeather.qml +++ b/Modules/Settings/Tabs/TimeWeather.qml @@ -1,94 +1,136 @@ import QtQuick +import QtQuick.Controls import QtQuick.Layouts import qs.Services import qs.Widgets -Item { - property real scaling: 1 - readonly property string tabIcon: "schedule" - readonly property string tabLabel: "Time & Weather" - readonly property int tabIndex: 2 - Layout.fillWidth: true - Layout.fillHeight: true +ColumnLayout { + id: root - ColumnLayout { - anchors.fill: parent - spacing: Style.marginMedium * scaling + spacing: 0 - NText { - text: "Time" - font.weight: Style.fontWeightBold - color: Colors.accentSecondary - } + ScrollView { + id: scrollView - NToggle { - label: "Use 12 Hour Clock" - description: "Display time in 12-hour format (e.g., 2:30 PM) instead of 24-hour format" - value: Settings.data.location.use12HourClock - onToggled: function (newValue) { - Settings.data.location.use12HourClock = newValue + Layout.fillWidth: true + Layout.fillHeight: true + padding: 16 + rightPadding: 12 + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + + ColumnLayout { + width: scrollView.availableWidth + spacing: 0 + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 0 } - } - NToggle { - label: "US Style Date" - description: "Display dates in MM/DD/YYYY format instead of DD/MM/YYYY" - value: Settings.data.location.reverseDayMonth - onToggled: function (newValue) { - Settings.data.location.reverseDayMonth = newValue + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + NText { + text: "Time & Weather Settings" + font.pointSize: 18 + font.weight: Style.fontWeightBold + color: Colors.textPrimary + Layout.bottomMargin: 8 + } + + // Location section + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + NText { + text: "Location" + font.pointSize: 13 + font.weight: Style.fontWeightBold + color: Colors.textPrimary + } + + NTextInput { + text: Settings.data.location.name + placeholderText: "Enter city name" + Layout.fillWidth: true + onEditingFinished: function () { + Settings.data.location.name = text + } + } + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: 26 + Layout.bottomMargin: 18 + } + + // Time section + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + NText { + text: "Time Format" + font.pointSize: 18 + font.weight: Style.fontWeightBold + color: Colors.textPrimary + Layout.bottomMargin: 8 + } + + NToggle { + label: "Use 12-Hour Clock" + description: "Display time in 12-hour format (AM/PM) instead of 24-hour" + value: Settings.data.location.use12HourClock + onToggled: function (newValue) { + Settings.data.location.use12HourClock = newValue + } + } + + NToggle { + label: "Reverse Day/Month" + description: "Display date as DD/MM instead of MM/DD" + value: Settings.data.location.reverseDayMonth + onToggled: function (newValue) { + Settings.data.location.reverseDayMonth = newValue + } + } + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: 26 + Layout.bottomMargin: 18 + } + + // Weather section + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + NText { + text: "Weather" + font.pointSize: 18 + font.weight: Style.fontWeightBold + color: Colors.textPrimary + Layout.bottomMargin: 8 + } + + NToggle { + label: "Use Fahrenheit" + description: "Display temperature in Fahrenheit instead of Celsius" + value: Settings.data.location.useFahrenheit + onToggled: function (newValue) { + Settings.data.location.useFahrenheit = newValue + } + } + } } } - - NDivider { - Layout.fillWidth: true - } - - NText { - text: "Weather" - font.weight: Style.fontWeightBold - color: Colors.accentSecondary - } - - NText { - text: "Location" - color: Colors.textPrimary - font.weight: Style.fontWeightBold - } - NText { - text: "Your city name for weather information" - color: Colors.textSecondary - font.pointSize: Style.fontSizeSmall * scaling - } - NTextInput { - text: Settings.data.location.name - Layout.fillWidth: true - onEditingFinished: function () { - Settings.data.location.name = text - Location.resetWeather() - } - } - - NText { - text: "Temperature Unit" - color: Colors.textPrimary - font.weight: Style.fontWeightBold - } - NText { - text: "Choose between Celsius and Fahrenheit" - color: Colors.textSecondary - font.pointSize: Style.fontSizeSmall * scaling - } - NComboBox { - optionsKeys: ["c", "f"] - optionsLabels: ["Celsius", "Fahrenheit"] - currentKey: Settings.data.location.useFahrenheit ? "f" : "c" - onSelected: function (key) { - Settings.data.location.useFahrenheit = (key === "f") - } - } - - Item { - Layout.fillHeight: true - } } } diff --git a/Modules/Settings/Tabs/Wallpaper.qml b/Modules/Settings/Tabs/Wallpaper.qml index aa474b8..d121236 100644 --- a/Modules/Settings/Tabs/Wallpaper.qml +++ b/Modules/Settings/Tabs/Wallpaper.qml @@ -1,215 +1,357 @@ import QtQuick +import QtQuick.Controls import QtQuick.Layouts import qs.Services import qs.Widgets -Item { - property real scaling: 1 - readonly property string tabIcon: "image" - readonly property string tabLabel: "Wallpaper" - readonly property int tabIndex: 6 - Layout.fillWidth: true - Layout.fillHeight: true +ColumnLayout { + id: root - ColumnLayout { - anchors.fill: parent - spacing: Style.marginMedium * scaling + spacing: 0 - NText { - text: "Wallpaper Settings" - font.weight: Style.fontWeightBold - color: Colors.accentSecondary - } + ScrollView { + id: scrollView - // Folder - NText { - text: "Wallpaper Folder" + Layout.fillWidth: true + Layout.fillHeight: true + padding: 16 + rightPadding: 12 + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded - font.weight: Style.fontWeightBold - } - NText { - text: "Path to your wallpaper folder" - color: Colors.textSecondary - wrapMode: Text.WordWrap - } - NTextInput { - text: Settings.data.wallpaper.directory - Layout.fillWidth: true - onEditingFinished: function () { - Settings.data.wallpaper.directory = text - } - } + ColumnLayout { + width: scrollView.availableWidth + spacing: 0 - NDivider { - Layout.fillWidth: true - } - - // ---------------------------- - NText { - text: "Automation" - font.weight: Style.fontWeightBold - color: Colors.accentSecondary - } - - NToggle { - label: "Random Wallpaper" - description: "Automatically select random wallpapers from the folder" - value: Settings.data.wallpaper.isRandom - onToggled: function (newValue) { - Settings.data.wallpaper.isRandom = newValue - } - } - - NToggle { - label: "Use Wallpaper Theme" - description: "Automatically adjust theme colors based on wallpaper" - value: Settings.data.wallpaper.generateTheme - onToggled: function (newValue) { - Settings.data.wallpaper.generateTheme = newValue - } - } - - NText { - text: "Wallpaper Interval" - color: Colors.textPrimary - font.weight: Style.fontWeightBold - } - NText { - text: "How often to change wallpapers automatically (in seconds)" - color: Colors.textSecondary - } - RowLayout { - Layout.fillWidth: true - NText { - text: Settings.data.wallpaper.randomInterval + " seconds" - color: Colors.textPrimary - } Item { Layout.fillWidth: true - } - } - NSlider { - Layout.fillWidth: true - from: 10 - to: 900 - stepSize: 10 - value: Settings.data.wallpaper.randomInterval - onPressedChanged: Settings.data.wallpaper.randomInterval = Math.round(value) - cutoutColor: Colors.backgroundPrimary - } - - NDivider { - Layout.fillWidth: true - } - - NText { - text: "SWWW" - font.weight: Style.fontWeightBold - color: Colors.accentSecondary - } - - NToggle { - label: "Use SWWW" - description: "Use SWWW daemon for advanced wallpaper management" - value: Settings.data.wallpaper.swww.enabled - onToggled: function (newValue) { - Settings.data.wallpaper.swww.enabled = newValue - } - } - - // SWWW settings - ColumnLayout { - spacing: Style.marginSmall * scaling - visible: Settings.data.wallpaper.swww.enabled - - NText { - text: "Resize Mode" - font.weight: Style.fontWeightBold - } - NText { - text: "How SWWW should resize wallpapers to fit the screen" - color: Colors.textSecondary - wrapMode: Text.WordWrap - } - NComboBox { - optionsKeys: ["no", "crop", "fit", "stretch"] - optionsLabels: ["No", "Crop", "Fit", "Stretch"] - currentKey: Settings.data.wallpaper.swww.resizeMethod - onSelected: function (key) { - Settings.data.wallpaper.swww.resizeMethod = key - } + Layout.preferredHeight: 0 } - NText { - text: "Transition Type" - font.weight: Style.fontWeightBold - } - NText { - text: "Animation type when switching between wallpapers" - color: Colors.textSecondary - wrapMode: Text.WordWrap - } - NComboBox { - optionsKeys: ["none", "simple", "fade", "left", "right", "top", "bottom", "wipe", "wave", "grow", "center", "any", "outer", "random"] - optionsLabels: ["None", "Simple", "Fade", "Left", "Right", "Top", "Bottom", "Wipe", "Wave", "Grow", "Center", "Any", "Outer", "Random"] - currentKey: Settings.data.wallpaper.swww.transitionType - onSelected: function (key) { - Settings.data.wallpaper.swww.transitionType = key - } - } - - NText { - text: "Transition FPS" - font.weight: Style.fontWeightBold - } - RowLayout { + ColumnLayout { + spacing: 4 Layout.fillWidth: true + NText { - text: Settings.data.wallpaper.swww.transitionFps + " FPS" + text: "Wallpaper Settings" + font.pointSize: 18 + font.weight: Style.fontWeightBold color: Colors.textPrimary + Layout.bottomMargin: 8 } - Item { + + // Wallpaper Settings Category + ColumnLayout { + spacing: 8 Layout.fillWidth: true + Layout.topMargin: 8 + + // Wallpaper Folder + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + + NText { + text: "Wallpaper Folder" + font.pointSize: 13 + font.weight: Style.fontWeightBold + color: Colors.textPrimary + } + + NText { + text: "Path to your wallpaper folder" + font.pointSize: 12 + color: Colors.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + NTextInput { + text: Settings.data.wallpaper.directory + Layout.fillWidth: true + onEditingFinished: function () { + Settings.data.wallpaper.directory = text + } + } + } } } - NSlider { - Layout.fillWidth: true - from: 30 - to: 500 - stepSize: 5 - value: Settings.data.wallpaper.swww.transitionFps - onPressedChanged: Settings.data.wallpaper.swww.transitionFps = Math.round(value) - cutoutColor: Colors.backgroundPrimary - } - NText { - text: "Transition Duration" - color: Colors.textPrimary - font.weight: Style.fontWeightBold - } - RowLayout { + NDivider { Layout.fillWidth: true + Layout.topMargin: 26 + Layout.bottomMargin: 18 + } + + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + NText { - text: Settings.data.wallpaper.swww.transitionDuration.toFixed(2) + " s" + text: "Automation" + font.pointSize: 18 + font.weight: Style.fontWeightBold color: Colors.textPrimary + Layout.bottomMargin: 8 } - Item { - Layout.fillWidth: true - } - } - NSlider { - Layout.fillWidth: true - from: 0.25 - to: 10 - stepSize: 0.05 - value: Settings.data.wallpaper.swww.transitionDuration - onPressedChanged: Settings.data.wallpaper.swww.transitionDuration = value - cutoutColor: Colors.backgroundPrimary - } - } - Item { - Layout.fillHeight: true + // Random Wallpaper + NToggle { + label: "Random Wallpaper" + description: "Automatically select random wallpapers from the folder" + value: Settings.data.wallpaper.isRandom + onToggled: function (newValue) { + Settings.data.wallpaper.isRandom = newValue + } + } + + // Use Wallpaper Theme + NToggle { + label: "Use Wallpaper Theme" + description: "Automatically adjust theme colors based on wallpaper" + value: Settings.data.wallpaper.generateTheme + onToggled: function (newValue) { + Settings.data.wallpaper.generateTheme = newValue + } + } + + // Wallpaper Interval + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + NText { + text: "Wallpaper Interval" + font.pointSize: 13 + font.weight: Style.fontWeightBold + color: Colors.textPrimary + } + + NText { + text: "How often to change wallpapers automatically (in seconds)" + font.pointSize: 12 + color: Colors.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + RowLayout { + Layout.fillWidth: true + + NText { + text: Settings.data.wallpaper.randomInterval + " seconds" + font.pointSize: 13 + color: Colors.textPrimary + } + + Item { + Layout.fillWidth: true + } + } + + NSlider { + Layout.fillWidth: true + from: 10 + to: 900 + stepSize: 10 + value: Settings.data.wallpaper.randomInterval + onMoved: Settings.data.wallpaper.randomInterval = Math.round(value) + cutoutColor: Colors.backgroundPrimary + } + } + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: 26 + Layout.bottomMargin: 18 + } + + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + NText { + text: "SWWW" + font.pointSize: 18 + font.weight: Style.fontWeightBold + color: Colors.textPrimary + Layout.bottomMargin: 8 + } + + // Use SWWW + NToggle { + label: "Use SWWW" + description: "Use SWWW daemon for advanced wallpaper management" + value: Settings.data.wallpaper.swww.enabled + onToggled: function (newValue) { + Settings.data.wallpaper.swww.enabled = newValue + } + } + + // SWWW Settings (only visible when useSWWW is enabled) + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + visible: Settings.data.wallpaper.swww.enabled + + // Resize Mode + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + + NText { + text: "Resize Mode" + font.pointSize: 13 + font.weight: Style.fontWeightBold + color: Colors.textPrimary + } + + NText { + text: "How SWWW should resize wallpapers to fit the screen" + font.pointSize: 12 + color: Colors.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + NComboBox { + optionsKeys: ["no", "crop", "fit", "stretch"] + optionsLabels: ["No", "Crop", "Fit", "Stretch"] + currentKey: Settings.data.wallpaper.swww.resizeMethod + onSelected: function (key) { + Settings.data.wallpaper.swww.resizeMethod = key + } + } + } + + // Transition Type + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + NText { + text: "Transition Type" + font.pointSize: 13 + font.weight: Style.fontWeightBold + color: Colors.textPrimary + } + + NText { + text: "Animation type when switching between wallpapers" + font.pointSize: 12 + color: Colors.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + NComboBox { + optionsKeys: ["none", "simple", "fade", "left", "right", "top", "bottom", "wipe", "wave", "grow", "center", "any", "outer", "random"] + optionsLabels: ["None", "Simple", "Fade", "Left", "Right", "Top", "Bottom", "Wipe", "Wave", "Grow", "Center", "Any", "Outer", "Random"] + currentKey: Settings.data.wallpaper.swww.transitionType + onSelected: function (key) { + Settings.data.wallpaper.swww.transitionType = key + } + } + } + + // Transition FPS + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + NText { + text: "Transition FPS" + font.pointSize: 13 + font.weight: Style.fontWeightBold + color: Colors.textPrimary + } + + NText { + text: "Frames per second for transition animations" + font.pointSize: 12 + color: Colors.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + RowLayout { + Layout.fillWidth: true + + NText { + text: Settings.data.wallpaper.swww.transitionFps + " FPS" + font.pointSize: 13 + color: Colors.textPrimary + } + + Item { + Layout.fillWidth: true + } + } + + NSlider { + Layout.fillWidth: true + from: 30 + to: 500 + stepSize: 5 + value: Settings.data.wallpaper.swww.transitionFps + onMoved: Settings.data.wallpaper.swww.transitionFps = Math.round(value) + cutoutColor: Colors.backgroundPrimary + } + } + + // Transition Duration + ColumnLayout { + spacing: 8 + Layout.fillWidth: true + Layout.topMargin: 8 + + NText { + text: "Transition Duration" + font.pointSize: 13 + font.weight: Style.fontWeightBold + color: Colors.textPrimary + } + + NText { + text: "Duration of transition animations in seconds" + font.pointSize: 12 + color: Colors.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + RowLayout { + Layout.fillWidth: true + + NText { + text: Settings.data.wallpaper.swww.transitionDuration.toFixed(3) + " seconds" + font.pointSize: 13 + color: Colors.textPrimary + } + + Item { + Layout.fillWidth: true + } + } + + NSlider { + Layout.fillWidth: true + from: 0.25 + to: 10 + stepSize: 0.05 + value: Settings.data.wallpaper.swww.transitionDuration + onMoved: Settings.data.wallpaper.swww.transitionDuration = value + cutoutColor: Colors.backgroundPrimary + } + } + } + } } } } diff --git a/Modules/Settings/Tabs/WallpaperSelector.qml b/Modules/Settings/Tabs/WallpaperSelector.qml new file mode 100644 index 0000000..f1f0b5e --- /dev/null +++ b/Modules/Settings/Tabs/WallpaperSelector.qml @@ -0,0 +1,270 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Qt.labs.folderlistmodel +import qs.Services +import qs.Widgets + +Item { + property real scaling: 1 + readonly property string tabIcon: "photo_library" + readonly property string tabLabel: "Wallpaper Selector" + readonly property int tabIndex: 7 + Layout.fillWidth: true + Layout.fillHeight: true + + ColumnLayout { + anchors.fill: parent + spacing: Style.marginMedium * scaling + + NText { + text: "Wallpaper Selector" + font.weight: Style.fontWeightBold + color: Colors.accentSecondary + } + + NText { + text: "Select a wallpaper from your configured directory" + color: Colors.textSecondary + wrapMode: Text.WordWrap + } + + // Current wallpaper display + NText { + text: "Current Wallpaper" + font.weight: Style.fontWeightBold + color: Colors.textPrimary + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 120 * scaling + radius: Style.radiusMedium * scaling + color: Colors.backgroundSecondary + border.color: Colors.outline + border.width: Math.max(1, Style.borderThin * scaling) + clip: true + + Image { + id: currentWallpaperImage + anchors.fill: parent + anchors.margins: Style.marginSmall * scaling + source: Wallpapers.currentWallpaper + fillMode: Image.PreserveAspectCrop + asynchronous: true + cache: true + } + + // Fallback if no image + Rectangle { + anchors.fill: parent + anchors.margins: Style.marginSmall * scaling + color: Colors.backgroundTertiary + radius: Style.radiusSmall * scaling + visible: currentWallpaperImage.status !== Image.Ready + + ColumnLayout { + anchors.centerIn: parent + spacing: Style.marginSmall * scaling + + NText { + text: "image" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeLarge * scaling + color: Colors.textSecondary + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "No wallpaper selected" + color: Colors.textSecondary + Layout.alignment: Qt.AlignHCenter + } + } + } + } + + NDivider { + Layout.fillWidth: true + } + + // Wallpaper grid + NText { + text: "Available Wallpapers" + font.weight: Style.fontWeightBold + color: Colors.textPrimary + } + + NText { + text: "Click on a wallpaper to set it as your current wallpaper" + color: Colors.textSecondary + wrapMode: Text.WordWrap + } + + NText { + text: Settings.data.wallpaper.swww.enabled ? + "Wallpapers will change with " + Settings.data.wallpaper.swww.transitionType + " transition" : + "Wallpapers will change instantly" + color: Colors.textSecondary + font.pointSize: Style.fontSizeSmall * scaling + visible: Settings.data.wallpaper.swww.enabled + } + + // Refresh button and status + RowLayout { + Layout.fillWidth: true + spacing: Style.marginSmall * scaling + + NIconButton { + icon: "refresh" + tooltipText: "Refresh wallpaper list" + onClicked: { + Wallpapers.loadWallpapers() + } + } + + NText { + text: "Refresh" + color: Colors.textSecondary + } + } + + // Wallpaper grid container + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + FolderListModel { + id: folderModel + folder: "file://" + (Settings.data.wallpaper.directory !== undefined ? Settings.data.wallpaper.directory : "") + nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.pnm", "*.bmp"] + showDirs: false + sortField: FolderListModel.Name + } + + GridView { + id: wallpaperGridView + anchors.fill: parent + clip: true + model: folderModel + + // Fixed 5 items per row - more aggressive sizing + property int itemSize: Math.floor((width - leftMargin - rightMargin - (4 * Style.marginSmall * scaling)) / 5) + + cellWidth: Math.floor((width - leftMargin - rightMargin) / 5) + cellHeight: Math.floor(itemSize * 0.67) + Style.marginSmall * scaling + + leftMargin: Style.marginSmall * scaling + rightMargin: Style.marginSmall * scaling + topMargin: Style.marginSmall * scaling + bottomMargin: Style.marginSmall * scaling + + delegate: Rectangle { + id: wallpaperItem + property string wallpaperPath: Settings.data.wallpaper.directory + "/" + fileName + property bool isSelected: wallpaperPath === Wallpapers.currentWallpaper + + width: wallpaperGridView.itemSize + height: Math.floor(wallpaperGridView.itemSize * 0.67) + radius: Style.radiusMedium * scaling + color: isSelected ? Colors.accentPrimary : Colors.backgroundSecondary + border.color: isSelected ? Colors.accentSecondary : Colors.outline + border.width: Math.max(1, Style.borderThin * scaling) + clip: true + + Image { + anchors.fill: parent + anchors.margins: Style.marginTiny * scaling + source: wallpaperPath + fillMode: Image.PreserveAspectCrop + asynchronous: true + cache: true + smooth: true + } + + // Selection indicator + Rectangle { + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: Style.marginTiny * scaling + width: 20 * scaling + height: 20 * scaling + radius: width / 2 + color: Colors.accentPrimary + border.color: Colors.onAccent + border.width: Math.max(1, Style.borderThin * scaling) + visible: isSelected + + NText { + anchors.centerIn: parent + text: "check" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeSmall * scaling + color: Colors.onAccent + } + } + + // Hover effect + Rectangle { + anchors.fill: parent + color: Colors.textPrimary + opacity: mouseArea.containsMouse ? 0.1 : 0 + radius: parent.radius + + Behavior on opacity { + NumberAnimation { duration: 150 } + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + acceptedButtons: Qt.LeftButton + hoverEnabled: true + onClicked: { + Wallpapers.changeWallpaper(wallpaperPath) + } + } + } + } + + // Empty state + Rectangle { + anchors.fill: parent + color: Colors.backgroundSecondary + radius: Style.radiusMedium * scaling + border.color: Colors.outline + border.width: Math.max(1, Style.borderThin * scaling) + visible: folderModel.count === 0 && !Wallpapers.scanning + + ColumnLayout { + anchors.centerIn: parent + spacing: Style.marginMedium * scaling + + NText { + text: "folder_open" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeLarge * scaling + color: Colors.textSecondary + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "No wallpapers found" + color: Colors.textSecondary + font.weight: Style.fontWeightBold + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "Make sure your wallpaper directory is configured and contains image files" + color: Colors.textSecondary + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + Layout.preferredWidth: 300 * scaling + } + } + } + } + } +} \ No newline at end of file diff --git a/Services/Settings.qml b/Services/Settings.qml index 3d31e06..7e7b0b4 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -50,7 +50,14 @@ Singleton { Component.onCompleted: function () { reload() } - onLoaded: function () {} + onLoaded: function () { + Qt.callLater(function () { + if (adapter.wallpaper.current !== "") { + console.log("Settings: Initializing wallpaper to:", adapter.wallpaper.current) + Wallpapers.setCurrentWallpaper(adapter.wallpaper.current, true) + } + }) + } onLoadFailed: function (error) { if (error.toString().includes("No such file") || error === 2) // File doesn't exist, create it with default values @@ -113,7 +120,7 @@ Singleton { wallpaper: JsonObject { property string directory: "/usr/share/wallpapers" - property string current: defaultWallpaper + property string current: "" property bool isRandom: false property int randomInterval: 300 property bool generateTheme: false @@ -178,4 +185,11 @@ Singleton { } } } + + Connections { + target: adapter.wallpaper + function onIsRandomChanged() { Wallpapers.toggleRandomWallpaper() } + function onRandomIntervalChanged() { Wallpapers.restartRandomWallpaperTimer() } + function onDirectoryChanged() { Wallpapers.loadWallpapers() } + } } diff --git a/Services/Wallpapers.qml b/Services/Wallpapers.qml index d18f3a5..e586a09 100644 --- a/Services/Wallpapers.qml +++ b/Services/Wallpapers.qml @@ -11,8 +11,13 @@ Singleton { Item { Component.onCompleted: { loadWallpapers() - setCurrentWallpaper(currentWallpaper, true) - toggleRandomWallpaper() + // Only set initial wallpaper if it's not empty + if (currentWallpaper !== "") { + console.log("Wallpapers: Initializing with wallpaper:", currentWallpaper) + setCurrentWallpaper(currentWallpaper, true) + } + // Don't start random wallpaper during initialization + // toggleRandomWallpaper() } } @@ -20,7 +25,7 @@ Singleton { property string currentWallpaper: Settings.data.wallpaper.current property bool scanning: false property string transitionType: Settings.data.wallpaper.swww.transitionType - property var randomChoices: ["fade", "left", "right", "top", "bottom", "wipe", "wave", "grow", "center", "any", "outer"] + property var randomChoices: ["simple", "fade", "left", "right", "top", "bottom", "wipe", "wave", "grow", "center", "any", "outer"] function loadWallpapers() { scanning = true @@ -29,10 +34,12 @@ Singleton { } function changeWallpaper(path) { - setCurrentWallpaper(path) + console.log("Wallpapers: changeWallpaper called with:", path) + setCurrentWallpaper(path, false) } function setCurrentWallpaper(path, isInitial) { + console.log("Wallpapers: Setting wallpaper to:", path, "isInitial:", isInitial) currentWallpaper = path if (!isInitial) { Settings.data.wallpaper.current = path @@ -43,7 +50,11 @@ Singleton { } else { transitionType = Settings.data.wallpaper.swww.transitionType } + console.log("SWWW: Changing wallpaper with transition type:", transitionType) changeWallpaperProcess.running = true + } else { + // Fallback: update the settings directly for non-SWWW mode + console.log("Non-SWWW mode: Setting wallpaper directly") } if (randomWallpaperTimer.running) { @@ -59,7 +70,7 @@ Singleton { if (!randomPath) { return } - setCurrentWallpaper(randomPath) + setCurrentWallpaper(randomPath, false) } function toggleRandomWallpaper() { @@ -84,6 +95,13 @@ Singleton { } } + function startSWWWDaemon() { + if (Settings.data.wallpaper.swww.enabled) { + console.log("SWWW: Attempting to start swww-daemon...") + startDaemonProcess.running = true + } + } + Timer { id: randomWallpaperTimer interval: Settings.data.wallpaper.randomInterval * 1000 @@ -120,6 +138,18 @@ Singleton { ), "--transition-type", transitionType, "--transition-duration", Settings.data.wallpaper.swww.transitionDuration.toString( ), currentWallpaper] running: false + + onStarted: { + console.log("SWWW: Process started with command:", command.join(" ")) + } + + onExited: function(exitCode, exitStatus) { + console.log("SWWW: Process finished with exit code:", exitCode, "status:", exitStatus) + if (exitCode !== 0) { + console.log("SWWW: Process failed. Make sure swww-daemon is running with: swww-daemon") + console.log("SWWW: You can start it with: swww-daemon --format xrgb") + } + } } Process { @@ -128,4 +158,23 @@ Singleton { workingDirectory: Quickshell.shellDir running: false } + + Process { + id: startDaemonProcess + command: ["swww-daemon", "--format", "xrgb"] + running: false + + onStarted: { + console.log("SWWW: Daemon start process initiated") + } + + onExited: function(exitCode, exitStatus) { + console.log("SWWW: Daemon start process finished with exit code:", exitCode) + if (exitCode === 0) { + console.log("SWWW: Daemon started successfully") + } else { + console.log("SWWW: Failed to start daemon. It might already be running.") + } + } + } } From 904afa2537705e1e537f550ab30fc20068ac44d5 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 11:28:42 -0400 Subject: [PATCH 181/394] Sysmon script + renaming cards so its easier to know what they do --- Bin/sysmon.sh | 50 +++++++++++++++++++ ...rProfileCard.qml => PowerProfilesCard.qml} | 0 .../{SystemCard.qml => SystemMonitorCard.qml} | 0 Modules/SidePanel/SidePanel.qml | 6 ++- Widgets/NIconButton.qml | 7 ++- 5 files changed, 57 insertions(+), 6 deletions(-) create mode 100755 Bin/sysmon.sh rename Modules/SidePanel/Cards/{PowerProfileCard.qml => PowerProfilesCard.qml} (100%) rename Modules/SidePanel/Cards/{SystemCard.qml => SystemMonitorCard.qml} (100%) diff --git a/Bin/sysmon.sh b/Bin/sysmon.sh new file mode 100755 index 0000000..d91b88c --- /dev/null +++ b/Bin/sysmon.sh @@ -0,0 +1,50 @@ +#!/bin/bash + +# A script to display CPU temperature, CPU usage, memory usage, and disk usage without the 'sensors' package. + +echo "--- System Metrics ---" + +# Get CPU Temperature in Celsius from a kernel file. +# This method is more common on modern systems but may vary. +# It reads the temperature from the first available core. +# Function to get CPU temperature + + # Check for the common thermal zone path + if [ -f "/sys/class/thermal/thermal_zone0/temp" ]; then + temp_file="/sys/class/thermal/thermal_zone0/temp" + # Check for a different thermal zone path (e.g., some older systems) + elif [ -f "/sys/class/hwmon/hwmon0/temp1_input" ]; then + temp_file="/sys/class/hwmon/hwmon0/temp1_input" + else + echo "Error: Could not find a CPU temperature file." + exit 1 + fi + + # Read the raw temperature value + raw_temp=$(cat "$temp_file") + + # The value is usually in millidegrees Celsius, so we divide by 1000. + # We use 'bc' for floating-point arithmetic. + temp_celsius=$(echo "scale=2; $raw_temp / 1000" | bc) + + echo "CPU Temperature: ${temp_celsius}°C" + + +# Get CPU Usage +# 'top' is a standard utility for this. It gives a real-time view of system processes. +cpu_usage=$(top -bn1 | grep "Cpu(s)" | sed "s/.*, *\([0-9.]*\)%* id.*/\1/" | awk '{print 100 - $1}') +echo "CPU Usage: ${cpu_usage}%" + +# Get Memory Usage +# 'free' provides information about memory usage. +mem_total=$(free | grep Mem | awk '{print $2}') +mem_used=$(free | grep Mem | awk '{print $3}') +mem_usage=$(echo "scale=2; ($mem_used/$mem_total)*100" | bc) +echo "Memory Usage: ${mem_usage}%" + +# Get Disk Usage +# 'df' reports file system disk space usage. We check the root directory. +disk_usage=$(df -h / | grep / | awk '{print $5}' | sed 's/%//g') +echo "Disk Usage: ${disk_usage}%" + +echo "----------------------" \ No newline at end of file diff --git a/Modules/SidePanel/Cards/PowerProfileCard.qml b/Modules/SidePanel/Cards/PowerProfilesCard.qml similarity index 100% rename from Modules/SidePanel/Cards/PowerProfileCard.qml rename to Modules/SidePanel/Cards/PowerProfilesCard.qml diff --git a/Modules/SidePanel/Cards/SystemCard.qml b/Modules/SidePanel/Cards/SystemMonitorCard.qml similarity index 100% rename from Modules/SidePanel/Cards/SystemCard.qml rename to Modules/SidePanel/Cards/SystemMonitorCard.qml diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml index c8936d8..5b0a5c6 100644 --- a/Modules/SidePanel/SidePanel.qml +++ b/Modules/SidePanel/SidePanel.qml @@ -110,7 +110,7 @@ NLoader { } // System monitors combined in one card - SystemCard { + SystemMonitorCard { id: statsCard } } @@ -122,8 +122,10 @@ NLoader { Layout.bottomMargin: 0 spacing: sidePanel.cardSpacing - PowerProfileCard {} + // Power Profiles switcher + PowerProfilesCard {} + // Utilities buttons UtilitiesCard {} } } diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml index 825eadb..04de320 100644 --- a/Widgets/NIconButton.qml +++ b/Widgets/NIconButton.qml @@ -37,14 +37,13 @@ Rectangle { text: root.icon font.family: fontFamily font.pointSize: root.fontPointSize * scaling - font.variableAxes: { - "wght": (Font.Normal + Font.Bold) / 2.0 - } + font.variableAxes: { + "wght": (Font.Normal + Font.Bold) / 2.0 + } color: root.hovering ? Colors.onAccent : showBorder ? Colors.accentPrimary : Colors.textPrimary horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter opacity: root.enabled ? Style.opacityFull : Style.opacityMedium - } NTooltip { From a867ae4bca342ed57e2e47707126ed82ef79570b Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Tue, 12 Aug 2025 17:29:18 +0200 Subject: [PATCH 182/394] Add #105 PR from main --- Services/Workspaces.qml | 280 ++++++++++++++++++++-------------------- 1 file changed, 141 insertions(+), 139 deletions(-) diff --git a/Services/Workspaces.qml b/Services/Workspaces.qml index f5de53b..9bedc6b 100644 --- a/Services/Workspaces.qml +++ b/Services/Workspaces.qml @@ -1,6 +1,5 @@ pragma Singleton - -pragma ComponentBehavior +pragma ComponentBehavior: Bound import QtQuick import Quickshell @@ -9,146 +8,149 @@ import Quickshell.Hyprland import qs.Services Singleton { - id: root + id: root - property ListModel workspaces: ListModel {} - property bool isHyprland: false - property bool isNiri: false - property var hlWorkspaces: Hyprland.workspaces.values - // Detect which compositor we're using - Component.onCompleted: { - console.log("Workspace initializing...") - detectCompositor() - } + property ListModel workspaces: ListModel {} + property bool isHyprland: false + property bool isNiri: false + property var hlWorkspaces: Hyprland.workspaces.values + // Detect which compositor we're using + Component.onCompleted: { + console.log("WorkspaceManager initializing..."); + detectCompositor(); + } - function detectCompositor() { - try { - try { - if (Hyprland.eventSocketPath) { - console.log("Detected Hyprland compositor") - isHyprland = true - isNiri = false - initHyprland() - return + function detectCompositor() { + try { + try { + if (Hyprland.eventSocketPath) { + console.log("Detected Hyprland compositor"); + isHyprland = true; + isNiri = false; + initHyprland(); + return; + } + } catch (e) { + console.log("Hyprland not available:", e); + } + + if (typeof Niri !== "undefined") { + console.log("Detected Niri service"); + isHyprland = false; + isNiri = true; + initNiri(); + return; + } + + console.log("No supported compositor detected"); + } catch (e) { + console.error("Error detecting compositor:", e); } - } catch (e) { - console.log("Hyprland not available:", e) - } - - if (typeof Niri !== "undefined") { - console.log("Detected Niri service") - isHyprland = false - isNiri = true - initNiri() - return - } - - console.log("No supported compositor detected") - } catch (e) { - console.error("Error detecting compositor:", e) - } - } - - // Initialize Hyprland integration - function initHyprland() { - try { - // Fixes the odd workspace issue. - Hyprland.refreshWorkspaces() - // hlWorkspaces = Hyprland.workspaces.values; - // updateHyprlandWorkspaces(); - return true - } catch (e) { - console.error("Error initializing Hyprland:", e) - isHyprland = false - return false - } - } - - onHlWorkspacesChanged: { - updateHyprlandWorkspaces() - } - - Connections { - target: Hyprland.workspaces - function onValuesChanged() { - updateHyprlandWorkspaces() - } - } - - Connections { - target: Hyprland - function onRawEvent(event) { - updateHyprlandWorkspaces() - } - } - - function updateHyprlandWorkspaces() { - workspaces.clear() - try { - for (var i = 0; i < hlWorkspaces.length; i++) { - const ws = hlWorkspaces[i] - workspaces.append({ - "id": i, - "idx": ws.id, - "name": ws.name || "", - "output": ws.monitor?.name || "", - "isActive": ws.active === true, - "isFocused": ws.focused === true, - "isUrgent": ws.urgent === true - }) - } - workspacesChanged() - } catch (e) { - console.error("Error updating Hyprland workspaces:", e) - } - } - - function initNiri() { - updateNiriWorkspaces() - } - - Connections { - target: Niri - function onWorkspacesChanged() { - updateNiriWorkspaces() - } - } - - function updateNiriWorkspaces() { - const niriWorkspaces = Niri.workspaces || [] - workspaces.clear() - for (var i = 0; i < niriWorkspaces.length; i++) { - const ws = niriWorkspaces[i] - workspaces.append({ - "id": ws.id, - "idx": ws.idx || 1, - "name": ws.name || "", - "output": ws.output || "", - "isFocused": ws.isFocused === true, - "isActive": ws.isActive === true, - "isUrgent": ws.isUrgent === true, - "isOccupied": ws.isOccupied === true - }) } - workspacesChanged() - } - - function switchToWorkspace(workspaceId) { - if (isHyprland) { - try { - Hyprland.dispatch(`workspace ${workspaceId}`) - } catch (e) { - console.error("Error switching Hyprland workspace:", e) - } - } else if (isNiri) { - try { - Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", workspaceId.toString()]) - } catch (e) { - console.error("Error switching Niri workspace:", e) - } - } else { - console.warn("No supported compositor detected for workspace switching") + // Initialize Hyprland integration + function initHyprland() { + try { + // Fixes the odd workspace issue. + Hyprland.refreshWorkspaces(); + // hlWorkspaces = Hyprland.workspaces.values; + // updateHyprlandWorkspaces(); + return true; + } catch (e) { + console.error("Error initializing Hyprland:", e); + isHyprland = false; + return false; + } } - } -} + + onHlWorkspacesChanged: { + updateHyprlandWorkspaces(); + } + + Connections { + target: Hyprland.workspaces + function onValuesChanged() { + updateHyprlandWorkspaces(); + } + } + + Connections { + target: Hyprland + function onRawEvent(event) { + updateHyprlandWorkspaces(); + } + } + + function updateHyprlandWorkspaces() { + workspaces.clear(); + try { + for (let i = 0; i < hlWorkspaces.length; i++) { + const ws = hlWorkspaces[i]; + // Only append workspaces with id >= 1 + if (ws.id >= 1) { + workspaces.append({ + id: i, + idx: ws.id, + name: ws.name || "", + output: ws.monitor?.name || "", + isActive: ws.active === true, + isFocused: ws.focused === true, + isUrgent: ws.urgent === true + }); + } + } + workspacesChanged(); + } catch (e) { + console.error("Error updating Hyprland workspaces:", e); + } + } + + function initNiri() { + updateNiriWorkspaces(); + } + + Connections { + target: Niri + function onWorkspacesChanged() { + updateNiriWorkspaces(); + } + } + + function updateNiriWorkspaces() { + const niriWorkspaces = Niri.workspaces || []; + workspaces.clear(); + for (let i = 0; i < niriWorkspaces.length; i++) { + const ws = niriWorkspaces[i]; + workspaces.append({ + id: ws.id, + idx: ws.idx || 1, + name: ws.name || "", + output: ws.output || "", + isFocused: ws.isFocused === true, + isActive: ws.isActive === true, + isUrgent: ws.isUrgent === true, + isOccupied: ws.isOccupied === true, + }); + } + + workspacesChanged(); + } + + function switchToWorkspace(workspaceId) { + if (isHyprland) { + try { + Hyprland.dispatch(`workspace ${workspaceId}`); + } catch (e) { + console.error("Error switching Hyprland workspace:", e); + } + } else if (isNiri) { + try { + Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", workspaceId.toString()]); + } catch (e) { + console.error("Error switching Niri workspace:", e); + } + } else { + console.warn("No supported compositor detected for workspace switching"); + } + } +} \ No newline at end of file From 8585c5c72905838bb7baf93e6421ad663d96ade9 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Tue, 12 Aug 2025 17:34:13 +0200 Subject: [PATCH 183/394] Make Wallpaper Selector scrollable --- Modules/Settings/Tabs/WallpaperSelector.qml | 447 ++++++++++---------- 1 file changed, 228 insertions(+), 219 deletions(-) diff --git a/Modules/Settings/Tabs/WallpaperSelector.qml b/Modules/Settings/Tabs/WallpaperSelector.qml index f1f0b5e..95a8976 100644 --- a/Modules/Settings/Tabs/WallpaperSelector.qml +++ b/Modules/Settings/Tabs/WallpaperSelector.qml @@ -13,255 +13,264 @@ Item { Layout.fillWidth: true Layout.fillHeight: true - ColumnLayout { + ScrollView { anchors.fill: parent - spacing: Style.marginMedium * scaling + clip: true + ScrollBar.vertical.policy: ScrollBar.AsNeeded + ScrollBar.horizontal.policy: ScrollBar.AsNeeded + contentWidth: parent.width - NText { - text: "Wallpaper Selector" - font.weight: Style.fontWeightBold - color: Colors.accentSecondary - } - - NText { - text: "Select a wallpaper from your configured directory" - color: Colors.textSecondary - wrapMode: Text.WordWrap - } - - // Current wallpaper display - NText { - text: "Current Wallpaper" - font.weight: Style.fontWeightBold - color: Colors.textPrimary - } - - Rectangle { + ColumnLayout { + width: parent.width + spacing: Style.marginMedium * scaling Layout.fillWidth: true - Layout.preferredHeight: 120 * scaling - radius: Style.radiusMedium * scaling - color: Colors.backgroundSecondary - border.color: Colors.outline - border.width: Math.max(1, Style.borderThin * scaling) - clip: true - Image { - id: currentWallpaperImage - anchors.fill: parent - anchors.margins: Style.marginSmall * scaling - source: Wallpapers.currentWallpaper - fillMode: Image.PreserveAspectCrop - asynchronous: true - cache: true - } - - // Fallback if no image - Rectangle { - anchors.fill: parent - anchors.margins: Style.marginSmall * scaling - color: Colors.backgroundTertiary - radius: Style.radiusSmall * scaling - visible: currentWallpaperImage.status !== Image.Ready - - ColumnLayout { - anchors.centerIn: parent - spacing: Style.marginSmall * scaling - - NText { - text: "image" - font.family: "Material Symbols Outlined" - font.pointSize: Style.fontSizeLarge * scaling - color: Colors.textSecondary - Layout.alignment: Qt.AlignHCenter - } - - NText { - text: "No wallpaper selected" - color: Colors.textSecondary - Layout.alignment: Qt.AlignHCenter - } - } - } - } - - NDivider { - Layout.fillWidth: true - } - - // Wallpaper grid - NText { - text: "Available Wallpapers" - font.weight: Style.fontWeightBold - color: Colors.textPrimary - } - - NText { - text: "Click on a wallpaper to set it as your current wallpaper" - color: Colors.textSecondary - wrapMode: Text.WordWrap - } - - NText { - text: Settings.data.wallpaper.swww.enabled ? - "Wallpapers will change with " + Settings.data.wallpaper.swww.transitionType + " transition" : - "Wallpapers will change instantly" - color: Colors.textSecondary - font.pointSize: Style.fontSizeSmall * scaling - visible: Settings.data.wallpaper.swww.enabled - } - - // Refresh button and status - RowLayout { - Layout.fillWidth: true - spacing: Style.marginSmall * scaling - - NIconButton { - icon: "refresh" - tooltipText: "Refresh wallpaper list" - onClicked: { - Wallpapers.loadWallpapers() - } + NText { + text: "Wallpaper Selector" + font.weight: Style.fontWeightBold + color: Colors.accentSecondary } NText { - text: "Refresh" + text: "Select a wallpaper from your configured directory" color: Colors.textSecondary - } - } - - // Wallpaper grid container - Item { - Layout.fillWidth: true - Layout.fillHeight: true - - FolderListModel { - id: folderModel - folder: "file://" + (Settings.data.wallpaper.directory !== undefined ? Settings.data.wallpaper.directory : "") - nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.pnm", "*.bmp"] - showDirs: false - sortField: FolderListModel.Name + wrapMode: Text.WordWrap } - GridView { - id: wallpaperGridView - anchors.fill: parent + // Current wallpaper display + NText { + text: "Current Wallpaper" + font.weight: Style.fontWeightBold + color: Colors.textPrimary + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 120 * scaling + radius: Style.radiusMedium * scaling + color: Colors.backgroundSecondary + border.color: Colors.outline + border.width: Math.max(1, Style.borderThin * scaling) clip: true - model: folderModel - - // Fixed 5 items per row - more aggressive sizing - property int itemSize: Math.floor((width - leftMargin - rightMargin - (4 * Style.marginSmall * scaling)) / 5) - - cellWidth: Math.floor((width - leftMargin - rightMargin) / 5) - cellHeight: Math.floor(itemSize * 0.67) + Style.marginSmall * scaling - - leftMargin: Style.marginSmall * scaling - rightMargin: Style.marginSmall * scaling - topMargin: Style.marginSmall * scaling - bottomMargin: Style.marginSmall * scaling - delegate: Rectangle { - id: wallpaperItem - property string wallpaperPath: Settings.data.wallpaper.directory + "/" + fileName - property bool isSelected: wallpaperPath === Wallpapers.currentWallpaper + Image { + id: currentWallpaperImage + anchors.fill: parent + anchors.margins: Style.marginSmall * scaling + source: Wallpapers.currentWallpaper + fillMode: Image.PreserveAspectCrop + asynchronous: true + cache: true + } - width: wallpaperGridView.itemSize - height: Math.floor(wallpaperGridView.itemSize * 0.67) - radius: Style.radiusMedium * scaling - color: isSelected ? Colors.accentPrimary : Colors.backgroundSecondary - border.color: isSelected ? Colors.accentSecondary : Colors.outline - border.width: Math.max(1, Style.borderThin * scaling) - clip: true + // Fallback if no image + Rectangle { + anchors.fill: parent + anchors.margins: Style.marginSmall * scaling + color: Colors.backgroundTertiary + radius: Style.radiusSmall * scaling + visible: currentWallpaperImage.status !== Image.Ready - Image { - anchors.fill: parent - anchors.margins: Style.marginTiny * scaling - source: wallpaperPath - fillMode: Image.PreserveAspectCrop - asynchronous: true - cache: true - smooth: true - } - - // Selection indicator - Rectangle { - anchors.top: parent.top - anchors.right: parent.right - anchors.margins: Style.marginTiny * scaling - width: 20 * scaling - height: 20 * scaling - radius: width / 2 - color: Colors.accentPrimary - border.color: Colors.onAccent - border.width: Math.max(1, Style.borderThin * scaling) - visible: isSelected + ColumnLayout { + anchors.centerIn: parent + spacing: Style.marginSmall * scaling NText { - anchors.centerIn: parent - text: "check" + text: "image" font.family: "Material Symbols Outlined" - font.pointSize: Style.fontSizeSmall * scaling - color: Colors.onAccent + font.pointSize: Style.fontSizeLarge * scaling + color: Colors.textSecondary + Layout.alignment: Qt.AlignHCenter } - } - // Hover effect - Rectangle { - anchors.fill: parent - color: Colors.textPrimary - opacity: mouseArea.containsMouse ? 0.1 : 0 - radius: parent.radius - - Behavior on opacity { - NumberAnimation { duration: 150 } - } - } - - MouseArea { - id: mouseArea - anchors.fill: parent - acceptedButtons: Qt.LeftButton - hoverEnabled: true - onClicked: { - Wallpapers.changeWallpaper(wallpaperPath) + NText { + text: "No wallpaper selected" + color: Colors.textSecondary + Layout.alignment: Qt.AlignHCenter } } } } - // Empty state - Rectangle { - anchors.fill: parent - color: Colors.backgroundSecondary - radius: Style.radiusMedium * scaling - border.color: Colors.outline - border.width: Math.max(1, Style.borderThin * scaling) - visible: folderModel.count === 0 && !Wallpapers.scanning + NDivider { + Layout.fillWidth: true + } - ColumnLayout { - anchors.centerIn: parent - spacing: Style.marginMedium * scaling + // Wallpaper grid + NText { + text: "Available Wallpapers" + font.weight: Style.fontWeightBold + color: Colors.textPrimary + } - NText { - text: "folder_open" - font.family: "Material Symbols Outlined" - font.pointSize: Style.fontSizeLarge * scaling - color: Colors.textSecondary - Layout.alignment: Qt.AlignHCenter + NText { + text: "Click on a wallpaper to set it as your current wallpaper" + color: Colors.textSecondary + wrapMode: Text.WordWrap + } + + NText { + text: Settings.data.wallpaper.swww.enabled ? + "Wallpapers will change with " + Settings.data.wallpaper.swww.transitionType + " transition" : + "Wallpapers will change instantly" + color: Colors.textSecondary + font.pointSize: Style.fontSizeSmall * scaling + visible: Settings.data.wallpaper.swww.enabled + } + + // Refresh button and status + RowLayout { + Layout.fillWidth: true + spacing: Style.marginSmall * scaling + + NIconButton { + icon: "refresh" + tooltipText: "Refresh wallpaper list" + onClicked: { + Wallpapers.loadWallpapers() } + } - NText { - text: "No wallpapers found" - color: Colors.textSecondary - font.weight: Style.fontWeightBold - Layout.alignment: Qt.AlignHCenter + NText { + text: "Refresh" + color: Colors.textSecondary + } + } + + // Wallpaper grid container + Item { + Layout.fillWidth: true + Layout.preferredHeight: 400 * scaling + + FolderListModel { + id: folderModel + folder: "file://" + (Settings.data.wallpaper.directory !== undefined ? Settings.data.wallpaper.directory : "") + nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.pnm", "*.bmp"] + showDirs: false + sortField: FolderListModel.Name + } + + GridView { + id: wallpaperGridView + anchors.fill: parent + clip: true + model: folderModel + + // Fixed 5 items per row - more aggressive sizing + property int itemSize: Math.floor((width - leftMargin - rightMargin - (4 * Style.marginSmall * scaling)) / 5) + + cellWidth: Math.floor((width - leftMargin - rightMargin) / 5) + cellHeight: Math.floor(itemSize * 0.67) + Style.marginSmall * scaling + + leftMargin: Style.marginSmall * scaling + rightMargin: Style.marginSmall * scaling + topMargin: Style.marginSmall * scaling + bottomMargin: Style.marginSmall * scaling + + delegate: Rectangle { + id: wallpaperItem + property string wallpaperPath: Settings.data.wallpaper.directory + "/" + fileName + property bool isSelected: wallpaperPath === Wallpapers.currentWallpaper + + width: wallpaperGridView.itemSize + height: Math.floor(wallpaperGridView.itemSize * 0.67) + radius: Style.radiusMedium * scaling + color: isSelected ? Colors.accentPrimary : Colors.backgroundSecondary + border.color: isSelected ? Colors.accentSecondary : Colors.outline + border.width: Math.max(1, Style.borderThin * scaling) + clip: true + + Image { + anchors.fill: parent + anchors.margins: Style.marginTiny * scaling + source: wallpaperPath + fillMode: Image.PreserveAspectCrop + asynchronous: true + cache: true + smooth: true + } + + // Selection indicator + Rectangle { + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: Style.marginTiny * scaling + width: 20 * scaling + height: 20 * scaling + radius: width / 2 + color: Colors.accentPrimary + border.color: Colors.onAccent + border.width: Math.max(1, Style.borderThin * scaling) + visible: isSelected + + NText { + anchors.centerIn: parent + text: "check" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeSmall * scaling + color: Colors.onAccent + } + } + + // Hover effect + Rectangle { + anchors.fill: parent + color: Colors.textPrimary + opacity: mouseArea.containsMouse ? 0.1 : 0 + radius: parent.radius + + Behavior on opacity { + NumberAnimation { duration: 150 } + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + acceptedButtons: Qt.LeftButton + hoverEnabled: true + onClicked: { + Wallpapers.changeWallpaper(wallpaperPath) + } + } } + } - NText { - text: "Make sure your wallpaper directory is configured and contains image files" - color: Colors.textSecondary - wrapMode: Text.WordWrap - horizontalAlignment: Text.AlignHCenter - Layout.preferredWidth: 300 * scaling + // Empty state + Rectangle { + anchors.fill: parent + color: Colors.backgroundSecondary + radius: Style.radiusMedium * scaling + border.color: Colors.outline + border.width: Math.max(1, Style.borderThin * scaling) + visible: folderModel.count === 0 && !Wallpapers.scanning + + ColumnLayout { + anchors.centerIn: parent + spacing: Style.marginMedium * scaling + + NText { + text: "folder_open" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeLarge * scaling + color: Colors.textSecondary + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "No wallpapers found" + color: Colors.textSecondary + font.weight: Style.fontWeightBold + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "Make sure your wallpaper directory is configured and contains image files" + color: Colors.textSecondary + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + Layout.preferredWidth: 300 * scaling + } } } } From 73f3909202dd94978f5ca4a5e52e3b4a753f7aa4 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 11:40:29 -0400 Subject: [PATCH 184/394] no bc dependency --- Bin/sysmon.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Bin/sysmon.sh b/Bin/sysmon.sh index d91b88c..ec4eeaa 100755 --- a/Bin/sysmon.sh +++ b/Bin/sysmon.sh @@ -25,7 +25,7 @@ echo "--- System Metrics ---" # The value is usually in millidegrees Celsius, so we divide by 1000. # We use 'bc' for floating-point arithmetic. - temp_celsius=$(echo "scale=2; $raw_temp / 1000" | bc) + temp_celsius=$((raw_temp / 1000)) echo "CPU Temperature: ${temp_celsius}°C" From bd056cc98116aa9c819b24dd320a48d9fd1e0166 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 11:42:50 -0400 Subject: [PATCH 185/394] no more bc (take 2) --- Bin/sysmon.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Bin/sysmon.sh b/Bin/sysmon.sh index ec4eeaa..0d69904 100755 --- a/Bin/sysmon.sh +++ b/Bin/sysmon.sh @@ -24,7 +24,6 @@ echo "--- System Metrics ---" raw_temp=$(cat "$temp_file") # The value is usually in millidegrees Celsius, so we divide by 1000. - # We use 'bc' for floating-point arithmetic. temp_celsius=$((raw_temp / 1000)) echo "CPU Temperature: ${temp_celsius}°C" @@ -39,7 +38,7 @@ echo "CPU Usage: ${cpu_usage}%" # 'free' provides information about memory usage. mem_total=$(free | grep Mem | awk '{print $2}') mem_used=$(free | grep Mem | awk '{print $3}') -mem_usage=$(echo "scale=2; ($mem_used/$mem_total)*100" | bc) +mem_usage=$((100 * mem_used / mem_total)) echo "Memory Usage: ${mem_usage}%" # Get Disk Usage From 4ebc4cd299cd92e02102b8b59d1590fabc115b45 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Tue, 12 Aug 2025 17:51:11 +0200 Subject: [PATCH 186/394] Make wallpapers rounded in WallpaperSelector --- Modules/Settings/Tabs/WallpaperSelector.qml | 51 +++++---------------- Widgets/NImageRounded.qml | 6 ++- 2 files changed, 16 insertions(+), 41 deletions(-) diff --git a/Modules/Settings/Tabs/WallpaperSelector.qml b/Modules/Settings/Tabs/WallpaperSelector.qml index 95a8976..281887d 100644 --- a/Modules/Settings/Tabs/WallpaperSelector.qml +++ b/Modules/Settings/Tabs/WallpaperSelector.qml @@ -53,42 +53,15 @@ Item { border.width: Math.max(1, Style.borderThin * scaling) clip: true - Image { + NImageRounded { id: currentWallpaperImage anchors.fill: parent anchors.margins: Style.marginSmall * scaling - source: Wallpapers.currentWallpaper - fillMode: Image.PreserveAspectCrop - asynchronous: true - cache: true - } - - // Fallback if no image - Rectangle { - anchors.fill: parent - anchors.margins: Style.marginSmall * scaling - color: Colors.backgroundTertiary - radius: Style.radiusSmall * scaling - visible: currentWallpaperImage.status !== Image.Ready - - ColumnLayout { - anchors.centerIn: parent - spacing: Style.marginSmall * scaling - - NText { - text: "image" - font.family: "Material Symbols Outlined" - font.pointSize: Style.fontSizeLarge * scaling - color: Colors.textSecondary - Layout.alignment: Qt.AlignHCenter - } - - NText { - text: "No wallpaper selected" - color: Colors.textSecondary - Layout.alignment: Qt.AlignHCenter - } - } + imagePath: Wallpapers.currentWallpaper + fallbackIcon: "image" + borderColor: Colors.outline + borderWidth: Math.max(1, Style.borderThin * scaling) + imageRadius: Style.radiusMedium * scaling } } @@ -180,14 +153,14 @@ Item { border.width: Math.max(1, Style.borderThin * scaling) clip: true - Image { + NImageRounded { anchors.fill: parent anchors.margins: Style.marginTiny * scaling - source: wallpaperPath - fillMode: Image.PreserveAspectCrop - asynchronous: true - cache: true - smooth: true + imagePath: wallpaperPath + fallbackIcon: "image" + borderColor: "transparent" + borderWidth: 0 + imageRadius: Style.radiusMedium * scaling } // Selection indicator diff --git a/Widgets/NImageRounded.qml b/Widgets/NImageRounded.qml index abef690..f1a37d9 100644 --- a/Widgets/NImageRounded.qml +++ b/Widgets/NImageRounded.qml @@ -5,8 +5,10 @@ import Quickshell.Widgets import qs.Services Rectangle { + id: root color: "transparent" - radius: width * 0.5 + property real imageRadius: width * 0.5 + radius: imageRadius readonly property real scaling: Scaling.scale(screen) property string imagePath: "" @@ -52,7 +54,7 @@ Rectangle { visible: false Rectangle { anchors.fill: parent - radius: img.width * 0.5 + radius: root.imageRadius } } From 032547c46eec61ed760accdaaa3a8c29fa4545c9 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Tue, 12 Aug 2025 18:03:09 +0200 Subject: [PATCH 187/394] Prepare for wallust integration --- Modules/Settings/Tabs/Wallpaper.qml | 2 +- Templates/templates/noctalia.json | 28 +++++++++++++++++ Templates/wallust.toml | 47 +++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 Templates/templates/noctalia.json create mode 100644 Templates/wallust.toml diff --git a/Modules/Settings/Tabs/Wallpaper.qml b/Modules/Settings/Tabs/Wallpaper.qml index d121236..430b636 100644 --- a/Modules/Settings/Tabs/Wallpaper.qml +++ b/Modules/Settings/Tabs/Wallpaper.qml @@ -61,7 +61,7 @@ ColumnLayout { NText { text: "Path to your wallpaper folder" - font.pointSize: 12 + font.pointSize: Style.fontSizeSmall * scaling color: Colors.textSecondary wrapMode: Text.WordWrap Layout.fillWidth: true diff --git a/Templates/templates/noctalia.json b/Templates/templates/noctalia.json new file mode 100644 index 0000000..16aef1c --- /dev/null +++ b/Templates/templates/noctalia.json @@ -0,0 +1,28 @@ +{ + "backgroundPrimary": "{{ background }}", + "backgroundSecondary": "{{ background | lighten(0.05) }}", + "backgroundTertiary": "{{ background | lighten(0.1) }}", + + "surface": "{{ background | lighten(0.08) }}", + "surfaceVariant": "{{ background | lighten(0.15) }}", + + "textPrimary": "{{ foreground }}", + "textSecondary": "{{ foreground | darken(0.1) }}", + "textDisabled": "{{ foreground | darken(0.4) }}", + + "accentPrimary": "{{ color4 }}", + "accentSecondary": "{{ color4 | lighten(0.2) }}", + "accentTertiary": "{{ color4 | darken(0.2) }}", + + "error": "{{ color5 | lighten(0.1) }}", + "warning": "{{ color5 | lighten(0.3) }}", + + "highlight": "{{ color4 | lighten(0.4) }}", + "rippleEffect": "{{ color4 | lighten(0.1) }}", + + "onAccent": "{{ background }}", + "outline": "{{ background | lighten(0.3) }}", + + "shadow": "{{ background }}", + "overlay": "{{ background }}" +} diff --git a/Templates/wallust.toml b/Templates/wallust.toml new file mode 100644 index 0000000..cbd3593 --- /dev/null +++ b/Templates/wallust.toml @@ -0,0 +1,47 @@ +# wallust v3.4 +# +# You can copy this file to ~/.config/wallust/wallust.toml (keep in mind is a sample config) + +# SIMPLE TUTORIAL, or `man wallust.5`: +# https://explosion-mental.codeberg.page/wallust/ +# +# If comming from v2: https://explosion-mental.codeberg.page/wallust/v3.html#wallusttoml + +# Global section - values below can be overwritten by command line flags + +# How the image is parse, in order to get the colors: +# full - resized - wal - thumb - fastresize - kmeans +backend = "fastresize" + +# What color space to use to produce and select the most prominent colors: +# lab - labmixed - lch - lchmixed +color_space = "lch" + +# Use the most prominent colors in a way that makes sense, a scheme color palette: +# dark - dark16 - darkcomp - darkcomp16 +# light - light16 - lightcomp - lightcomp16 +# harddark - harddark16 - harddarkcomp - harddarkcomp16 +# softdark - softdark16 - softdarkcomp - softdarkcomp16 +# softlight - softlight16 - softlightcomp - softlightcomp16 +palette = "dark" + +# Ensures a "readable contrast" (OPTIONAL, disabled by default) +# Should only be enabled when you notice an unreadable contrast frequently happening +# with your images. The reference color for the contrast is the background color. +#check_contrast = true + +# Color saturation, between [1% and 100%] (OPTIONAL, disabled by default) +# usually something higher than 50 increases the saturation and below +# decreases it (on a scheme with strong and vivid colors) +#saturation = 35 + +# Alpha value for templating, by default 100 (no other use whatsoever) +#alpha = 100 + +[templates] +# NOTE: prefer '' over "" for paths, avoids escaping. +# template: A RELATIVE path that points to `~/.config/wallust/template` (depends on platform) +# target: ABSOLUTE path in which to place a file with generated templated values. +# ¡ If either one is a directory, then both SHOULD be one. ! +# zathura = { template = 'zathura', target = '~/.config/zathura/zathurarc' } +Noctalia = { template = 'noctalia.json', target = '~/.config/noctalia/Theme.json' } From 8a16b6b7f567416668ee51c18864a0306a9588fb Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 12:07:51 -0400 Subject: [PATCH 188/394] Formatting --- Modules/Background/Background.qml | 5 +- Modules/Background/Overview.qml | 3 +- Modules/Settings/SettingsWindow.qml | 6 +- Modules/Settings/Tabs/Display.qml | 2 +- Modules/Settings/Tabs/ScreenRecorder.qml | 2 +- Services/Settings.qml | 12 +- Services/Wallpapers.qml | 12 +- Services/Workspaces.qml | 277 ++++++++++++----------- 8 files changed, 163 insertions(+), 156 deletions(-) diff --git a/Modules/Background/Background.qml b/Modules/Background/Background.qml index 20752c9..4e2175e 100644 --- a/Modules/Background/Background.qml +++ b/Modules/Background/Background.qml @@ -8,10 +8,11 @@ Variants { delegate: PanelWindow { required property ShellScreen modelData - property string wallpaperSource: Wallpapers.currentWallpaper !== "" && !Settings.data.wallpaper.swww.enabled ? Wallpapers.currentWallpaper : "" + property string wallpaperSource: Wallpapers.currentWallpaper !== "" + && !Settings.data.wallpaper.swww.enabled ? Wallpapers.currentWallpaper : "" visible: wallpaperSource !== "" && !Settings.data.wallpaper.swww.enabled - + // Force update when SWWW setting changes onVisibleChanged: { if (visible) { diff --git a/Modules/Background/Overview.qml b/Modules/Background/Overview.qml index d2a7aad..1f64f7c 100644 --- a/Modules/Background/Overview.qml +++ b/Modules/Background/Overview.qml @@ -9,7 +9,8 @@ Variants { delegate: PanelWindow { required property ShellScreen modelData - property string wallpaperSource: Wallpapers.currentWallpaper !== "" && !Settings.data.wallpaper.swww.enabled ? Wallpapers.currentWallpaper : "" + property string wallpaperSource: Wallpapers.currentWallpaper !== "" + && !Settings.data.wallpaper.swww.enabled ? Wallpapers.currentWallpaper : "" visible: wallpaperSource !== "" && !Settings.data.wallpaper.swww.enabled color: "transparent" diff --git a/Modules/Settings/SettingsWindow.qml b/Modules/Settings/SettingsWindow.qml index c05e32b..cae584f 100644 --- a/Modules/Settings/SettingsWindow.qml +++ b/Modules/Settings/SettingsWindow.qml @@ -46,13 +46,11 @@ NLoader { "label": "Wallpaper", "icon": "image", "source": "Tabs/Wallpaper.qml" - }, - { + }, { "label": "Wallpaper Selector", "icon": "wallpaper_slideshow", "source": "Tabs/WallpaperSelector.qml" - }, - { + }, { "label": "Misc", "icon": "more_horiz", "source": "Tabs/Misc.qml" diff --git a/Modules/Settings/Tabs/Display.qml b/Modules/Settings/Tabs/Display.qml index 86dea3d..ea587d7 100644 --- a/Modules/Settings/Tabs/Display.qml +++ b/Modules/Settings/Tabs/Display.qml @@ -116,4 +116,4 @@ Item { Layout.fillHeight: true } } -} \ No newline at end of file +} diff --git a/Modules/Settings/Tabs/ScreenRecorder.qml b/Modules/Settings/Tabs/ScreenRecorder.qml index d102ce9..286908f 100644 --- a/Modules/Settings/Tabs/ScreenRecorder.qml +++ b/Modules/Settings/Tabs/ScreenRecorder.qml @@ -324,4 +324,4 @@ ColumnLayout { } } } -} \ No newline at end of file +} diff --git a/Services/Settings.qml b/Services/Settings.qml index 7e7b0b4..9420b82 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -188,8 +188,14 @@ Singleton { Connections { target: adapter.wallpaper - function onIsRandomChanged() { Wallpapers.toggleRandomWallpaper() } - function onRandomIntervalChanged() { Wallpapers.restartRandomWallpaperTimer() } - function onDirectoryChanged() { Wallpapers.loadWallpapers() } + function onIsRandomChanged() { + Wallpapers.toggleRandomWallpaper() + } + function onRandomIntervalChanged() { + Wallpapers.restartRandomWallpaperTimer() + } + function onDirectoryChanged() { + Wallpapers.loadWallpapers() + } } } diff --git a/Services/Wallpapers.qml b/Services/Wallpapers.qml index e586a09..f087da4 100644 --- a/Services/Wallpapers.qml +++ b/Services/Wallpapers.qml @@ -138,12 +138,12 @@ Singleton { ), "--transition-type", transitionType, "--transition-duration", Settings.data.wallpaper.swww.transitionDuration.toString( ), currentWallpaper] running: false - + onStarted: { console.log("SWWW: Process started with command:", command.join(" ")) } - - onExited: function(exitCode, exitStatus) { + + onExited: function (exitCode, exitStatus) { console.log("SWWW: Process finished with exit code:", exitCode, "status:", exitStatus) if (exitCode !== 0) { console.log("SWWW: Process failed. Make sure swww-daemon is running with: swww-daemon") @@ -163,12 +163,12 @@ Singleton { id: startDaemonProcess command: ["swww-daemon", "--format", "xrgb"] running: false - + onStarted: { console.log("SWWW: Daemon start process initiated") } - - onExited: function(exitCode, exitStatus) { + + onExited: function (exitCode, exitStatus) { console.log("SWWW: Daemon start process finished with exit code:", exitCode) if (exitCode === 0) { console.log("SWWW: Daemon started successfully") diff --git a/Services/Workspaces.qml b/Services/Workspaces.qml index 9bedc6b..b73713d 100644 --- a/Services/Workspaces.qml +++ b/Services/Workspaces.qml @@ -1,5 +1,6 @@ pragma Singleton -pragma ComponentBehavior: Bound + +pragma ComponentBehavior import QtQuick import Quickshell @@ -8,149 +9,149 @@ import Quickshell.Hyprland import qs.Services Singleton { - id: root + id: root - property ListModel workspaces: ListModel {} - property bool isHyprland: false - property bool isNiri: false - property var hlWorkspaces: Hyprland.workspaces.values - // Detect which compositor we're using - Component.onCompleted: { - console.log("WorkspaceManager initializing..."); - detectCompositor(); - } + property ListModel workspaces: ListModel {} + property bool isHyprland: false + property bool isNiri: false + property var hlWorkspaces: Hyprland.workspaces.values + // Detect which compositor we're using + Component.onCompleted: { + console.log("WorkspaceManager initializing...") + detectCompositor() + } - function detectCompositor() { - try { - try { - if (Hyprland.eventSocketPath) { - console.log("Detected Hyprland compositor"); - isHyprland = true; - isNiri = false; - initHyprland(); - return; - } - } catch (e) { - console.log("Hyprland not available:", e); - } - - if (typeof Niri !== "undefined") { - console.log("Detected Niri service"); - isHyprland = false; - isNiri = true; - initNiri(); - return; - } - - console.log("No supported compositor detected"); - } catch (e) { - console.error("Error detecting compositor:", e); + function detectCompositor() { + try { + try { + if (Hyprland.eventSocketPath) { + console.log("Detected Hyprland compositor") + isHyprland = true + isNiri = false + initHyprland() + return } - } + } catch (e) { + console.log("Hyprland not available:", e) + } - // Initialize Hyprland integration - function initHyprland() { - try { - // Fixes the odd workspace issue. - Hyprland.refreshWorkspaces(); - // hlWorkspaces = Hyprland.workspaces.values; - // updateHyprlandWorkspaces(); - return true; - } catch (e) { - console.error("Error initializing Hyprland:", e); - isHyprland = false; - return false; + if (typeof Niri !== "undefined") { + console.log("Detected Niri service") + isHyprland = false + isNiri = true + initNiri() + return + } + + console.log("No supported compositor detected") + } catch (e) { + console.error("Error detecting compositor:", e) + } + } + + // Initialize Hyprland integration + function initHyprland() { + try { + // Fixes the odd workspace issue. + Hyprland.refreshWorkspaces() + // hlWorkspaces = Hyprland.workspaces.values; + // updateHyprlandWorkspaces(); + return true + } catch (e) { + console.error("Error initializing Hyprland:", e) + isHyprland = false + return false + } + } + + onHlWorkspacesChanged: { + updateHyprlandWorkspaces() + } + + Connections { + target: Hyprland.workspaces + function onValuesChanged() { + updateHyprlandWorkspaces() + } + } + + Connections { + target: Hyprland + function onRawEvent(event) { + updateHyprlandWorkspaces() + } + } + + function updateHyprlandWorkspaces() { + workspaces.clear() + try { + for (var i = 0; i < hlWorkspaces.length; i++) { + const ws = hlWorkspaces[i] + // Only append workspaces with id >= 1 + if (ws.id >= 1) { + workspaces.append({ + "id": i, + "idx": ws.id, + "name": ws.name || "", + "output": ws.monitor?.name || "", + "isActive": ws.active === true, + "isFocused": ws.focused === true, + "isUrgent": ws.urgent === true + }) } + } + workspacesChanged() + } catch (e) { + console.error("Error updating Hyprland workspaces:", e) + } + } + + function initNiri() { + updateNiriWorkspaces() + } + + Connections { + target: Niri + function onWorkspacesChanged() { + updateNiriWorkspaces() + } + } + + function updateNiriWorkspaces() { + const niriWorkspaces = Niri.workspaces || [] + workspaces.clear() + for (var i = 0; i < niriWorkspaces.length; i++) { + const ws = niriWorkspaces[i] + workspaces.append({ + "id": ws.id, + "idx": ws.idx || 1, + "name": ws.name || "", + "output": ws.output || "", + "isFocused": ws.isFocused === true, + "isActive": ws.isActive === true, + "isUrgent": ws.isUrgent === true, + "isOccupied": ws.isOccupied === true + }) } - onHlWorkspacesChanged: { - updateHyprlandWorkspaces(); - } + workspacesChanged() + } - Connections { - target: Hyprland.workspaces - function onValuesChanged() { - updateHyprlandWorkspaces(); - } + function switchToWorkspace(workspaceId) { + if (isHyprland) { + try { + Hyprland.dispatch(`workspace ${workspaceId}`) + } catch (e) { + console.error("Error switching Hyprland workspace:", e) + } + } else if (isNiri) { + try { + Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", workspaceId.toString()]) + } catch (e) { + console.error("Error switching Niri workspace:", e) + } + } else { + console.warn("No supported compositor detected for workspace switching") } - - Connections { - target: Hyprland - function onRawEvent(event) { - updateHyprlandWorkspaces(); - } - } - - function updateHyprlandWorkspaces() { - workspaces.clear(); - try { - for (let i = 0; i < hlWorkspaces.length; i++) { - const ws = hlWorkspaces[i]; - // Only append workspaces with id >= 1 - if (ws.id >= 1) { - workspaces.append({ - id: i, - idx: ws.id, - name: ws.name || "", - output: ws.monitor?.name || "", - isActive: ws.active === true, - isFocused: ws.focused === true, - isUrgent: ws.urgent === true - }); - } - } - workspacesChanged(); - } catch (e) { - console.error("Error updating Hyprland workspaces:", e); - } - } - - function initNiri() { - updateNiriWorkspaces(); - } - - Connections { - target: Niri - function onWorkspacesChanged() { - updateNiriWorkspaces(); - } - } - - function updateNiriWorkspaces() { - const niriWorkspaces = Niri.workspaces || []; - workspaces.clear(); - for (let i = 0; i < niriWorkspaces.length; i++) { - const ws = niriWorkspaces[i]; - workspaces.append({ - id: ws.id, - idx: ws.idx || 1, - name: ws.name || "", - output: ws.output || "", - isFocused: ws.isFocused === true, - isActive: ws.isActive === true, - isUrgent: ws.isUrgent === true, - isOccupied: ws.isOccupied === true, - }); - } - - workspacesChanged(); - } - - function switchToWorkspace(workspaceId) { - if (isHyprland) { - try { - Hyprland.dispatch(`workspace ${workspaceId}`); - } catch (e) { - console.error("Error switching Hyprland workspace:", e); - } - } else if (isNiri) { - try { - Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", workspaceId.toString()]); - } catch (e) { - console.error("Error switching Niri workspace:", e); - } - } else { - console.warn("No supported compositor detected for workspace switching"); - } - } -} \ No newline at end of file + } +} From a7d4e0ec1d1d2ce252491336c0daf19e4683ca93 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 13:22:15 -0400 Subject: [PATCH 189/394] Replaced all NWidgets callback by proper signals --- Modules/Bar/Bar.qml | 4 +-- Modules/Bar/Clock.qml | 6 ++--- Modules/Bar/Volume.qml | 2 +- Modules/Bar/WiFi.qml | 2 +- Modules/Bar/WiFiMenu.qml | 4 +-- Modules/Calendar/Calendar.qml | 4 +-- Modules/DemoPanel/DemoPanel.qml | 9 ++++--- Modules/Notification/Notification.qml | 2 +- Modules/Settings/SettingsWindow.qml | 4 +-- Modules/Settings/Tabs/General.qml | 2 +- Modules/Settings/Tabs/ScreenRecorder.qml | 2 +- Modules/Settings/Tabs/TimeWeather.qml | 2 +- Modules/Settings/Tabs/Wallpaper.qml | 2 +- Modules/Settings/Tabs/WallpaperSelector.qml | 19 +++++++------- Modules/SidePanel/Cards/PowerProfilesCard.qml | 12 ++++++--- Modules/SidePanel/Cards/ProfileCard.qml | 2 +- Services/Github.qml | 4 +-- Services/Location.qml | 2 +- Services/Settings.qml | 4 +-- Widgets/NClock.qml | 13 +++++----- Widgets/NComboBox.qml | 5 ++-- Widgets/NIconButton.qml | 13 +++++----- Widgets/NPanel.qml | 17 ++++++------ Widgets/NPill.qml | 26 +++++++++---------- Widgets/NSlider.qml | 9 +------ Widgets/NTextInput.qml | 5 ++-- Widgets/NToggle.qml | 5 ++-- 27 files changed, 93 insertions(+), 88 deletions(-) diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 4db35a7..9ad9815 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -109,7 +109,7 @@ Variants { sizeMultiplier: 0.8 showBorder: false anchors.verticalCenter: parent.verticalCenter - onClicked: function () { + onClicked: { demoPanel.isLoaded = !demoPanel.isLoaded } } @@ -121,7 +121,7 @@ Variants { sizeMultiplier: 0.8 showBorder: false anchors.verticalCenter: parent.verticalCenter - onClicked: function () { + onClicked: { // Map this button's center to the screen and open the side panel below it const localCenterX = width / 2 const localCenterY = height / 2 diff --git a/Modules/Bar/Clock.qml b/Modules/Bar/Clock.qml index 6e05d79..19d25e8 100644 --- a/Modules/Bar/Clock.qml +++ b/Modules/Bar/Clock.qml @@ -12,15 +12,15 @@ NClock { target: root } - onEntered: function () { + onEntered: { if (!calendar.isLoaded) { tooltip.show() } } - onExited: function () { + onExited: { tooltip.hide() } - onClicked: function () { + onClicked: { tooltip.hide() calendar.isLoaded = !calendar.isLoaded } diff --git a/Modules/Bar/Volume.qml b/Modules/Bar/Volume.qml index f8f69d7..ee7b690 100644 --- a/Modules/Bar/Volume.qml +++ b/Modules/Bar/Volume.qml @@ -70,7 +70,7 @@ Item { Audio.volumeDecrement() } } - onClicked: function () { + onClicked: { audioDeviceSelector.isLoaded = !audioDeviceSelector.isLoaded } } diff --git a/Modules/Bar/WiFi.qml b/Modules/Bar/WiFi.qml index 059736f..f494a76 100644 --- a/Modules/Bar/WiFi.qml +++ b/Modules/Bar/WiFi.qml @@ -24,7 +24,7 @@ NIconButton { return connected ? network.signalIcon(parent.currentSignal) : "wifi_off" } tooltipText: "WiFi Networks" - onClicked: function () { + onClicked: { if (!wifiMenuLoader.active) { wifiMenuLoader.isLoaded = true } diff --git a/Modules/Bar/WiFiMenu.qml b/Modules/Bar/WiFiMenu.qml index dad6c9d..6431007 100644 --- a/Modules/Bar/WiFiMenu.qml +++ b/Modules/Bar/WiFiMenu.qml @@ -78,7 +78,7 @@ NLoader { NIconButton { icon: "refresh" sizeMultiplier: 0.8 - onClicked: function () { + onClicked: { network.refreshNetworks() } } @@ -86,7 +86,7 @@ NLoader { NIconButton { icon: "close" sizeMultiplier: 0.8 - onClicked: function () { + onClicked: { wifiPanel.visible = false network.onMenuClosed() } diff --git a/Modules/Calendar/Calendar.qml b/Modules/Calendar/Calendar.qml index ad05d47..fba498f 100644 --- a/Modules/Calendar/Calendar.qml +++ b/Modules/Calendar/Calendar.qml @@ -47,7 +47,7 @@ NLoader { NIconButton { icon: "chevron_left" - onClicked: function () { + onClicked: { let newDate = new Date(grid.year, grid.month - 1, 1) grid.year = newDate.getFullYear() grid.month = newDate.getMonth() @@ -65,7 +65,7 @@ NLoader { NIconButton { icon: "chevron_right" - onClicked: function () { + onClicked: { let newDate = new Date(grid.year, grid.month + 1, 1) grid.year = newDate.getFullYear() grid.month = newDate.getMonth() diff --git a/Modules/DemoPanel/DemoPanel.qml b/Modules/DemoPanel/DemoPanel.qml index 634ebda..0d37f4e 100644 --- a/Modules/DemoPanel/DemoPanel.qml +++ b/Modules/DemoPanel/DemoPanel.qml @@ -73,17 +73,17 @@ NLoader { stepSize: 0.01 value: Scaling.overrideScale implicitWidth: bgRect.width * 0.75 - onMoved: function () { + onMoved: { Scaling.overrideScale = value } - onPressedChanged: function () { + onPressedChanged: { Scaling.overrideEnabled = true } } NIconButton { icon: "refresh" fontPointSize: Style.fontSizeXL * scaling - onClicked: function () { + onClicked: { Scaling.overrideEnabled = false Scaling.overrideScale = 1.0 } @@ -171,8 +171,9 @@ NLoader { NTextInput { text: "Type anything" Layout.fillWidth: true - onEditingFinished: function () {} + onEditingFinished: { + } NDivider { Layout.fillWidth: true } diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index 7adaf9c..40a3cff 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -191,7 +191,7 @@ PanelWindow { anchors.right: parent.right anchors.margins: Style.marginSmall * scaling icon: "close" - onClicked: function () { + onClicked: { animateOut() } } diff --git a/Modules/Settings/SettingsWindow.qml b/Modules/Settings/SettingsWindow.qml index cae584f..ca8a4da 100644 --- a/Modules/Settings/SettingsWindow.qml +++ b/Modules/Settings/SettingsWindow.qml @@ -60,7 +60,7 @@ NLoader { "source": "Tabs/About.qml" }] - onVisibleChanged: function () { + onVisibleChanged: { if (visible) currentTabIndex = 0 } @@ -188,7 +188,7 @@ NLoader { icon: "close" tooltipText: "Close settings panel" Layout.alignment: Qt.AlignVCenter - onClicked: function () { + onClicked: { settingsWindow.isLoaded = !settingsWindow.isLoaded } } diff --git a/Modules/Settings/Tabs/General.qml b/Modules/Settings/Tabs/General.qml index 2d71032..9e4e08f 100644 --- a/Modules/Settings/Tabs/General.qml +++ b/Modules/Settings/Tabs/General.qml @@ -84,7 +84,7 @@ ColumnLayout { text: Settings.data.general.avatarImage placeholderText: "/home/user/.face" Layout.fillWidth: true - onEditingFinished: function () { + onEditingFinished: { Settings.data.general.avatarImage = text } } diff --git a/Modules/Settings/Tabs/ScreenRecorder.qml b/Modules/Settings/Tabs/ScreenRecorder.qml index 286908f..6376087 100644 --- a/Modules/Settings/Tabs/ScreenRecorder.qml +++ b/Modules/Settings/Tabs/ScreenRecorder.qml @@ -65,7 +65,7 @@ ColumnLayout { NTextInput { text: Settings.data.screenRecorder.directory Layout.fillWidth: true - onEditingFinished: function () { + onEditingFinished: { Settings.data.screenRecorder.directory = text } } diff --git a/Modules/Settings/Tabs/TimeWeather.qml b/Modules/Settings/Tabs/TimeWeather.qml index a5f1b1f..f8d43d7 100644 --- a/Modules/Settings/Tabs/TimeWeather.qml +++ b/Modules/Settings/Tabs/TimeWeather.qml @@ -58,7 +58,7 @@ ColumnLayout { text: Settings.data.location.name placeholderText: "Enter city name" Layout.fillWidth: true - onEditingFinished: function () { + onEditingFinished: { Settings.data.location.name = text } } diff --git a/Modules/Settings/Tabs/Wallpaper.qml b/Modules/Settings/Tabs/Wallpaper.qml index 430b636..609efe6 100644 --- a/Modules/Settings/Tabs/Wallpaper.qml +++ b/Modules/Settings/Tabs/Wallpaper.qml @@ -70,7 +70,7 @@ ColumnLayout { NTextInput { text: Settings.data.wallpaper.directory Layout.fillWidth: true - onEditingFinished: function () { + onEditingFinished: { Settings.data.wallpaper.directory = text } } diff --git a/Modules/Settings/Tabs/WallpaperSelector.qml b/Modules/Settings/Tabs/WallpaperSelector.qml index 281887d..2f743d0 100644 --- a/Modules/Settings/Tabs/WallpaperSelector.qml +++ b/Modules/Settings/Tabs/WallpaperSelector.qml @@ -83,9 +83,8 @@ Item { } NText { - text: Settings.data.wallpaper.swww.enabled ? - "Wallpapers will change with " + Settings.data.wallpaper.swww.transitionType + " transition" : - "Wallpapers will change instantly" + text: Settings.data.wallpaper.swww.enabled ? "Wallpapers will change with " + Settings.data.wallpaper.swww.transitionType + + " transition" : "Wallpapers will change instantly" color: Colors.textSecondary font.pointSize: Style.fontSizeSmall * scaling visible: Settings.data.wallpaper.swww.enabled @@ -128,13 +127,13 @@ Item { anchors.fill: parent clip: true model: folderModel - + // Fixed 5 items per row - more aggressive sizing property int itemSize: Math.floor((width - leftMargin - rightMargin - (4 * Style.marginSmall * scaling)) / 5) - + cellWidth: Math.floor((width - leftMargin - rightMargin) / 5) cellHeight: Math.floor(itemSize * 0.67) + Style.marginSmall * scaling - + leftMargin: Style.marginSmall * scaling rightMargin: Style.marginSmall * scaling topMargin: Style.marginSmall * scaling @@ -191,9 +190,11 @@ Item { color: Colors.textPrimary opacity: mouseArea.containsMouse ? 0.1 : 0 radius: parent.radius - + Behavior on opacity { - NumberAnimation { duration: 150 } + NumberAnimation { + duration: 150 + } } } @@ -249,4 +250,4 @@ Item { } } } -} \ No newline at end of file +} diff --git a/Modules/SidePanel/Cards/PowerProfilesCard.qml b/Modules/SidePanel/Cards/PowerProfilesCard.qml index 6fd612d..6b74b24 100644 --- a/Modules/SidePanel/Cards/PowerProfilesCard.qml +++ b/Modules/SidePanel/Cards/PowerProfilesCard.qml @@ -21,17 +21,23 @@ NBox { // Performance NIconButton { icon: "speed" - onClicked: function () {/* TODO: hook to power profile */ } + onClicked: { + + /* TODO: hook to power profile */ } } // Balanced NIconButton { icon: "balance" - onClicked: function () {/* TODO: hook to power profile */ } + onClicked: { + + /* TODO: hook to power profile */ } } // Eco NIconButton { icon: "eco" - onClicked: function () {/* TODO: hook to power profile */ } + onClicked: { + + /* TODO: hook to power profile */ } } Item { Layout.fillWidth: true diff --git a/Modules/SidePanel/Cards/ProfileCard.qml b/Modules/SidePanel/Cards/ProfileCard.qml index 9d7f638..ac9041a 100644 --- a/Modules/SidePanel/Cards/ProfileCard.qml +++ b/Modules/SidePanel/Cards/ProfileCard.qml @@ -59,7 +59,7 @@ NBox { } NIconButton { icon: "settings" - onClicked: function () { + onClicked: { if (!root.settingsWindow) { const comp = Qt.createComponent("../../Settings/SettingsWindow.qml") if (comp.status === Component.Ready) { diff --git a/Services/Github.qml b/Services/Github.qml index a1409a6..8a4bc1e 100644 --- a/Services/Github.qml +++ b/Services/Github.qml @@ -23,10 +23,10 @@ Singleton { watchChanges: true onFileChanged: reload() onAdapterUpdated: writeAdapter() - Component.onCompleted: function () { + Component.onCompleted: { reload() } - onLoaded: function () { + onLoaded: { loadFromCache() } onLoadFailed: function (error) { diff --git a/Services/Location.qml b/Services/Location.qml index 1c6e3cd..70587b4 100644 --- a/Services/Location.qml +++ b/Services/Location.qml @@ -16,7 +16,7 @@ Singleton { FileView { path: locationFile onAdapterUpdated: writeAdapter() - onLoaded: function () { + onLoaded: { updateWeather() } onLoadFailed: function (error) { diff --git a/Services/Settings.qml b/Services/Settings.qml index 9420b82..e85f471 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -47,10 +47,10 @@ Singleton { watchChanges: true onFileChanged: reload() onAdapterUpdated: writeAdapter() - Component.onCompleted: function () { + Component.onCompleted: { reload() } - onLoaded: function () { + onLoaded: { Qt.callLater(function () { if (adapter.wallpaper.current !== "") { console.log("Settings: Initializing wallpaper to:", adapter.wallpaper.current) diff --git a/Widgets/NClock.qml b/Widgets/NClock.qml index e1c4bd8..c249c01 100644 --- a/Widgets/NClock.qml +++ b/Widgets/NClock.qml @@ -6,9 +6,10 @@ Rectangle { id: root readonly property real scaling: Scaling.scale(screen) - property var onEntered: function () {} - property var onExited: function () {} - property var onClicked: function () {} + + signal entered + signal exited + signal clicked width: textItem.paintedWidth height: textItem.paintedHeight @@ -26,8 +27,8 @@ Rectangle { anchors.fill: parent cursorShape: Qt.PointingHandCursor hoverEnabled: true - onEntered: root.onEntered() - onExited: root.onExited() - onClicked: root.onClicked() + onEntered: root.entered() + onExited: root.exited() + onClicked: root.clicked() } } diff --git a/Widgets/NComboBox.qml b/Widgets/NComboBox.qml index 0eba662..0b27fa6 100644 --- a/Widgets/NComboBox.qml +++ b/Widgets/NComboBox.qml @@ -13,7 +13,8 @@ ComboBox { property list optionsKeys: [] property list optionsLabels: [] property string currentKey: '' - property var onSelected: function (string) {} + + signal selected(string key) Layout.fillWidth: true Layout.preferredHeight: height @@ -21,7 +22,7 @@ ComboBox { model: optionsKeys currentIndex: model.indexOf(currentKey) onActivated: { - root.onSelected(model[currentIndex]) + root.selected(model[currentIndex]) } // Rounded background diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml index 04de320..b42f2ec 100644 --- a/Widgets/NIconButton.qml +++ b/Widgets/NIconButton.qml @@ -15,12 +15,13 @@ Rectangle { property bool showBorder: true property bool enabled: true property bool hovering: false - property var onEntered: function () {} - property var onExited: function () {} - property var onClicked: function () {} property real fontPointSize: Style.fontSizeMedium property string fontFamily: "Material Symbols Outlined" + signal entered + signal exited + signal clicked + implicitWidth: size implicitHeight: size @@ -62,20 +63,20 @@ Rectangle { if (tooltipText) { tooltip.show() } - root.onEntered() + root.entered() } onExited: { hovering = false if (tooltipText) { tooltip.hide() } - root.onExited() + root.exited() } onClicked: { if (tooltipText) { tooltip.hide() } - root.onClicked() + root.clicked() } } } diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index f0613f5..704defc 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -4,26 +4,27 @@ import Quickshell.Wayland import qs.Services PanelWindow { - id: outerPanel + id: root readonly property real scaling: Scaling.scale(screen) property bool showOverlay: Settings.data.general.dimDesktop property int topMargin: Style.barHeight * scaling property color overlayColor: showOverlay ? Colors.overlay : "transparent" + signal dismissed function hide() { //visible = false - dismissed() + root.dismissed() } function show() { // Ensure only one panel is visible at a time using Settings as ephemeral store try { - if (Settings.openPanel && Settings.openPanel !== outerPanel && Settings.openPanel.hide) { + if (Settings.openPanel && Settings.openPanel !== root && Settings.openPanel.hide) { Settings.openPanel.hide() } - Settings.openPanel = outerPanel + Settings.openPanel = root } catch (e) { // ignore @@ -45,7 +46,7 @@ PanelWindow { MouseArea { anchors.fill: parent - onClicked: outerPanel.hide() + onClicked: root.hide() } Behavior on color { @@ -57,16 +58,16 @@ PanelWindow { Component.onDestruction: { try { - if (visible && Settings.openPanel === outerPanel) + if (visible && Settings.openPanel === root) Settings.openPanel = null } catch (e) { } } - onVisibleChanged: function () { + onVisibleChanged: { try { - if (!visible && Settings.openPanel === outerPanel) + if (!visible && Settings.openPanel === root) Settings.openPanel = null } catch (e) { diff --git a/Widgets/NPill.qml b/Widgets/NPill.qml index 3a6ac0e..05c3a3e 100644 --- a/Widgets/NPill.qml +++ b/Widgets/NPill.qml @@ -18,10 +18,12 @@ Item { property real sizeMultiplier: 0.8 property bool autoHide: false - property var onEntered: function () {} - property var onExited: function () {} - property var onClicked: function () {} - property var onWheel: function (delta) {} + signal shown + signal hidden + signal entered + signal exited + signal clicked + signal wheel(int delta) // Internal state property bool showPill: false @@ -34,10 +36,6 @@ Item { readonly property int pillOverlap: iconSize * 0.5 readonly property int maxPillWidth: Math.max(1, textItem.implicitWidth + pillPaddingHorizontal * 2 + pillOverlap) - // TBC, do we use those ? - signal shown - signal hidden - width: iconSize + (showPill ? maxPillWidth - pillOverlap : 0) height: pillHeight @@ -127,7 +125,7 @@ Item { } onStopped: { delayedHideAnim.start() - shown() + root.shown() } } @@ -166,7 +164,7 @@ Item { onStopped: { showPill = false shouldAnimateHide = false - hidden() + root.hidden() } } @@ -194,18 +192,18 @@ Item { onEntered: { showDelayed() tooltip.show() - root.onEntered() + root.entered() } onExited: { hide() tooltip.hide() - root.onExited() + root.exited() } onClicked: { - root.onClicked() + root.clicked() } onWheel: wheel => { - root.onWheel(wheel.angleDelta.y) + root.wheel(wheel.angleDelta.y) } } diff --git a/Widgets/NSlider.qml b/Widgets/NSlider.qml index 3afc394..2e9e6d1 100644 --- a/Widgets/NSlider.qml +++ b/Widgets/NSlider.qml @@ -35,14 +35,6 @@ Slider { height: parent.height color: Colors.accentPrimary radius: parent.radius - - // Feels more responsive without animation - // Behavior on width { - // NumberAnimation { - // duration: 50 - // easing.type: Easing.OutQuad - // } - // } } // Circular cutout @@ -84,6 +76,7 @@ Slider { color: root.pressed ? Colors.surfaceVariant : Colors.surface border.color: Colors.accentPrimary border.width: Math.max(1, Style.borderThick * scaling) + // Press feedback halo (using accent color, low opacity) Rectangle { anchors.centerIn: parent diff --git a/Widgets/NTextInput.qml b/Widgets/NTextInput.qml index b2e00bf..cceadcf 100644 --- a/Widgets/NTextInput.qml +++ b/Widgets/NTextInput.qml @@ -13,7 +13,8 @@ Item { property alias placeholderText: input.placeholderText property bool readOnly: false property bool enabled: true - property var onEditingFinished: function () {} + + signal editingFinished // Sizing implicitHeight: Style.baseWidgetSize * 1.25 * scaling @@ -55,7 +56,7 @@ Item { placeholderTextColor: Colors.textSecondary background: null font.pointSize: Style.fontSizeSmall * scaling - onEditingFinished: root.onEditingFinished() + 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. } diff --git a/Widgets/NToggle.qml b/Widgets/NToggle.qml index 5150cd2..55d4e2e 100644 --- a/Widgets/NToggle.qml +++ b/Widgets/NToggle.qml @@ -12,7 +12,8 @@ RowLayout { property bool value: false property bool hovering: false property int baseSize: Style.baseWidgetSize - property var onToggled: function (value) {} + + signal toggled(bool balue) Layout.fillWidth: true @@ -72,7 +73,7 @@ RowLayout { onExited: hovering = false onClicked: { value = !value - root.onToggled(value) + root.toggled(value) } } } From 37c3dacff46f00eb65dcfd83f86453e803c65caa Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Tue, 12 Aug 2025 19:35:50 +0200 Subject: [PATCH 190/394] Add wallust support --- Assets/Wallust/Templates/noctalia.json | 26 ------ Assets/Wallust/templates/noctalia.json | 26 ++++++ Assets/Wallust/wallust.toml | 2 +- Modules/Background/Background.qml | 4 +- Modules/Settings/Tabs/Wallpaper.qml | 2 +- Services/Audio.qml | 2 +- Services/Colors.qml | 112 ++++++++++++++++++++----- Services/Settings.qml | 2 +- Services/Wallpapers.qml | 10 +-- Templates/templates/noctalia.json | 28 ------- Templates/wallust.toml | 47 ----------- 11 files changed, 126 insertions(+), 135 deletions(-) delete mode 100644 Assets/Wallust/Templates/noctalia.json create mode 100644 Assets/Wallust/templates/noctalia.json delete mode 100644 Templates/templates/noctalia.json delete mode 100644 Templates/wallust.toml diff --git a/Assets/Wallust/Templates/noctalia.json b/Assets/Wallust/Templates/noctalia.json deleted file mode 100644 index f10f03e..0000000 --- a/Assets/Wallust/Templates/noctalia.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "backgroundPrimary": "{{ background }}", - "backgroundSecondary": "{{ background | lighten(0.05) }}", - "backgroundTertiary": "{{ background | lighten(0.1) }}", - - "surface": "{{ background | lighten(0.08) }}", - "surfaceVariant": "{{ background | lighten(0.15) }}", - - "textPrimary": "{{ foreground }}", - "textSecondary": "{{ foreground | darken(0.1) }}", - "textDisabled": "{{ foreground | darken(0.4) }}", - - "accentPrimary": "{{ color4 }}", - "accentSecondary": "{{ color4 | lighten(0.2) }}", - "accentTertiary": "{{ color4 | darken(0.2) }}", - - "error": "{{ color5 | lighten(0.1) }}", - "warning": "{{ color5 | lighten(0.3) }}", - - "highlight": "{{ color4 | lighten(0.4) }}", - "onAccent": "{{ background }}", - "outline": "{{ background | lighten(0.3) }}", - - "shadow": "{{ background }}", - "overlay": "{{ background }}" -} \ No newline at end of file diff --git a/Assets/Wallust/templates/noctalia.json b/Assets/Wallust/templates/noctalia.json new file mode 100644 index 0000000..dd3e547 --- /dev/null +++ b/Assets/Wallust/templates/noctalia.json @@ -0,0 +1,26 @@ +{ + "backgroundPrimary": "{{ background }}", + "backgroundSecondary": "{{ background | lighten(0.03) }}", + "backgroundTertiary": "{{ background | lighten(0.06) }}", + + "surface": "{{ background | lighten(0.04) }}", + "surfaceVariant": "{{ background | lighten(0.08) }}", + + "textPrimary": "{{ foreground | darken(0.1) }}", + "textSecondary": "{{ foreground | darken(0.3) }}", + "textDisabled": "{{ foreground | darken(0.5) }}", + + "accentPrimary": "{{ color4 | darken(0.3) | saturate(0.4) }}", + "accentSecondary": "{{ color1 | darken(0.4) | saturate(0.3) }}", + "accentTertiary": "{{ color3 | darken(0.35) | saturate(0.35) }}", + + "error": "{{ color5 | darken(0.25) | saturate(0.5) }}", + "warning": "{{ color6 | darken(0.3) | saturate(0.4) }}", + + "hover": "{{ color4 | darken(0.2) | saturate(0.3) }}", + "onAccent": "{{ background }}", + "outline": "{{ background | lighten(0.15) }}", + + "shadow": "{{ background }}", + "overlay": "{{ background }}" +} \ No newline at end of file diff --git a/Assets/Wallust/wallust.toml b/Assets/Wallust/wallust.toml index e50c5bb..6c1b8f2 100644 --- a/Assets/Wallust/wallust.toml +++ b/Assets/Wallust/wallust.toml @@ -44,4 +44,4 @@ check_contrast = true # target: ABSOLUTE path in which to place a file with generated templated values. # ¡ If either one is a directory, then both SHOULD be one. ! # zathura = { template = 'zathura', target = '~/.config/zathura/zathurarc' } -Quickshell = { template = 'quickshell.json', target = '~/.config/Noctalia/Theme.json' } \ No newline at end of file +Noctalia = { template = 'noctalia.json', target = '~/.config/noctalia/Theme.json' } \ No newline at end of file diff --git a/Modules/Background/Background.qml b/Modules/Background/Background.qml index 20752c9..1b2a185 100644 --- a/Modules/Background/Background.qml +++ b/Modules/Background/Background.qml @@ -15,9 +15,9 @@ Variants { // Force update when SWWW setting changes onVisibleChanged: { if (visible) { - console.log("Background: Showing wallpaper:", wallpaperSource) + } else { - console.log("Background: Hiding wallpaper (SWWW enabled)") + } } color: "transparent" diff --git a/Modules/Settings/Tabs/Wallpaper.qml b/Modules/Settings/Tabs/Wallpaper.qml index 430b636..d121236 100644 --- a/Modules/Settings/Tabs/Wallpaper.qml +++ b/Modules/Settings/Tabs/Wallpaper.qml @@ -61,7 +61,7 @@ ColumnLayout { NText { text: "Path to your wallpaper folder" - font.pointSize: Style.fontSizeSmall * scaling + font.pointSize: 12 color: Colors.textSecondary wrapMode: Text.WordWrap Layout.fillWidth: true diff --git a/Services/Audio.qml b/Services/Audio.qml index 1a3c8fa..4f4641e 100644 --- a/Services/Audio.qml +++ b/Services/Audio.qml @@ -49,7 +49,7 @@ Singleton { vol = 0 } root._volume = vol - console.log("[Audio] onVolumeChanged: " + root._volume.toFixed(2)) + } function onMutedChanged() { diff --git a/Services/Colors.qml b/Services/Colors.qml index 0bd125a..c5376a1 100644 --- a/Services/Colors.qml +++ b/Services/Colors.qml @@ -9,47 +9,113 @@ Singleton { id: root // Backgrounds - property color backgroundPrimary: themeData.backgroundPrimary - property color backgroundSecondary: themeData.backgroundSecondary - property color backgroundTertiary: themeData.backgroundTertiary + property color backgroundPrimary: useWallust ? wallustTheme.backgroundPrimary : defaultTheme.backgroundPrimary + property color backgroundSecondary: useWallust ? wallustTheme.backgroundSecondary : defaultTheme.backgroundSecondary + property color backgroundTertiary: useWallust ? wallustTheme.backgroundTertiary : defaultTheme.backgroundTertiary // Surfaces & Elevation - property color surface: themeData.surface - property color surfaceVariant: themeData.surfaceVariant + property color surface: useWallust ? wallustTheme.surface : defaultTheme.surface + property color surfaceVariant: useWallust ? wallustTheme.surfaceVariant : defaultTheme.surfaceVariant // Text Colors - property color textPrimary: themeData.textPrimary - property color textSecondary: themeData.textSecondary - property color textDisabled: themeData.textDisabled + property color textPrimary: useWallust ? wallustTheme.textPrimary : defaultTheme.textPrimary + property color textSecondary: useWallust ? wallustTheme.textSecondary : defaultTheme.textSecondary + property color textDisabled: useWallust ? wallustTheme.textDisabled : defaultTheme.textDisabled // Accent Colors - property color accentPrimary: themeData.accentPrimary - property color accentSecondary: themeData.accentSecondary - property color accentTertiary: themeData.accentTertiary + property color accentPrimary: useWallust ? wallustTheme.accentPrimary : defaultTheme.accentPrimary + property color accentSecondary: useWallust ? wallustTheme.accentSecondary : defaultTheme.accentSecondary + property color accentTertiary: useWallust ? wallustTheme.accentTertiary : defaultTheme.accentTertiary // Error/Warning - property color error: themeData.error - property color warning: themeData.warning + property color error: useWallust ? wallustTheme.error : defaultTheme.error + property color warning: useWallust ? wallustTheme.warning : defaultTheme.warning // Hover - property color hover: themeData.hover + property color hover: useWallust ? wallustTheme.hover : defaultTheme.hover // Additional Theme Properties - property color onAccent: themeData.onAccent - property color outline: themeData.outline + property color onAccent: useWallust ? wallustTheme.onAccent : defaultTheme.onAccent + property color outline: useWallust ? wallustTheme.outline : defaultTheme.outline // Shadows & Overlays - property color shadow: applyOpacity(themeData.shadow, "B3") - property color overlay: applyOpacity(themeData.overlay, "66") + property color shadow: applyOpacity(useWallust ? wallustTheme.shadow : defaultTheme.shadow, "B3") + property color overlay: applyOpacity(useWallust ? wallustTheme.overlay : defaultTheme.overlay, "66") + + // Check if we should use Wallust theme + property bool useWallust: Settings.data.wallpaper.generateTheme && wallustFile.loaded function applyOpacity(color, opacity) { - return color.replace("#", "#" + opacity) + // Convert color to string and apply opacity + return color.toString().replace("#", "#" + opacity) } - // FileView to load theme data from JSON file + // Default theme colors + QtObject { + id: defaultTheme + + property color backgroundPrimary: "#191724" + property color backgroundSecondary: "#1f1d2e" + property color backgroundTertiary: "#26233a" + + property color surface: "#1f1d2e" + property color surfaceVariant: "#37354c" + + property color textPrimary: "#e0def4" + property color textSecondary: "#908caa" + property color textDisabled: "#6e6a86" + + property color accentPrimary: "#ebbcba" + property color accentSecondary: "#31748f" + property color accentTertiary: "#9ccfd8" + + property color error: "#eb6f92" + property color warning: "#f6c177" + + property color hover: "#c4a7e7" + + property color onAccent: "#191724" + property color outline: "#44415a" + + property color shadow: "#191724" + property color overlay: "#191724" + } + + // Wallust theme colors (loaded from Theme.json) + QtObject { + id: wallustTheme + + property color backgroundPrimary: wallustData.backgroundPrimary + property color backgroundSecondary: wallustData.backgroundSecondary + property color backgroundTertiary: wallustData.backgroundTertiary + + property color surface: wallustData.surface + property color surfaceVariant: wallustData.surfaceVariant + + property color textPrimary: wallustData.textPrimary + property color textSecondary: wallustData.textSecondary + property color textDisabled: wallustData.textDisabled + + property color accentPrimary: wallustData.accentPrimary + property color accentSecondary: wallustData.accentSecondary + property color accentTertiary: wallustData.accentTertiary + + property color error: wallustData.error + property color warning: wallustData.warning + + property color hover: wallustData.hover + + property color onAccent: wallustData.onAccent + property color outline: wallustData.outline + + property color shadow: wallustData.shadow + property color overlay: wallustData.overlay + } + + // FileView to load Wallust theme data from Theme.json FileView { - id: themeFile - path: Settings.colorsFile + id: wallustFile + path: Settings.configDir + "Theme.json" watchChanges: true onFileChanged: reload() onAdapterUpdated: writeAdapter() @@ -60,7 +126,7 @@ Singleton { } } JsonAdapter { - id: themeData + id: wallustData // Backgrounds property string backgroundPrimary: "#191724" diff --git a/Services/Settings.qml b/Services/Settings.qml index 7e7b0b4..9c8ab95 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -53,7 +53,7 @@ Singleton { onLoaded: function () { Qt.callLater(function () { if (adapter.wallpaper.current !== "") { - console.log("Settings: Initializing wallpaper to:", adapter.wallpaper.current) + Wallpapers.setCurrentWallpaper(adapter.wallpaper.current, true) } }) diff --git a/Services/Wallpapers.qml b/Services/Wallpapers.qml index e586a09..ca9ee48 100644 --- a/Services/Wallpapers.qml +++ b/Services/Wallpapers.qml @@ -39,7 +39,7 @@ Singleton { } function setCurrentWallpaper(path, isInitial) { - console.log("Wallpapers: Setting wallpaper to:", path, "isInitial:", isInitial) + currentWallpaper = path if (!isInitial) { Settings.data.wallpaper.current = path @@ -50,7 +50,7 @@ Singleton { } else { transitionType = Settings.data.wallpaper.swww.transitionType } - console.log("SWWW: Changing wallpaper with transition type:", transitionType) + changeWallpaperProcess.running = true } else { // Fallback: update the settings directly for non-SWWW mode @@ -140,11 +140,11 @@ Singleton { running: false onStarted: { - console.log("SWWW: Process started with command:", command.join(" ")) + } onExited: function(exitCode, exitStatus) { - console.log("SWWW: Process finished with exit code:", exitCode, "status:", exitStatus) + if (exitCode !== 0) { console.log("SWWW: Process failed. Make sure swww-daemon is running with: swww-daemon") console.log("SWWW: You can start it with: swww-daemon --format xrgb") @@ -154,7 +154,7 @@ Singleton { Process { id: generateThemeProcess - command: ["wallust", "run", currentWallpaper, "-u", "-k", "-d", "Templates"] + command: ["wallust", "run", currentWallpaper, "-u", "-k", "-d", "Assets/Wallust"] workingDirectory: Quickshell.shellDir running: false } diff --git a/Templates/templates/noctalia.json b/Templates/templates/noctalia.json deleted file mode 100644 index 16aef1c..0000000 --- a/Templates/templates/noctalia.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "backgroundPrimary": "{{ background }}", - "backgroundSecondary": "{{ background | lighten(0.05) }}", - "backgroundTertiary": "{{ background | lighten(0.1) }}", - - "surface": "{{ background | lighten(0.08) }}", - "surfaceVariant": "{{ background | lighten(0.15) }}", - - "textPrimary": "{{ foreground }}", - "textSecondary": "{{ foreground | darken(0.1) }}", - "textDisabled": "{{ foreground | darken(0.4) }}", - - "accentPrimary": "{{ color4 }}", - "accentSecondary": "{{ color4 | lighten(0.2) }}", - "accentTertiary": "{{ color4 | darken(0.2) }}", - - "error": "{{ color5 | lighten(0.1) }}", - "warning": "{{ color5 | lighten(0.3) }}", - - "highlight": "{{ color4 | lighten(0.4) }}", - "rippleEffect": "{{ color4 | lighten(0.1) }}", - - "onAccent": "{{ background }}", - "outline": "{{ background | lighten(0.3) }}", - - "shadow": "{{ background }}", - "overlay": "{{ background }}" -} diff --git a/Templates/wallust.toml b/Templates/wallust.toml deleted file mode 100644 index cbd3593..0000000 --- a/Templates/wallust.toml +++ /dev/null @@ -1,47 +0,0 @@ -# wallust v3.4 -# -# You can copy this file to ~/.config/wallust/wallust.toml (keep in mind is a sample config) - -# SIMPLE TUTORIAL, or `man wallust.5`: -# https://explosion-mental.codeberg.page/wallust/ -# -# If comming from v2: https://explosion-mental.codeberg.page/wallust/v3.html#wallusttoml - -# Global section - values below can be overwritten by command line flags - -# How the image is parse, in order to get the colors: -# full - resized - wal - thumb - fastresize - kmeans -backend = "fastresize" - -# What color space to use to produce and select the most prominent colors: -# lab - labmixed - lch - lchmixed -color_space = "lch" - -# Use the most prominent colors in a way that makes sense, a scheme color palette: -# dark - dark16 - darkcomp - darkcomp16 -# light - light16 - lightcomp - lightcomp16 -# harddark - harddark16 - harddarkcomp - harddarkcomp16 -# softdark - softdark16 - softdarkcomp - softdarkcomp16 -# softlight - softlight16 - softlightcomp - softlightcomp16 -palette = "dark" - -# Ensures a "readable contrast" (OPTIONAL, disabled by default) -# Should only be enabled when you notice an unreadable contrast frequently happening -# with your images. The reference color for the contrast is the background color. -#check_contrast = true - -# Color saturation, between [1% and 100%] (OPTIONAL, disabled by default) -# usually something higher than 50 increases the saturation and below -# decreases it (on a scheme with strong and vivid colors) -#saturation = 35 - -# Alpha value for templating, by default 100 (no other use whatsoever) -#alpha = 100 - -[templates] -# NOTE: prefer '' over "" for paths, avoids escaping. -# template: A RELATIVE path that points to `~/.config/wallust/template` (depends on platform) -# target: ABSOLUTE path in which to place a file with generated templated values. -# ¡ If either one is a directory, then both SHOULD be one. ! -# zathura = { template = 'zathura', target = '~/.config/zathura/zathurarc' } -Noctalia = { template = 'noctalia.json', target = '~/.config/noctalia/Theme.json' } From f62a0e5f832c761e53ae8072b1e9f5ebd8dabaa6 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 14:05:19 -0400 Subject: [PATCH 191/394] Attempt at improving Wallust - looks ok with bright wallpapers - kinda suck with dark wallpapers --- Assets/Wallust/templates/noctalia.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Assets/Wallust/templates/noctalia.json b/Assets/Wallust/templates/noctalia.json index dd3e547..90af322 100644 --- a/Assets/Wallust/templates/noctalia.json +++ b/Assets/Wallust/templates/noctalia.json @@ -6,18 +6,18 @@ "surface": "{{ background | lighten(0.04) }}", "surfaceVariant": "{{ background | lighten(0.08) }}", - "textPrimary": "{{ foreground | darken(0.1) }}", - "textSecondary": "{{ foreground | darken(0.3) }}", + "textPrimary": "{{ foreground }}", + "textSecondary": "{{ foreground | darken(0.25) }}", "textDisabled": "{{ foreground | darken(0.5) }}", - "accentPrimary": "{{ color4 | darken(0.3) | saturate(0.4) }}", - "accentSecondary": "{{ color1 | darken(0.4) | saturate(0.3) }}", - "accentTertiary": "{{ color3 | darken(0.35) | saturate(0.35) }}", + "accentPrimary": "{{ color1 }}", + "accentSecondary": "{{ color6 }}", + "accentTertiary": "{{ color4 }}", - "error": "{{ color5 | darken(0.25) | saturate(0.5) }}", - "warning": "{{ color6 | darken(0.3) | saturate(0.4) }}", + "error": "{{ color5 | saturate(0.5) }}", + "warning": "{{ color6 | saturate(0.4) }}", - "hover": "{{ color4 | darken(0.2) | saturate(0.3) }}", + "hover": "{{ color14 }}", "onAccent": "{{ background }}", "outline": "{{ background | lighten(0.15) }}", From 0d196b2917a856ee13ffc80a15f2bf758b9d2298 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 14:19:58 -0400 Subject: [PATCH 192/394] Cleanup all the console.log Also renamed Github to GitHub --- Modules/Background/ScreenCorners.qml | 2 +- Modules/Bar/Tray.qml | 2 +- Modules/DemoPanel/DemoPanel.qml | 6 +++--- Modules/Settings/Tabs/About.qml | 8 ++++---- Services/Audio.qml | 2 +- Services/{Github.qml => GitHub.qml} | 24 ++++++++++++------------ Services/Location.qml | 2 +- Services/Settings.qml | 5 ----- Services/Wallpapers.qml | 27 ++++++++++++++++----------- Services/Workspaces.qml | 14 +++++++------- Widgets/NLoader.qml | 2 +- 11 files changed, 47 insertions(+), 47 deletions(-) rename Services/{Github.qml => GitHub.qml} (84%) diff --git a/Modules/Background/ScreenCorners.qml b/Modules/Background/ScreenCorners.qml index be93b46..6bd65cd 100644 --- a/Modules/Background/ScreenCorners.qml +++ b/Modules/Background/ScreenCorners.qml @@ -68,7 +68,7 @@ NLoader { anchors.fill: parent antialiasing: true renderTarget: Canvas.FramebufferObject - onPaint: function () { + onPaint: { const ctx = getContext("2d") ctx.reset() ctx.clearRect(0, 0, width, height) diff --git a/Modules/Bar/Tray.qml b/Modules/Bar/Tray.qml index 5b2c806..f4cc90b 100644 --- a/Modules/Bar/Tray.qml +++ b/Modules/Bar/Tray.qml @@ -100,7 +100,7 @@ Item { trayPanel.show() } else { - console.log("Tray: no menu available for", modelData.id, "or trayMenu not set") + console.log("[Tray] no menu available for", modelData.id, "or trayMenu not set") } } } diff --git a/Modules/DemoPanel/DemoPanel.qml b/Modules/DemoPanel/DemoPanel.qml index 0d37f4e..877cb4c 100644 --- a/Modules/DemoPanel/DemoPanel.qml +++ b/Modules/DemoPanel/DemoPanel.qml @@ -17,7 +17,7 @@ NLoader { // Ensure panel shows itself once created Component.onCompleted: { - console.log("[DemoPanel] Component completed, showing panel...") + console.log("[DemoPanel] Component completed, showing panel") show() } @@ -127,7 +127,7 @@ NLoader { label: "Label" description: "Description" onToggled: function (value) { - console.log("NToggle: " + value) + console.log("[DemoPanel] NToggle:", value) } } @@ -150,7 +150,7 @@ NLoader { optionsLabels: ["Cat", "Dog", "Bird", "Monkey", "Fish", "Turtle", "Elephant", "Tiger"] currentKey: "cat" onSelected: function (value) { - console.log("NComboBox: selected " + value) + console.log("[DemoPanel] NComboBox: selected ", value) } } diff --git a/Modules/Settings/Tabs/About.qml b/Modules/Settings/Tabs/About.qml index 58e5eb1..883effc 100644 --- a/Modules/Settings/Tabs/About.qml +++ b/Modules/Settings/Tabs/About.qml @@ -10,13 +10,13 @@ import qs.Widgets ColumnLayout { id: root - property string latestVersion: Github.latestVersion + property string latestVersion: GitHub.latestVersion property string currentVersion: "v1.2.1" // Fallback version - property var contributors: Github.contributors + property var contributors: GitHub.contributors Component.onCompleted: { - // Initialize the Github service - Github.init() + // Initialize the GitHub service + GitHub.init() } spacing: 0 diff --git a/Services/Audio.qml b/Services/Audio.qml index 4f4641e..4b25e09 100644 --- a/Services/Audio.qml +++ b/Services/Audio.qml @@ -54,7 +54,7 @@ Singleton { function onMutedChanged() { root._muted = (sink?.audio.muted ?? true) - console.log("[Audio] onMuteChanged " + root._muted) + console.log("[Audio] onMuteChanged:", root._muted) } } } diff --git a/Services/Github.qml b/Services/GitHub.qml similarity index 84% rename from Services/Github.qml rename to Services/GitHub.qml index 8a4bc1e..a32c0aa 100644 --- a/Services/Github.qml +++ b/Services/GitHub.qml @@ -10,7 +10,7 @@ Singleton { property string githubDataFile: Quickshell.env("NOCTALIA_GITHUB_FILE") || (Settings.cacheDir + "github.json") property int githubUpdateFrequency: 60 * 60 // 1 hour expressed in seconds - property var data: adapter // Used to access via Github.data.xxx.yyy + property var data: adapter // Used to access via GitHub.data.xxx.yyy property bool isFetchingData: false // Public properties for easy access @@ -32,7 +32,7 @@ Singleton { onLoadFailed: function (error) { if (error.toString().includes("No such file") || error === 2) { // File doesn't exist, create it with default values - console.log("[Github] Creating new cache file...") + console.log("[GitHub] Creating new cache file") writeAdapter() // Fetch data after a short delay to ensure file is created Qt.callLater(() => { @@ -59,11 +59,11 @@ Singleton { function loadFromCache() { const now = Date.now() if (!data.timestamp || (now - data.timestamp > githubUpdateFrequency * 1000)) { - console.log("[Github] Cache expired or missing, fetching new data from GitHub...") + console.log("[GitHub] Cache expired or missing, fetching new data from GitHub...") fetchFromGitHub() return } - console.log("[Github] Loading cached GitHub data (age: " + Math.round((now - data.timestamp) / 60000) + " minutes)") + console.log("[GitHub] Loading cached GitHub data (age:", Math.round((now - data.timestamp) / 60000),"minutes)") if (data.version) { root.latestVersion = data.version @@ -76,7 +76,7 @@ Singleton { // -------------------------------- function fetchFromGitHub() { if (isFetchingData) { - console.warn("[Github] GitHub data is still fetching") + console.warn("[GitHub] GitHub data is still fetching") return } @@ -122,15 +122,15 @@ Singleton { const version = data.tag_name root.data.version = version root.latestVersion = version - console.log("[Github] Latest version fetched from GitHub:", version) + console.log("[GitHub] Latest version fetched from GitHub:", version) } else { - console.log("[Github] No tag_name in GitHub response") + console.log("[GitHub] No tag_name in GitHub response") } } else { - console.log("[Github] Empty response from GitHub API") + console.log("[GitHub] Empty response from GitHub API") } } catch (e) { - console.error("[Github] Failed to parse version:", e) + console.error("[GitHub] Failed to parse version:", e) } // Check if both processes are done @@ -152,14 +152,14 @@ Singleton { const data = JSON.parse(response) root.data.contributors = data || [] root.contributors = root.data.contributors - console.log("[Github] Contributors fetched from GitHub:", root.contributors.length) + console.log("[GitHub] Contributors fetched from GitHub:", root.contributors.length) } else { - console.log("[Github] Empty response from GitHub API for contributors") + console.log("[GitHub] Empty response from GitHub API for contributors") root.data.contributors = [] root.contributors = [] } } catch (e) { - console.error("[Github] Failed to parse contributors:", e) + console.error("[GitHub] Failed to parse contributors:", e) root.data.contributors = [] root.contributors = [] } diff --git a/Services/Location.qml b/Services/Location.qml index 70587b4..267f6ee 100644 --- a/Services/Location.qml +++ b/Services/Location.qml @@ -90,7 +90,7 @@ Singleton { if ((data.latitude === "") || (data.longitude === "") || (data.name !== Settings.data.location.name)) { _geocodeLocation(Settings.data.location.name, function (latitude, longitude) { - console.log("[Location] Geocoded " + Settings.data.location.name + " to: " + latitude + " / " + longitude) + console.log("[Location] Geocoded", Settings.data.location.name, "to:", latitude, "/", longitude) // Save location name data.name = Settings.data.location.name diff --git a/Services/Settings.qml b/Services/Settings.qml index bde80db..422baa9 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -38,11 +38,6 @@ Singleton { } FileView { - - // TBC ? needed for SWWW only ? - // Qt.callLater(function () { - // WallpaperManager.setCurrentWallpaper(settings.currentWallpaper, true); - // }) path: settingsFile watchChanges: true onFileChanged: reload() diff --git a/Services/Wallpapers.qml b/Services/Wallpapers.qml index 7e7c1ef..1b5792e 100644 --- a/Services/Wallpapers.qml +++ b/Services/Wallpapers.qml @@ -13,7 +13,7 @@ Singleton { loadWallpapers() // Only set initial wallpaper if it's not empty if (currentWallpaper !== "") { - console.log("Wallpapers: Initializing with wallpaper:", currentWallpaper) + console.log("[WP] initializing with:", currentWallpaper) setCurrentWallpaper(currentWallpaper, true) } // Don't start random wallpaper during initialization @@ -34,7 +34,7 @@ Singleton { } function changeWallpaper(path) { - console.log("Wallpapers: changeWallpaper called with:", path) + console.log("[WP] changing to:", path) setCurrentWallpaper(path, false) } @@ -54,7 +54,7 @@ Singleton { changeWallpaperProcess.running = true } else { // Fallback: update the settings directly for non-SWWW mode - console.log("Non-SWWW mode: Setting wallpaper directly") + //console.log("[WP] Not using Swww, setting wallpaper directly") } if (randomWallpaperTimer.running) { @@ -97,7 +97,7 @@ Singleton { function startSWWWDaemon() { if (Settings.data.wallpaper.swww.enabled) { - console.log("SWWW: Attempting to start swww-daemon...") + console.log("[SWWW] Requesting swww-daemon") startDaemonProcess.running = true } } @@ -144,10 +144,10 @@ Singleton { } onExited: function (exitCode, exitStatus) { - console.log("SWWW: Process finished with exit code:", exitCode, "status:", exitStatus) + console.log("[SWWW] Process finished with exit code:", exitCode, "status:", exitStatus) if (exitCode !== 0) { - console.log("SWWW: Process failed. Make sure swww-daemon is running with: swww-daemon") - console.log("SWWW: You can start it with: swww-daemon --format xrgb") + console.log("[SWWW] Process failed. Make sure swww-daemon is running with: swww-daemon") + console.log("[SWWW] You can start it with: swww-daemon --format xrgb") } } } @@ -157,6 +157,11 @@ Singleton { command: ["wallust", "run", currentWallpaper, "-u", "-k", "-d", "Assets/Wallust"] workingDirectory: Quickshell.shellDir running: false + stdout: StdioCollector { + onStreamFinished: { + // console.log(this.text) + } + } } Process { @@ -165,15 +170,15 @@ Singleton { running: false onStarted: { - console.log("SWWW: Daemon start process initiated") + console.log("[SWWW] Daemon start process initiated") } onExited: function (exitCode, exitStatus) { - console.log("SWWW: Daemon start process finished with exit code:", exitCode) + console.log("[SWWW] Daemon start process finished with exit code:", exitCode) if (exitCode === 0) { - console.log("SWWW: Daemon started successfully") + console.log("[SWWW] Daemon started successfully") } else { - console.log("SWWW: Failed to start daemon. It might already be running.") + console.log("[SWWW] Failed to start daemon, may already be running") } } } diff --git a/Services/Workspaces.qml b/Services/Workspaces.qml index b73713d..7bdf040 100644 --- a/Services/Workspaces.qml +++ b/Services/Workspaces.qml @@ -17,7 +17,7 @@ Singleton { property var hlWorkspaces: Hyprland.workspaces.values // Detect which compositor we're using Component.onCompleted: { - console.log("WorkspaceManager initializing...") + console.log("[WS] Initializing workspaces service") detectCompositor() } @@ -25,27 +25,27 @@ Singleton { try { try { if (Hyprland.eventSocketPath) { - console.log("Detected Hyprland compositor") + console.log("[WS] Detected Hyprland compositor") isHyprland = true isNiri = false initHyprland() return } } catch (e) { - console.log("Hyprland not available:", e) + console.log("[WS] Hyprland not available:", e) } if (typeof Niri !== "undefined") { - console.log("Detected Niri service") + console.log("[WS] Detected Niri service") isHyprland = false isNiri = true initNiri() return } - console.log("No supported compositor detected") + console.log("[WS] Could not detect any supported compositor") } catch (e) { - console.error("Error detecting compositor:", e) + console.error("[WS] Error detecting compositor:", e) } } @@ -102,7 +102,7 @@ Singleton { } workspacesChanged() } catch (e) { - console.error("Error updating Hyprland workspaces:", e) + console.error("[WS] Error updating Hyprland workspaces:", e) } } diff --git a/Widgets/NLoader.qml b/Widgets/NLoader.qml index b7aa6b3..9539d80 100644 --- a/Widgets/NLoader.qml +++ b/Widgets/NLoader.qml @@ -18,7 +18,7 @@ Loader { sourceComponent: content // onLoaded: { - // console.log("NLoader onLoaded: " + item.toString()); + // console.log("[NLoader] onLoaded:", item.toString()); // } onActiveChanged: { if (active && item && item.show) { From e3ff4e5bcf9114efa504034a31c56b70db2692c3 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 14:26:00 -0400 Subject: [PATCH 193/394] GitHub: Use Time.timestamp instead of Date.now The timestamp is now Epoch Unix style (in seconds rather than ms) --- Services/GitHub.qml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Services/GitHub.qml b/Services/GitHub.qml index a32c0aa..5a6ff34 100644 --- a/Services/GitHub.qml +++ b/Services/GitHub.qml @@ -57,13 +57,13 @@ Singleton { // -------------------------------- function loadFromCache() { - const now = Date.now() - if (!data.timestamp || (now - data.timestamp > githubUpdateFrequency * 1000)) { + const now = Time.timestamp + if (!data.timestamp || (now >= data.timestamp + githubUpdateFrequency)) { console.log("[GitHub] Cache expired or missing, fetching new data from GitHub...") fetchFromGitHub() return } - console.log("[GitHub] Loading cached GitHub data (age:", Math.round((now - data.timestamp) / 60000),"minutes)") + console.log("[GitHub] Loading cached GitHub data (age:", Math.round((now - data.timestamp) / 60), "minutes)") if (data.version) { root.latestVersion = data.version @@ -87,7 +87,7 @@ Singleton { // -------------------------------- function saveData() { - data.timestamp = Date.now() + data.timestamp = Time.timestamp Qt.callLater(() => { // Access the FileView's writeAdapter method var fileView = root.children.find(child => child.objectName === "githubDataFileView") From b68d5c9f4c84f4aa5d36d5f0d266f3bc2b225437 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 14:29:58 -0400 Subject: [PATCH 194/394] GitHub removed some unecessary code --- Modules/Settings/Tabs/About.qml | 5 ----- Services/GitHub.qml | 8 -------- shell.qml | 3 ++- 3 files changed, 2 insertions(+), 14 deletions(-) diff --git a/Modules/Settings/Tabs/About.qml b/Modules/Settings/Tabs/About.qml index 883effc..574f219 100644 --- a/Modules/Settings/Tabs/About.qml +++ b/Modules/Settings/Tabs/About.qml @@ -14,11 +14,6 @@ ColumnLayout { property string currentVersion: "v1.2.1" // Fallback version property var contributors: GitHub.contributors - Component.onCompleted: { - // Initialize the GitHub service - GitHub.init() - } - spacing: 0 Layout.fillWidth: true Layout.fillHeight: true diff --git a/Services/GitHub.qml b/Services/GitHub.qml index 5a6ff34..8932d03 100644 --- a/Services/GitHub.qml +++ b/Services/GitHub.qml @@ -31,9 +31,6 @@ Singleton { } onLoadFailed: function (error) { if (error.toString().includes("No such file") || error === 2) { - // File doesn't exist, create it with default values - console.log("[GitHub] Creating new cache file") - writeAdapter() // Fetch data after a short delay to ensure file is created Qt.callLater(() => { fetchFromGitHub() @@ -50,11 +47,6 @@ Singleton { } } - // -------------------------------- - function init() {// does nothing but ensure the singleton is created - // do not remove - } - // -------------------------------- function loadFromCache() { const now = Time.timestamp diff --git a/shell.qml b/shell.qml index 1f7e057..4248b32 100644 --- a/shell.qml +++ b/shell.qml @@ -39,7 +39,8 @@ ShellRoot { } Component.onCompleted: { - // On startup, check if we need to get fresh weather data + // Ensure our singleton is created as soon as possible + // so we start fetching weather asap if necessary Location.init() } } From d009b8d5c8abd1da0b0470012eee3c79fe0fd78e Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 15:07:32 -0400 Subject: [PATCH 195/394] Using a bash script for SystemStats instead of ZigStat --- Bin/sysmon.sh | 49 ----- Bin/system-stats.sh | 192 ++++++++++++++++++ ...notifications.sh => test-notifications.sh} | 0 Modules/SidePanel/Cards/SystemMonitorCard.qml | 13 +- Services/SysInfo.qml | 47 ----- Services/SystemStats.qml | 37 ++++ Widgets/NCircleStat.qml | 2 +- Widgets/NSystemMonitor.qml | 54 ----- 8 files changed, 234 insertions(+), 160 deletions(-) delete mode 100755 Bin/sysmon.sh create mode 100755 Bin/system-stats.sh rename Bin/{test_notifications.sh => test-notifications.sh} (100%) delete mode 100644 Services/SysInfo.qml create mode 100644 Services/SystemStats.qml delete mode 100644 Widgets/NSystemMonitor.qml diff --git a/Bin/sysmon.sh b/Bin/sysmon.sh deleted file mode 100755 index 0d69904..0000000 --- a/Bin/sysmon.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/bash - -# A script to display CPU temperature, CPU usage, memory usage, and disk usage without the 'sensors' package. - -echo "--- System Metrics ---" - -# Get CPU Temperature in Celsius from a kernel file. -# This method is more common on modern systems but may vary. -# It reads the temperature from the first available core. -# Function to get CPU temperature - - # Check for the common thermal zone path - if [ -f "/sys/class/thermal/thermal_zone0/temp" ]; then - temp_file="/sys/class/thermal/thermal_zone0/temp" - # Check for a different thermal zone path (e.g., some older systems) - elif [ -f "/sys/class/hwmon/hwmon0/temp1_input" ]; then - temp_file="/sys/class/hwmon/hwmon0/temp1_input" - else - echo "Error: Could not find a CPU temperature file." - exit 1 - fi - - # Read the raw temperature value - raw_temp=$(cat "$temp_file") - - # The value is usually in millidegrees Celsius, so we divide by 1000. - temp_celsius=$((raw_temp / 1000)) - - echo "CPU Temperature: ${temp_celsius}°C" - - -# Get CPU Usage -# 'top' is a standard utility for this. It gives a real-time view of system processes. -cpu_usage=$(top -bn1 | grep "Cpu(s)" | sed "s/.*, *\([0-9.]*\)%* id.*/\1/" | awk '{print 100 - $1}') -echo "CPU Usage: ${cpu_usage}%" - -# Get Memory Usage -# 'free' provides information about memory usage. -mem_total=$(free | grep Mem | awk '{print $2}') -mem_used=$(free | grep Mem | awk '{print $3}') -mem_usage=$((100 * mem_used / mem_total)) -echo "Memory Usage: ${mem_usage}%" - -# Get Disk Usage -# 'df' reports file system disk space usage. We check the root directory. -disk_usage=$(df -h / | grep / | awk '{print $5}' | sed 's/%//g') -echo "Disk Usage: ${disk_usage}%" - -echo "----------------------" \ No newline at end of file diff --git a/Bin/system-stats.sh b/Bin/system-stats.sh new file mode 100755 index 0000000..7bd1170 --- /dev/null +++ b/Bin/system-stats.sh @@ -0,0 +1,192 @@ +#!/bin/bash + +# A Bash script to monitor system stats and output them in JSON format. +# This script is a conversion of ZigStat + +# --- Configuration --- +# Default sleep duration in seconds. Can be overridden by the first argument. +SLEEP_DURATION=3 + +# --- Argument Parsing --- +# Check if a command-line argument is provided for the sleep duration. +if [[ -n "$1" ]]; then + # Basic validation to ensure the argument is a number (integer or float). + if [[ "$1" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then + SLEEP_DURATION=$1 + else + # Output to stderr if the format is invalid. + echo "Warning: Invalid duration format '$1'. Using default of ${SLEEP_DURATION}s." >&2 + fi +fi + +# --- Global Cache Variables --- +# These variables will store the discovered CPU temperature sensor path and type +# to avoid searching for it on every loop iteration. +TEMP_SENSOR_PATH="" +TEMP_SENSOR_TYPE="" + +# --- Data Collection Functions --- + +# +# Gets memory usage in GB and as a percentage. +# +get_memory_info() { + awk ' + /MemTotal/ {total=$2} + /MemAvailable/ {available=$2} + END { + if (total > 0) { + usage_kb = total - available + usage_gb = usage_kb / 1000000 + usage_percent = (usage_kb / total) * 100 + # MODIFIED: Round the memory percentage to the nearest integer. + printf "%.1f %.0f\n", usage_gb, usage_percent + } else { + # Fallback if /proc/meminfo is unreadable or empty. + print "0.0 0.0" + } + } + ' /proc/meminfo +} + +# +# Gets the usage percentage of the root filesystem ("/"). +# +get_disk_usage() { + # df gets disk usage. --output=pcent shows only the percentage for the root path. + # tail -1 gets the data line, and tr removes the '%' sign and whitespace. + df --output=pcent / | tail -1 | tr -d ' %' +} + +# +# Calculates current CPU usage over a short interval. +# +get_cpu_usage() { + # Read all 10 CPU time fields to prevent errors on newer kernels. + read -r cpu prev_user prev_nice prev_system prev_idle prev_iowait prev_irq prev_softirq prev_steal prev_guest prev_guest_nice < /proc/stat + + # Calculate previous total and idle times. + local prev_total_idle=$((prev_idle + prev_iowait)) + local prev_total=$((prev_user + prev_nice + prev_system + prev_idle + prev_iowait + prev_irq + prev_softirq + prev_steal + prev_guest + prev_guest_nice)) + + # Wait for a short period. + sleep 0.05 + + # Read all 10 CPU time fields again for the second measurement. + read -r cpu user nice system idle iowait irq softirq steal guest guest_nice < /proc/stat + + # Calculate new total and idle times. + local total_idle=$((idle + iowait)) + local total=$((user + nice + system + idle + iowait + irq + softirq + steal + guest + guest_nice)) + + # Add a check to prevent division by zero if total hasn't changed. + if (( total <= prev_total )); then + echo "0.0" + return + fi + + # Calculate the difference over the interval. + local diff_total=$((total - prev_total)) + local diff_idle=$((total_idle - prev_total_idle)) + + # Use awk for floating-point calculation and print the percentage. + awk -v total="$diff_total" -v idle="$diff_idle" ' + BEGIN { + if (total > 0) { + # Formula: 100 * (Total - Idle) / Total + usage = 100 * (total - idle) / total + # MODIFIED: Changed format from "%.2f" back to "%.1f" for one decimal place. + printf "%.1f\n", usage + } else { + # MODIFIED: Changed output back to "0.0" to match the precision. + print "0.0" + } + }' +} + +# +# Finds and returns the CPU temperature in degrees Celsius. +# Caches the sensor path for efficiency. +# +get_cpu_temp() { + # If the sensor path hasn't been found yet, search for it. + if [[ -z "$TEMP_SENSOR_PATH" ]]; then + for dir in /sys/class/hwmon/hwmon*; do + # Check if the 'name' file exists and read it. + if [[ -f "$dir/name" ]]; then + local name + name=$(<"$dir/name") + # Check for supported sensor types (matches Zig code). + if [[ "$name" == "coretemp" || "$name" == "k10temp" ]]; then + TEMP_SENSOR_PATH=$dir + TEMP_SENSOR_TYPE=$name + break # Found it, no need to keep searching. + fi + fi + done + fi + + # If after searching no sensor was found, return 0. + if [[ -z "$TEMP_SENSOR_PATH" ]]; then + echo 0 + return + fi + + # --- Get temp based on sensor type --- + if [[ "$TEMP_SENSOR_TYPE" == "coretemp" ]]; then + # For Intel 'coretemp', average all core temperatures. + # find gets all temp inputs, cat reads them, and awk calculates the average. + # The value is in millidegrees Celsius, so we divide by 1000. + find "$TEMP_SENSOR_PATH" -type f -name 'temp*_input' -print0 | xargs -0 cat | awk ' + { total += $1; count++ } + END { + if (count > 0) print int(total / count / 1000); + else print 0; + }' + + elif [[ "$TEMP_SENSOR_TYPE" == "k10temp" ]]; then + # For AMD 'k10temp', find the 'Tctl' sensor, which is the control temperature. + local tctl_input="" + for label_file in "$TEMP_SENSOR_PATH"/temp*_label; do + if [[ -f "$label_file" ]] && [[ $(<"$label_file") == "Tctl" ]]; then + # The input file has the same name but with '_input' instead of '_label'. + tctl_input="${label_file%_label}_input" + break + fi + done + + if [[ -f "$tctl_input" ]]; then + # Read the temperature and convert from millidegrees to degrees. + echo "$(( $(<"$tctl_input") / 1000 ))" + else + echo 0 # Fallback + fi + else + echo 0 # Should not happen if cache logic is correct. + fi +} + + +# --- Main Loop --- +# This loop runs indefinitely, gathering and printing stats. +while true; do + # Call the functions to gather all the data. + # 'read' is used to capture the two output values from get_memory_info. + read -r mem_gb mem_per <<< "$(get_memory_info)" + + # Command substitution captures the single output from the other functions. + disk_per=$(get_disk_usage) + cpu_usage=$(get_cpu_usage) + cpu_temp=$(get_cpu_temp) + + # Use printf to format the final JSON output string, matching the Zig program. + printf '{"mem":"%s", "cpu": "%s", "cputemp": "%s", "memper": "%s", "diskper": "%s"}\n' \ + "$mem_gb" \ + "$cpu_usage" \ + "$cpu_temp" \ + "$mem_per" \ + "$disk_per" + + # Wait for the specified duration before the next update. + sleep "$SLEEP_DURATION" +done \ No newline at end of file diff --git a/Bin/test_notifications.sh b/Bin/test-notifications.sh similarity index 100% rename from Bin/test_notifications.sh rename to Bin/test-notifications.sh diff --git a/Modules/SidePanel/Cards/SystemMonitorCard.qml b/Modules/SidePanel/Cards/SystemMonitorCard.qml index f66c405..221a2ca 100644 --- a/Modules/SidePanel/Cards/SystemMonitorCard.qml +++ b/Modules/SidePanel/Cards/SystemMonitorCard.qml @@ -28,13 +28,8 @@ NBox { height: Style.marginTiny * scaling } - NSystemMonitor { - id: sysMon - intervalSeconds: 1 - } - NCircleStat { - value: sysMon.cpuUsage || SysInfo.cpuUsage + value: SystemStats.cpuUsage icon: "speed" flat: true contentScale: 0.8 @@ -42,7 +37,7 @@ NBox { height: 68 * scaling } NCircleStat { - value: sysMon.cpuTemp || SysInfo.cpuTemp + value: SystemStats.cpuTemp suffix: "°C" icon: "device_thermostat" flat: true @@ -51,7 +46,7 @@ NBox { height: 68 * scaling } NCircleStat { - value: sysMon.memoryUsagePer || SysInfo.memoryUsagePer + value: SystemStats.memoryUsagePer icon: "memory" flat: true contentScale: 0.8 @@ -59,7 +54,7 @@ NBox { height: 68 * scaling } NCircleStat { - value: sysMon.diskUsage || SysInfo.diskUsage + value: SystemStats.diskUsage icon: "data_usage" flat: true contentScale: 0.8 diff --git a/Services/SysInfo.qml b/Services/SysInfo.qml deleted file mode 100644 index 39bef22..0000000 --- a/Services/SysInfo.qml +++ /dev/null @@ -1,47 +0,0 @@ -pragma Singleton - -import QtQuick -import Qt.labs.folderlistmodel -import Quickshell -import Quickshell.Io - -Singleton { - id: manager //TBC - - property string updateInterval: "2s" - property string cpuUsageStr: "" - property string cpuTempStr: "" - property string memoryUsageStr: "" - property string memoryUsagePerStr: "" - property real cpuUsage: 0 - property real memoryUsage: 0 - property real cpuTemp: 0 - property real diskUsage: 0 - property real memoryUsagePer: 0 - property string diskUsageStr: "" - - Process { - id: zigstatProcess - running: true - command: [Quickshell.shellDir + "/Programs/zigstat", updateInterval] - stdout: SplitParser { - onRead: function (line) { - try { - const data = JSON.parse(line) - cpuUsage = +data.cpu - cpuTemp = +data.cputemp - memoryUsage = +data.mem - memoryUsagePer = +data.memper - diskUsage = +data.diskper - cpuUsageStr = data.cpu + "%" - cpuTempStr = data.cputemp + "°C" - memoryUsageStr = data.mem + "G" - memoryUsagePerStr = data.memper + "%" - diskUsageStr = data.diskper + "%" - } catch (e) { - console.error("Failed to parse zigstat output:", e) - } - } - } - } -} diff --git a/Services/SystemStats.qml b/Services/SystemStats.qml new file mode 100644 index 0000000..add1e38 --- /dev/null +++ b/Services/SystemStats.qml @@ -0,0 +1,37 @@ +pragma Singleton + +import QtQuick +import Qt.labs.folderlistmodel +import Quickshell +import Quickshell.Io + +Singleton { + id: root + + // Public values + property real cpuUsage: 0 + property real cpuTemp: 0 + property real memoryUsagePer: 0 + property real diskUsage: 0 + + // Background process emitting one JSON line per sample + Process { + id: reader + running: true + command: ["sh", "-c", Quickshell.shellDir + "/Bin/system-stats.sh"] + stdout: SplitParser { + onRead: function (line) { + try { + const data = JSON.parse(line) + root.cpuUsage = data.cpu + root.cpuTemp = data.cputemp + root.memoryUsagePer = data.memper + root.diskUsage = data.diskper + } catch (e) { + + // ignore malformed lines + } + } + } + } +} diff --git a/Widgets/NCircleStat.qml b/Widgets/NCircleStat.qml index a0a740f..15995cf 100644 --- a/Widgets/NCircleStat.qml +++ b/Widgets/NCircleStat.qml @@ -76,7 +76,7 @@ Rectangle { Text { id: valueLabel anchors.centerIn: parent - text: `${Math.round(root.value)}${root.suffix}` + text: `${root.value}${root.suffix}` font.pointSize: Style.fontSizeMedium * scaling * contentScale color: Colors.textPrimary horizontalAlignment: Text.AlignHCenter diff --git a/Widgets/NSystemMonitor.qml b/Widgets/NSystemMonitor.qml deleted file mode 100644 index bb28716..0000000 --- a/Widgets/NSystemMonitor.qml +++ /dev/null @@ -1,54 +0,0 @@ -import QtQuick -import Quickshell -import Quickshell.Io - -// Lightweight system monitor using standard Linux interfaces. -// Provides cpu usage %, cpu temperature (°C), and memory usage %. -// No external helpers; uses /proc and /sys via a shell loop. -Item { - id: root - - // Public values - property real cpuUsage: 0 - property real cpuTemp: 0 - property real memoryUsagePer: 0 - property real diskUsage: 0 - - // Interval in seconds between updates - property int intervalSeconds: 1 - - // Background process emitting one JSON line per sample - Process { - id: reader - running: true - command: ["sh", "-c", // Outputs: {"cpu":,"memper":,"cputemp":} - "interval=" + intervalSeconds + "; " + "while true; do " + // First /proc/stat snapshot - "read _ u1 n1 s1 id1 iw1 ir1 si1 st1 gs1 < /proc/stat; " - + "t1=$((u1+n1+s1+id1+iw1+ir1+si1+st1)); i1=$((id1+iw1)); " + "sleep $interval; " + // Second /proc/stat snapshot - "read _ u2 n2 s2 id2 iw2 ir2 si2 st2 gs2 < /proc/stat; " + "t2=$((u2+n2+s2+id2+iw2+ir2+si2+st2)); i2=$((id2+iw2)); " - + "dt=$((t2 - t1)); di=$((i2 - i1)); " + "cpu=$(( (100*(dt - di)) / (dt>0?dt:1) )); " + // Memory percent via /proc/meminfo (kB) - "mt=$(awk '/MemTotal/ {print $2}' /proc/meminfo); " + "ma=$(awk '/MemAvailable/ {print $2}' /proc/meminfo); " - + "mm=$((mt - ma)); mp=$(( (100*mm) / (mt>0?mt:1) )); " + // Temperature: scan hwmon and thermal zones, choose max; convert m°C → °C - "ct=0; " + "for f in /sys/class/hwmon/hwmon*/temp*_input /sys/class/thermal/thermal_zone*/temp; do " - + "[ -r \"$f\" ] || continue; v=$(cat \"$f\" 2>/dev/null); " + "[ -z \"$v\" ] && continue; " - + "if [ \"$v\" -gt 1000 ] 2>/dev/null; then v=$((v/1000)); fi; " + "[ \"$v\" -gt \"$ct\" ] 2>/dev/null && ct=$v; " - + "done; " + // Disk usage percent for root filesystem - "dp=$(df -P / 2>/dev/null | awk 'NR==2{gsub(/%/,\"\",$5); print $5}'); " + "[ -z \"$dp\" ] && dp=0; " + // Emit JSON line - "echo \"{\\\"cpu\\\":$cpu,\\\"memper\\\":$mp,\\\"cputemp\\\":$ct,\\\"diskper\\\":$dp}\"; " + "done"] - - stdout: SplitParser { - onRead: function (line) { - try { - const data = JSON.parse(line) - root.cpuUsage = +data.cpu - root.cpuTemp = +data.cputemp - root.memoryUsagePer = +data.memper - root.diskUsage = +data.diskper - } catch (e) { - - // ignore malformed lines - } - } - } - } -} From 6f0b0b299f9351182bfafd4ba11e92e8dc854103 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 15:20:49 -0400 Subject: [PATCH 196/394] Slightly different look for the SidePanel system stats --- Modules/SidePanel/Cards/SystemMonitorCard.qml | 2 +- Modules/SidePanel/Cards/WeatherCard.qml | 4 ++-- Widgets/NCircleStat.qml | 19 ++++++++++--------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/Modules/SidePanel/Cards/SystemMonitorCard.qml b/Modules/SidePanel/Cards/SystemMonitorCard.qml index 221a2ca..90d4345 100644 --- a/Modules/SidePanel/Cards/SystemMonitorCard.qml +++ b/Modules/SidePanel/Cards/SystemMonitorCard.qml @@ -55,7 +55,7 @@ NBox { } NCircleStat { value: SystemStats.diskUsage - icon: "data_usage" + icon: "hard_drive" flat: true contentScale: 0.8 width: 72 * scaling diff --git a/Modules/SidePanel/Cards/WeatherCard.qml b/Modules/SidePanel/Cards/WeatherCard.qml index c3de73f..33fb661 100644 --- a/Modules/SidePanel/Cards/WeatherCard.qml +++ b/Modules/SidePanel/Cards/WeatherCard.qml @@ -28,12 +28,12 @@ NBox { NText { text: weatherReady ? Location.weatherSymbolFromCode(Location.data.weather.current_weather.weathercode) : "" font.family: "Material Symbols Outlined" - font.pointSize: Style.fontSizeXXL * 1.25 * scaling + font.pointSize: Style.fontSizeXXL * 1.5 * scaling color: Colors.accentSecondary } ColumnLayout { - + spacing: -Style.marginTiny * scaling NText { text: Settings.data.location.name font.weight: Style.fontWeightBold diff --git a/Widgets/NCircleStat.qml b/Widgets/NCircleStat.qml index 15995cf..1ca3f05 100644 --- a/Widgets/NCircleStat.qml +++ b/Widgets/NCircleStat.qml @@ -78,6 +78,7 @@ Rectangle { anchors.centerIn: parent text: `${root.value}${root.suffix}` font.pointSize: Style.fontSizeMedium * scaling * contentScale + font.weight: Style.fontWeightBold color: Colors.textPrimary horizontalAlignment: Text.AlignHCenter } @@ -85,23 +86,23 @@ Rectangle { // Tiny circular badge for the icon, inside the right-side gap Rectangle { id: iconBadge - width: 22 * scaling * contentScale + width: 28 * scaling * contentScale height: width radius: width / 2 - color: Colors.backgroundPrimary - border.color: Colors.accentPrimary - border.width: Math.max(1, Style.borderThin * scaling) + color: Colors.backgroundSecondary + // border.color: Colors.accentPrimary + // border.width: Math.max(1, Style.borderThin * scaling) anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.rightMargin: 4 * scaling * contentScale - anchors.bottomMargin: 4 * scaling * contentScale + anchors.top: parent.top + anchors.rightMargin: -6 * scaling * contentScale + anchors.topMargin: 4 * scaling * contentScale Text { anchors.centerIn: parent text: root.icon font.family: "Material Symbols Outlined" - font.pointSize: Style.fontSizeLarge * scaling * contentScale - color: Colors.accentPrimary + font.pointSize: Style.fontSizeLargeXL * scaling * contentScale + color: Colors.textSecondary horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } From b7332cc2e062ab986b0627f730db33e3648b404c Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 15:39:33 -0400 Subject: [PATCH 197/394] WeatherCard: keep the name for long location name (split around the comma) --- Modules/SidePanel/Cards/WeatherCard.qml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Modules/SidePanel/Cards/WeatherCard.qml b/Modules/SidePanel/Cards/WeatherCard.qml index 33fb661..1d4d066 100644 --- a/Modules/SidePanel/Cards/WeatherCard.qml +++ b/Modules/SidePanel/Cards/WeatherCard.qml @@ -35,7 +35,11 @@ NBox { ColumnLayout { spacing: -Style.marginTiny * scaling NText { - text: Settings.data.location.name + text: { + // Ensure the name is not too long if one had to specify the country + const chunks = Settings.data.location.name.split(",") + return chunks[0] + } font.weight: Style.fontWeightBold font.pointSize: Style.fontSizeXL * scaling } From c1f22d5e87fb3fe6ddfa0e46b4f7680221a58e65 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 15:42:31 -0400 Subject: [PATCH 198/394] added comment for later debug --- Modules/SidePanel/Cards/WeatherCard.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/Modules/SidePanel/Cards/WeatherCard.qml b/Modules/SidePanel/Cards/WeatherCard.qml index 1d4d066..a7e749a 100644 --- a/Modules/SidePanel/Cards/WeatherCard.qml +++ b/Modules/SidePanel/Cards/WeatherCard.qml @@ -10,6 +10,7 @@ NBox { readonly property real scaling: Scaling.scale(screen) readonly property bool weatherReady: (Location.data.weather !== null) + // TBC weatherReady is not turning to false when we reset weather... Layout.fillWidth: true // Height driven by content From e7588b29d945c53779adf2146e05b33bc572aaa2 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 16:18:49 -0400 Subject: [PATCH 199/394] ScreenRecorder Service + Reconnected the utility button --- Modules/SidePanel/Cards/UtilitiesCard.qml | 8 ++- Modules/SidePanel/Cards/WeatherCard.qml | 2 +- Services/Audio.qml | 1 - Services/Colors.qml | 32 +++++------ Services/GitHub.qml | 4 +- Services/ScreenRecorder.qml | 64 +++++++++++++++++++++ Services/Settings.qml | 3 +- Services/Time.qml | 68 +++++++++++++++-------- Services/Wallpapers.qml | 6 +- Widgets/NIconButton.qml | 5 +- 10 files changed, 142 insertions(+), 51 deletions(-) create mode 100644 Services/ScreenRecorder.qml diff --git a/Modules/SidePanel/Cards/UtilitiesCard.qml b/Modules/SidePanel/Cards/UtilitiesCard.qml index ec1126f..8040e06 100644 --- a/Modules/SidePanel/Cards/UtilitiesCard.qml +++ b/Modules/SidePanel/Cards/UtilitiesCard.qml @@ -18,14 +18,20 @@ NBox { Item { Layout.fillWidth: true } - // Record + // Screen Recorder NIconButton { icon: "videocam" + showFilled: ScreenRecorder.isRecording + onClicked: { + ScreenRecorder.toggleRecording() + } } + // Wallpaper NIconButton { icon: "image" } + Item { Layout.fillWidth: true } diff --git a/Modules/SidePanel/Cards/WeatherCard.qml b/Modules/SidePanel/Cards/WeatherCard.qml index a7e749a..4bbe940 100644 --- a/Modules/SidePanel/Cards/WeatherCard.qml +++ b/Modules/SidePanel/Cards/WeatherCard.qml @@ -10,8 +10,8 @@ NBox { readonly property real scaling: Scaling.scale(screen) readonly property bool weatherReady: (Location.data.weather !== null) - // TBC weatherReady is not turning to false when we reset weather... + // TBC weatherReady is not turning to false when we reset weather... Layout.fillWidth: true // Height driven by content implicitHeight: content.implicitHeight + Style.marginLarge * 2 * scaling diff --git a/Services/Audio.qml b/Services/Audio.qml index 4b25e09..ea88d63 100644 --- a/Services/Audio.qml +++ b/Services/Audio.qml @@ -49,7 +49,6 @@ Singleton { vol = 0 } root._volume = vol - } function onMutedChanged() { diff --git a/Services/Colors.qml b/Services/Colors.qml index c5376a1..be5ce2c 100644 --- a/Services/Colors.qml +++ b/Services/Colors.qml @@ -53,30 +53,30 @@ Singleton { // Default theme colors QtObject { id: defaultTheme - + property color backgroundPrimary: "#191724" property color backgroundSecondary: "#1f1d2e" property color backgroundTertiary: "#26233a" - + property color surface: "#1f1d2e" property color surfaceVariant: "#37354c" - + property color textPrimary: "#e0def4" property color textSecondary: "#908caa" property color textDisabled: "#6e6a86" - + property color accentPrimary: "#ebbcba" property color accentSecondary: "#31748f" property color accentTertiary: "#9ccfd8" - + property color error: "#eb6f92" property color warning: "#f6c177" - + property color hover: "#c4a7e7" - + property color onAccent: "#191724" property color outline: "#44415a" - + property color shadow: "#191724" property color overlay: "#191724" } @@ -84,30 +84,30 @@ Singleton { // Wallust theme colors (loaded from Theme.json) QtObject { id: wallustTheme - + property color backgroundPrimary: wallustData.backgroundPrimary property color backgroundSecondary: wallustData.backgroundSecondary property color backgroundTertiary: wallustData.backgroundTertiary - + property color surface: wallustData.surface property color surfaceVariant: wallustData.surfaceVariant - + property color textPrimary: wallustData.textPrimary property color textSecondary: wallustData.textSecondary property color textDisabled: wallustData.textDisabled - + property color accentPrimary: wallustData.accentPrimary property color accentSecondary: wallustData.accentSecondary property color accentTertiary: wallustData.accentTertiary - + property color error: wallustData.error property color warning: wallustData.warning - + property color hover: wallustData.hover - + property color onAccent: wallustData.onAccent property color outline: wallustData.outline - + property color shadow: wallustData.shadow property color overlay: wallustData.overlay } diff --git a/Services/GitHub.qml b/Services/GitHub.qml index 8932d03..620a616 100644 --- a/Services/GitHub.qml +++ b/Services/GitHub.qml @@ -49,7 +49,7 @@ Singleton { // -------------------------------- function loadFromCache() { - const now = Time.timestamp + const now = Time.timestamp if (!data.timestamp || (now >= data.timestamp + githubUpdateFrequency)) { console.log("[GitHub] Cache expired or missing, fetching new data from GitHub...") fetchFromGitHub() @@ -79,7 +79,7 @@ Singleton { // -------------------------------- function saveData() { - data.timestamp = Time.timestamp + data.timestamp = Time.timestamp Qt.callLater(() => { // Access the FileView's writeAdapter method var fileView = root.children.find(child => child.objectName === "githubDataFileView") diff --git a/Services/ScreenRecorder.qml b/Services/ScreenRecorder.qml new file mode 100644 index 0000000..4f48d15 --- /dev/null +++ b/Services/ScreenRecorder.qml @@ -0,0 +1,64 @@ +pragma Singleton + +import QtQuick +import Quickshell +import qs.Services + +Singleton { + id: root + + readonly property var settings: Settings.data.screenRecorder + property bool isRecording: false + property string outputPath: "" + + // Start or Stop recording + function toggleRecording() { + isRecording ? stopRecording() : startRecording() + } + + // Start screen recording using Quickshell.execDetached + function startRecording() { + if (isRecording) { + return + } + isRecording = true + + var filename = Time.getFormattedTimestamp() + ".mp4" + var videoDir = settings.directory + if (videoDir && !videoDir.endsWith("/")) { + videoDir += "/" + } + outputPath = videoDir + filename + var command = "gpu-screen-recorder -w portal" + " -f " + settings.frameRate + " -ac " + settings.audioCodec + + " -k " + settings.videoCodec + " -a " + settings.audioSource + " -q " + settings.quality + + " -cursor " + (settings.showCursor ? "yes" : "no") + " -cr " + settings.colorRange + " -o " + outputPath + + //console.log("[ScreenRecorder]", command) + Quickshell.execDetached(["sh", "-c", command]) + console.log("[ScreenRecorder] Started recording") + } + + // Stop recording using Quickshell.execDetached + function stopRecording() { + if (!isRecording) { + return + } + + Quickshell.execDetached(["sh", "-c", "pkill -SIGINT -f 'gpu-screen-recorder.*portal'"]) + console.log("[ScreenRecorder] Finished recording:", outputPath) + + // Just in case, force kill after 3 seconds + killTimer.running = true + isRecording = false + } + + Timer { + id: killTimer + interval: 3000 + running: false + repeat: false + onTriggered: { + Quickshell.execDetached(["sh", "-c", "pkill -9 -f 'gpu-screen-recorder.*portal' 2>/dev/null || true"]) + } + } +} diff --git a/Services/Settings.qml b/Services/Settings.qml index 422baa9..a732496 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -105,9 +105,8 @@ Singleton { property string videoCodec: "h264" property string quality: "very_high" property string colorRange: "limited" - property bool showCursor: true - // New: optional audio source selection (default: system output) property string audioSource: "default_output" + property bool showCursor: true } // wallpaper diff --git a/Services/Time.qml b/Services/Time.qml index 0ce3593..7a2463c 100644 --- a/Services/Time.qml +++ b/Services/Time.qml @@ -43,31 +43,51 @@ Singleton { return Math.floor(Date.now() / 1000) } - // Format an easy to read approximate duration ex: 4h32m - // Used to display the time remaining on the Battery widget - function formatVagueHumanReadableDuration(totalSeconds) { - const hours = Math.floor(totalSeconds / 3600) - const minutes = Math.floor((totalSeconds - (hours * 3600)) / 60) - const seconds = totalSeconds - (hours * 3600) - (minutes * 60) - var str = "" - if (hours) { - str += hours.toString() + "h" - } - if (minutes) { - str += minutes.toString() + "m" - } - if (!hours && !minutes) { - str += seconds.toString() + "s" - } - return str - } + /** + * Formats a Date object into a YYYYMMDD-HHMMSS string. + * @param {Date} [date=new Date()] - The date to format. Defaults to the current date and time. + * @returns {string} The formatted date string. + */ + function getFormattedTimestamp(date = new Date()) { + const year = date.getFullYear() - Timer { - interval: 1000 - repeat: true - running: true + // getMonth() is zero-based, so we add 1 + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') - onTriggered: root.date = new Date() - } + const hours = String(date.getHours()).padStart(2, '0') + const minutes = String(date.getMinutes()).padStart(2, '0') + const seconds = String(date.getSeconds()).padStart(2, '0') + + return `${year}${month}${day}-${hours}${minutes}${seconds}` +} + +// Format an easy to read approximate duration ex: 4h32m +// Used to display the time remaining on the Battery widget +function formatVagueHumanReadableDuration(totalSeconds) { + const hours = Math.floor(totalSeconds / 3600) + const minutes = Math.floor((totalSeconds - (hours * 3600)) / 60) + const seconds = totalSeconds - (hours * 3600) - (minutes * 60) + + var str = "" + if (hours) { + str += hours.toString() + "h" + } + if (minutes) { + str += minutes.toString() + "m" + } + if (!hours && !minutes) { + str += seconds.toString() + "s" + } + return str +} + +Timer { + interval: 1000 + repeat: true + running: true + + onTriggered: root.date = new Date() +} } diff --git a/Services/Wallpapers.qml b/Services/Wallpapers.qml index 1b5792e..6a0c183 100644 --- a/Services/Wallpapers.qml +++ b/Services/Wallpapers.qml @@ -50,9 +50,10 @@ Singleton { } else { transitionType = Settings.data.wallpaper.swww.transitionType } - + changeWallpaperProcess.running = true } else { + // Fallback: update the settings directly for non-SWWW mode //console.log("[WP] Not using Swww, setting wallpaper directly") } @@ -140,7 +141,7 @@ Singleton { running: false onStarted: { - + } onExited: function (exitCode, exitStatus) { @@ -159,6 +160,7 @@ Singleton { running: false stdout: StdioCollector { onStreamFinished: { + // console.log(this.text) } } diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml index b42f2ec..28e0b6f 100644 --- a/Widgets/NIconButton.qml +++ b/Widgets/NIconButton.qml @@ -13,6 +13,7 @@ Rectangle { property string icon property string tooltipText property bool showBorder: true + property bool showFilled: false property bool enabled: true property bool hovering: false property real fontPointSize: Style.fontSizeMedium @@ -25,7 +26,7 @@ Rectangle { implicitWidth: size implicitHeight: size - color: root.hovering ? Colors.accentPrimary : "transparent" + color: (root.hovering || showFilled) ? Colors.accentPrimary : "transparent" radius: width * 0.5 border.color: showBorder ? Colors.accentPrimary : "transparent" border.width: Math.max(1, Style.borderThin * scaling) @@ -41,7 +42,7 @@ Rectangle { font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 } - color: root.hovering ? Colors.onAccent : showBorder ? Colors.accentPrimary : Colors.textPrimary + color: (root.hovering || showFilled) ? Colors.onAccent : showBorder ? Colors.accentPrimary : Colors.textPrimary horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter opacity: root.enabled ? Style.opacityFull : Style.opacityMedium From f469e7f6cd612d4024b896701d7284b72038f252 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 17:08:17 -0400 Subject: [PATCH 200/394] Fixed laggy sliders again --- Modules/Settings/Tabs/ScreenRecorder.qml | 2 +- Modules/Settings/Tabs/Wallpaper.qml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Modules/Settings/Tabs/ScreenRecorder.qml b/Modules/Settings/Tabs/ScreenRecorder.qml index 6376087..991e34b 100644 --- a/Modules/Settings/Tabs/ScreenRecorder.qml +++ b/Modules/Settings/Tabs/ScreenRecorder.qml @@ -131,7 +131,7 @@ ColumnLayout { to: 144 stepSize: 1 value: Settings.data.screenRecorder.frameRate - onMoved: Settings.data.screenRecorder.frameRate = Math.round(value) + onPressedChanged: Settings.data.screenRecorder.frameRate = Math.round(value) cutoutColor: Colors.backgroundPrimary } } diff --git a/Modules/Settings/Tabs/Wallpaper.qml b/Modules/Settings/Tabs/Wallpaper.qml index cc94974..c9bf5ae 100644 --- a/Modules/Settings/Tabs/Wallpaper.qml +++ b/Modules/Settings/Tabs/Wallpaper.qml @@ -157,7 +157,7 @@ ColumnLayout { to: 900 stepSize: 10 value: Settings.data.wallpaper.randomInterval - onMoved: Settings.data.wallpaper.randomInterval = Math.round(value) + onPressedChanged: Settings.data.wallpaper.randomInterval = Math.round(value) cutoutColor: Colors.backgroundPrimary } } @@ -300,7 +300,7 @@ ColumnLayout { to: 500 stepSize: 5 value: Settings.data.wallpaper.swww.transitionFps - onMoved: Settings.data.wallpaper.swww.transitionFps = Math.round(value) + onPressedChanged: Settings.data.wallpaper.swww.transitionFps = Math.round(value) cutoutColor: Colors.backgroundPrimary } } @@ -346,7 +346,7 @@ ColumnLayout { to: 10 stepSize: 0.05 value: Settings.data.wallpaper.swww.transitionDuration - onMoved: Settings.data.wallpaper.swww.transitionDuration = value + onPressedChanged: Settings.data.wallpaper.swww.transitionDuration = value cutoutColor: Colors.backgroundPrimary } } From c00e1f7fb36819627c3047b280c7b809d3ef0c2b Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 17:20:14 -0400 Subject: [PATCH 201/394] Settings cleanup/scaling --- Modules/Settings/Tabs/Bar.qml | 22 +++---- Modules/Settings/Tabs/General.qml | 51 ++++++---------- Modules/Settings/Tabs/ScreenRecorder.qml | 78 ++++++++++-------------- Services/Settings.qml | 1 - Widgets/NTextInput.qml | 2 +- 5 files changed, 56 insertions(+), 98 deletions(-) diff --git a/Modules/Settings/Tabs/Bar.qml b/Modules/Settings/Tabs/Bar.qml index b9b4406..bcac604 100644 --- a/Modules/Settings/Tabs/Bar.qml +++ b/Modules/Settings/Tabs/Bar.qml @@ -14,8 +14,7 @@ ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true - padding: 16 - rightPadding: 12 + padding: Style.marginMedium * scaling clip: true ScrollBar.horizontal.policy: ScrollBar.AlwaysOff ScrollBar.vertical.policy: ScrollBar.AsNeeded @@ -30,29 +29,22 @@ ColumnLayout { } ColumnLayout { - spacing: 4 + spacing: Style.marginMedium * scaling Layout.fillWidth: true NText { - text: "Bar Settings" - font.pointSize: 18 + text: "Components" + font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold color: Colors.textPrimary - Layout.bottomMargin: 8 + Layout.bottomMargin: Style.marginSmall * scaling } // Elements section ColumnLayout { - spacing: 8 + spacing: Style.marginSmall * scaling Layout.fillWidth: true - Layout.topMargin: 8 - - NText { - text: "Elements" - font.pointSize: 13 - font.weight: Style.fontWeightBold - color: Colors.textPrimary - } + Layout.topMargin: Style.marginSmall * scaling NToggle { label: "Show Active Window" diff --git a/Modules/Settings/Tabs/General.qml b/Modules/Settings/Tabs/General.qml index 9e4e08f..f0ed6f0 100644 --- a/Modules/Settings/Tabs/General.qml +++ b/Modules/Settings/Tabs/General.qml @@ -14,8 +14,7 @@ ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true - padding: 16 - rightPadding: 12 + padding: Style.marginMedium * scaling clip: true ScrollBar.horizontal.policy: ScrollBar.AlwaysOff ScrollBar.vertical.policy: ScrollBar.AsNeeded @@ -30,38 +29,31 @@ ColumnLayout { } ColumnLayout { - spacing: 4 + spacing: Style.marginTiny * scaling Layout.fillWidth: true NText { text: "General Settings" - font.pointSize: 18 + font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold color: Colors.textPrimary - Layout.bottomMargin: 8 + Layout.bottomMargin: Style.marginSmall * scaling } // Profile section ColumnLayout { - spacing: 8 + spacing: Style.marginSmall * scaling Layout.fillWidth: true - Layout.topMargin: 8 - - NText { - text: "Profile" - font.pointSize: 13 - font.weight: Style.fontWeightBold - color: Colors.textPrimary - } + Layout.topMargin: Style.marginSmall * scaling RowLayout { Layout.fillWidth: true - spacing: 16 + spacing: Style.marginLarge * scaling // Avatar preview NImageRounded { - width: 64 - height: 64 + width: 64 * scaling + height: 64 * scaling imagePath: Settings.data.general.avatarImage fallbackIcon: "person" borderColor: Colors.accentPrimary @@ -69,16 +61,16 @@ ColumnLayout { } ColumnLayout { Layout.fillWidth: true - spacing: 4 + spacing: Style.marginTiny * scaling NText { - text: "Profile Image" + text: "Profile Picture" color: Colors.textPrimary font.weight: Style.fontWeightBold } NText { text: "Your profile picture displayed in various places throughout the shell" color: Colors.textSecondary - font.pointSize: 12 + font.pointSize: Style.fontSizeSmall * scaling } NTextInput { text: Settings.data.general.avatarImage @@ -95,20 +87,20 @@ ColumnLayout { NDivider { Layout.fillWidth: true - Layout.topMargin: 26 - Layout.bottomMargin: 18 + Layout.topMargin: Style.marginLarge * 2 * scaling + Layout.bottomMargin: Style.marginLarge * scaling } ColumnLayout { - spacing: 4 + spacing: Style.marginMedium * scaling Layout.fillWidth: true NText { text: "User Interface" - font.pointSize: 18 + font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold color: Colors.textPrimary - Layout.bottomMargin: 8 + Layout.bottomMargin: Style.marginSmall * scaling } NToggle { @@ -120,15 +112,6 @@ ColumnLayout { } } - NToggle { - label: "Show Dock" - description: "Display a dock at the bottom of the screen for quick access to applications" - value: Settings.data.general.showDock - onToggled: function (v) { - Settings.data.general.showDock = v - } - } - NToggle { label: "Dim Desktop" description: "Dim the desktop when panels or menus are open" diff --git a/Modules/Settings/Tabs/ScreenRecorder.qml b/Modules/Settings/Tabs/ScreenRecorder.qml index 991e34b..7aa1e77 100644 --- a/Modules/Settings/Tabs/ScreenRecorder.qml +++ b/Modules/Settings/Tabs/ScreenRecorder.qml @@ -14,8 +14,7 @@ ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true - padding: 16 - rightPadding: 12 + padding: Style.marginMedium * scaling clip: true ScrollBar.horizontal.policy: ScrollBar.AlwaysOff ScrollBar.vertical.policy: ScrollBar.AsNeeded @@ -30,22 +29,22 @@ ColumnLayout { } ColumnLayout { - spacing: 4 + spacing: Style.marginTiny * scaling Layout.fillWidth: true NText { text: "Screen Recording" - font.pointSize: 18 + font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold color: Colors.textPrimary - Layout.bottomMargin: 8 + Layout.bottomMargin: Style.marginSmall * scaling } // Output Directory ColumnLayout { - spacing: 8 + spacing: Style.marginSmall * scaling Layout.fillWidth: true - Layout.topMargin: 8 + Layout.topMargin: Style.marginSmall * scaling NText { text: "Output Directory" @@ -73,8 +72,8 @@ ColumnLayout { NDivider { Layout.fillWidth: true - Layout.topMargin: 26 - Layout.bottomMargin: 18 + Layout.topMargin: Style.marginLarge * 2 * scaling + Layout.bottomMargin: Style.marginLarge * scaling } // Video Settings @@ -84,17 +83,17 @@ ColumnLayout { NText { text: "Video Settings" - font.pointSize: 18 + font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold color: Colors.textPrimary - Layout.bottomMargin: 8 + Layout.bottomMargin: Style.marginSmall * scaling } // Frame Rate ColumnLayout { - spacing: 8 + spacing: Style.marginSmall * scaling Layout.fillWidth: true - Layout.topMargin: 8 + Layout.topMargin: Style.marginSmall * scaling NText { text: "Frame Rate" @@ -111,36 +110,21 @@ ColumnLayout { Layout.fillWidth: true } - RowLayout { - Layout.fillWidth: true - - NText { - text: Settings.data.screenRecorder.frameRate + " FPS" - font.pointSize: 13 - color: Colors.textPrimary + NComboBox { + optionsKeys: ["30", "60", "120", "240"] + optionsLabels: ["30 FPS", "60 FPS", "120 FPS", "240 FPS"] + currentKey: Settings.data.screenRecorder.frameRate + onSelected: function (key) { + Settings.data.screenRecorder.frameRate = key } - - Item { - Layout.fillWidth: true - } - } - - NSlider { - Layout.fillWidth: true - from: 24 - to: 144 - stepSize: 1 - value: Settings.data.screenRecorder.frameRate - onPressedChanged: Settings.data.screenRecorder.frameRate = Math.round(value) - cutoutColor: Colors.backgroundPrimary } } // Video Quality ColumnLayout { - spacing: 8 + spacing: Style.marginSmall * scaling Layout.fillWidth: true - Layout.topMargin: 8 + Layout.topMargin: Style.marginSmall * scaling NText { text: "Video Quality" @@ -171,7 +155,7 @@ ColumnLayout { ColumnLayout { spacing: 8 Layout.fillWidth: true - Layout.topMargin: 8 + Layout.topMargin: Style.marginSmall * scaling NText { text: "Video Codec" @@ -200,9 +184,9 @@ ColumnLayout { // Color Range ColumnLayout { - spacing: 8 + spacing: Style.marginSmall * scaling Layout.fillWidth: true - Layout.topMargin: 8 + Layout.topMargin: Style.marginSmall * scaling NText { text: "Color Range" @@ -232,13 +216,13 @@ ColumnLayout { NDivider { Layout.fillWidth: true - Layout.topMargin: 26 - Layout.bottomMargin: 18 + Layout.topMargin: Style.marginLarge * 2 * scaling + Layout.bottomMargin: Style.marginLarge * scaling } // Audio Settings ColumnLayout { - spacing: 4 + spacing: Style.spacingTiny * scaling Layout.fillWidth: true NText { @@ -246,14 +230,14 @@ ColumnLayout { font.pointSize: 18 font.weight: Style.fontWeightBold color: Colors.textPrimary - Layout.bottomMargin: 8 + Layout.bottomMargin: Style.marginSmall * scaling } // Audio Source ColumnLayout { - spacing: 8 + spacing: Style.marginSmall * scaling Layout.fillWidth: true - Layout.topMargin: 8 + Layout.topMargin: Style.marginSmall * scaling NText { text: "Audio Source" @@ -282,9 +266,9 @@ ColumnLayout { // Audio Codec ColumnLayout { - spacing: 8 + spacing: Style.marginSmall * scaling Layout.fillWidth: true - Layout.topMargin: 8 + Layout.topMargin: Style.marginSmall * scaling NText { text: "Audio Codec" diff --git a/Services/Settings.qml b/Services/Settings.qml index a732496..39df007 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -82,7 +82,6 @@ Singleton { property string avatarImage: defaultAvatar property bool dimDesktop: true property bool showScreenCorners: false - property bool showDock: false } // location diff --git a/Widgets/NTextInput.qml b/Widgets/NTextInput.qml index cceadcf..2787b90 100644 --- a/Widgets/NTextInput.qml +++ b/Widgets/NTextInput.qml @@ -17,8 +17,8 @@ Item { signal editingFinished // Sizing - implicitHeight: Style.baseWidgetSize * 1.25 * scaling implicitWidth: 320 * scaling + implicitHeight: Style.baseWidgetSize * 1.25 * scaling // Container Rectangle { From bf3f0cf88d6137749a877d7659af4fa9522af277 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 17:31:09 -0400 Subject: [PATCH 202/394] Settings ScreenRecorder wip --- Modules/Settings/Tabs/ScreenRecorder.qml | 99 +++++++++++++----------- Modules/Settings/Tabs/TimeWeather.qml | 40 ++++------ Widgets/NToggle.qml | 2 +- 3 files changed, 69 insertions(+), 72 deletions(-) diff --git a/Modules/Settings/Tabs/ScreenRecorder.qml b/Modules/Settings/Tabs/ScreenRecorder.qml index 7aa1e77..f9346a8 100644 --- a/Modules/Settings/Tabs/ScreenRecorder.qml +++ b/Modules/Settings/Tabs/ScreenRecorder.qml @@ -46,28 +46,44 @@ ColumnLayout { Layout.fillWidth: true Layout.topMargin: Style.marginSmall * scaling - NText { - text: "Output Directory" - font.pointSize: 13 - font.weight: Style.fontWeightBold - color: Colors.textPrimary - } + ColumnLayout { + spacing: Style.marginTiny * scaling - NText { - text: "Directory where screen recordings will be saved" - font.pointSize: 12 - color: Colors.textSecondary - wrapMode: Text.WordWrap - Layout.fillWidth: true + NText { + text: "Output Directory" + font.weight: Style.fontWeightBold + color: Colors.textPrimary + } + + NText { + text: "Directory where screen recordings will be saved" + font.pointSize: Style.fontSizeSmall * scaling + color: Colors.textSecondary + wrapMode: Text.WordWrap + } } NTextInput { text: Settings.data.screenRecorder.directory - Layout.fillWidth: true onEditingFinished: { Settings.data.screenRecorder.directory = text } } + + ColumnLayout { + spacing: Style.marginSmall * scaling + Layout.fillWidth: true + Layout.topMargin: Style.marginMedium * scaling + // Show Cursor + NToggle { + label: "Show Cursor" + description: "Record mouse cursor in the video" + value: Settings.data.screenRecorder.showCursor + onToggled: function (newValue) { + Settings.data.screenRecorder.showCursor = newValue + } + } + } } NDivider { @@ -78,7 +94,7 @@ ColumnLayout { // Video Settings ColumnLayout { - spacing: 4 + spacing: Style.marginTiny * scaling Layout.fillWidth: true NText { @@ -95,24 +111,24 @@ ColumnLayout { Layout.fillWidth: true Layout.topMargin: Style.marginSmall * scaling - NText { - text: "Frame Rate" - font.pointSize: 13 - font.weight: Style.fontWeightBold - color: Colors.textPrimary - } + ColumnLayout { + spacing: Style.marginTiny * scaling + NText { + text: "Frame Rate" + font.weight: Style.fontWeightBold + color: Colors.textPrimary + } - NText { - text: "Target frame rate for screen recordings (default: 60)" - font.pointSize: 12 - color: Colors.textSecondary - wrapMode: Text.WordWrap - Layout.fillWidth: true + NText { + text: "Target frame rate for screen recordings (default: 60)" + font.pointSize: Style.fontSizeSmall * scaling + color: Colors.textSecondary + wrapMode: Text.WordWrap + } } - NComboBox { - optionsKeys: ["30", "60", "120", "240"] - optionsLabels: ["30 FPS", "60 FPS", "120 FPS", "240 FPS"] + optionsKeys: ["30", "60", "120", "2Style.marginLarge * scaling0"] + optionsLabels: ["30 FPS", "60 FPS", "120 FPS", "2Style.marginLarge * scaling0 FPS"] currentKey: Settings.data.screenRecorder.frameRate onSelected: function (key) { Settings.data.screenRecorder.frameRate = key @@ -153,20 +169,19 @@ ColumnLayout { // Video Codec ColumnLayout { - spacing: 8 + spacing: Style.marginTiny * scaling Layout.fillWidth: true Layout.topMargin: Style.marginSmall * scaling NText { text: "Video Codec" - font.pointSize: 13 font.weight: Style.fontWeightBold color: Colors.textPrimary } NText { text: "Different codecs offer different compression and compatibility" - font.pointSize: 12 + font.pointSize: Style.fontSizeSmall * scaling color: Colors.textSecondary wrapMode: Text.WordWrap Layout.fillWidth: true @@ -190,14 +205,14 @@ ColumnLayout { NText { text: "Color Range" - font.pointSize: 13 + font.weight: Style.fontWeightBold color: Colors.textPrimary } NText { text: "Limited is recommended for better compatibility" - font.pointSize: 12 + font.pointSize: Style.fontSizeSmall * scaling color: Colors.textSecondary wrapMode: Text.WordWrap Layout.fillWidth: true @@ -227,7 +242,7 @@ ColumnLayout { NText { text: "Audio Settings" - font.pointSize: 18 + font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold color: Colors.textPrimary Layout.bottomMargin: Style.marginSmall * scaling @@ -241,14 +256,14 @@ ColumnLayout { NText { text: "Audio Source" - font.pointSize: 13 + font.weight: Style.fontWeightBold color: Colors.textPrimary } NText { text: "Audio source to capture during recording" - font.pointSize: 12 + font.pointSize: Style.fontSizeSmall * scaling color: Colors.textSecondary wrapMode: Text.WordWrap Layout.fillWidth: true @@ -294,16 +309,6 @@ ColumnLayout { } } } - - // Show Cursor - NToggle { - label: "Show Cursor" - description: "Record mouse cursor in the video" - value: Settings.data.screenRecorder.showCursor - onToggled: function (newValue) { - Settings.data.screenRecorder.showCursor = newValue - } - } } } } diff --git a/Modules/Settings/Tabs/TimeWeather.qml b/Modules/Settings/Tabs/TimeWeather.qml index f8d43d7..383eeba 100644 --- a/Modules/Settings/Tabs/TimeWeather.qml +++ b/Modules/Settings/Tabs/TimeWeather.qml @@ -14,8 +14,7 @@ ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true - padding: 16 - rightPadding: 12 + padding: Style.marginMedium * scaling clip: true ScrollBar.horizontal.policy: ScrollBar.AlwaysOff ScrollBar.vertical.policy: ScrollBar.AsNeeded @@ -30,29 +29,22 @@ ColumnLayout { } ColumnLayout { - spacing: 4 + spacing: Style.marginTiny * scaling Layout.fillWidth: true NText { - text: "Time & Weather Settings" - font.pointSize: 18 + text: "Location" + font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold color: Colors.textPrimary - Layout.bottomMargin: 8 + Layout.bottomMargin: Style.marginSmall * scaling } // Location section ColumnLayout { - spacing: 8 + spacing: Style.marginMedium * scaling Layout.fillWidth: true - Layout.topMargin: 8 - - NText { - text: "Location" - font.pointSize: 13 - font.weight: Style.fontWeightBold - color: Colors.textPrimary - } + Layout.topMargin: Style.marginSmall * scaling NTextInput { text: Settings.data.location.name @@ -66,18 +58,18 @@ ColumnLayout { NDivider { Layout.fillWidth: true - Layout.topMargin: 26 - Layout.bottomMargin: 18 + Layout.topMargin: Style.marginLarge * 2 * scaling + Layout.bottomMargin: Style.marginLarge * scaling } // Time section ColumnLayout { - spacing: 4 + spacing: Style.marginMedium * scaling Layout.fillWidth: true NText { text: "Time Format" - font.pointSize: 18 + font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold color: Colors.textPrimary Layout.bottomMargin: 8 @@ -104,21 +96,21 @@ ColumnLayout { NDivider { Layout.fillWidth: true - Layout.topMargin: 26 - Layout.bottomMargin: 18 + Layout.topMargin: Style.marginLarge * 2 * scaling + Layout.bottomMargin: Style.marginLarge * scaling } // Weather section ColumnLayout { - spacing: 4 + spacing: Style.marginMedium * scaling Layout.fillWidth: true NText { text: "Weather" - font.pointSize: 18 + font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold color: Colors.textPrimary - Layout.bottomMargin: 8 + Layout.bottomMargin: Style.marginSmall * scaling } NToggle { diff --git a/Widgets/NToggle.qml b/Widgets/NToggle.qml index 55d4e2e..9073f7c 100644 --- a/Widgets/NToggle.qml +++ b/Widgets/NToggle.qml @@ -18,7 +18,7 @@ RowLayout { Layout.fillWidth: true ColumnLayout { - spacing: 2 * scaling + spacing: Style.marginTiniest * scaling Layout.fillWidth: true NText { From 3997a369ec44d3ff22c0434ed2b131bb9ea397e9 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 18:03:00 -0400 Subject: [PATCH 203/394] NComboBox now includes label + description still working on settings --- Modules/Settings/Tabs/Bar.qml | 78 ++++----- Modules/Settings/Tabs/General.qml | 5 +- Modules/Settings/Tabs/ScreenRecorder.qml | 213 ++++++----------------- Modules/Settings/Tabs/TimeWeather.qml | 2 +- Widgets/NComboBox.qml | 148 +++++++++------- 5 files changed, 177 insertions(+), 269 deletions(-) diff --git a/Modules/Settings/Tabs/Bar.qml b/Modules/Settings/Tabs/Bar.qml index bcac604..39f609a 100644 --- a/Modules/Settings/Tabs/Bar.qml +++ b/Modules/Settings/Tabs/Bar.qml @@ -29,7 +29,7 @@ ColumnLayout { } ColumnLayout { - spacing: Style.marginMedium * scaling + spacing: Style.marginLarge * scaling Layout.fillWidth: true NText { @@ -37,58 +37,50 @@ ColumnLayout { font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold color: Colors.textPrimary - Layout.bottomMargin: Style.marginSmall * scaling } - // Elements section - ColumnLayout { - spacing: Style.marginSmall * scaling - Layout.fillWidth: true - Layout.topMargin: Style.marginSmall * scaling - - NToggle { - label: "Show Active Window" - description: "Display the title of the currently focused window below the bar" - value: Settings.data.bar.showActiveWindow - onToggled: function (newValue) { - Settings.data.bar.showActiveWindow = newValue - } + NToggle { + label: "Show Active Window" + description: "Display the title of the currently focused window below the bar" + value: Settings.data.bar.showActiveWindow + onToggled: function (newValue) { + Settings.data.bar.showActiveWindow = newValue } + } - NToggle { - label: "Show Active Window Icon" - description: "Display the icon of the currently focused window" - value: Settings.data.bar.showActiveWindowIcon - onToggled: function (newValue) { - Settings.data.bar.showActiveWindowIcon = newValue - } + NToggle { + label: "Show Active Window Icon" + description: "Display the icon of the currently focused window" + value: Settings.data.bar.showActiveWindowIcon + onToggled: function (newValue) { + Settings.data.bar.showActiveWindowIcon = newValue } + } - NToggle { - label: "Show System Info" - description: "Display system information (CPU, RAM, Temperature)" - value: Settings.data.bar.showSystemInfo - onToggled: function (newValue) { - Settings.data.bar.showSystemInfo = newValue - } + NToggle { + label: "Show System Info" + description: "Display system information (CPU, RAM, Temperature)" + value: Settings.data.bar.showSystemInfo + onToggled: function (newValue) { + Settings.data.bar.showSystemInfo = newValue } + } - NToggle { - label: "Show Taskbar" - description: "Display a taskbar showing currently open windows" - value: Settings.data.bar.showTaskbar - onToggled: function (newValue) { - Settings.data.bar.showTaskbar = newValue - } + NToggle { + label: "Show Taskbar" + description: "Display a taskbar showing currently open windows" + value: Settings.data.bar.showTaskbar + onToggled: function (newValue) { + Settings.data.bar.showTaskbar = newValue } + } - NToggle { - label: "Show Media" - description: "Display media controls and information" - value: Settings.data.bar.showMedia - onToggled: function (newValue) { - Settings.data.bar.showMedia = newValue - } + NToggle { + label: "Show Media" + description: "Display media controls and information" + value: Settings.data.bar.showMedia + onToggled: function (newValue) { + Settings.data.bar.showMedia = newValue } } } diff --git a/Modules/Settings/Tabs/General.qml b/Modules/Settings/Tabs/General.qml index f0ed6f0..343e4ef 100644 --- a/Modules/Settings/Tabs/General.qml +++ b/Modules/Settings/Tabs/General.qml @@ -29,7 +29,7 @@ ColumnLayout { } ColumnLayout { - spacing: Style.marginTiny * scaling + spacing: Style.marginLarge * scaling Layout.fillWidth: true NText { @@ -37,7 +37,6 @@ ColumnLayout { font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold color: Colors.textPrimary - Layout.bottomMargin: Style.marginSmall * scaling } // Profile section @@ -92,7 +91,7 @@ ColumnLayout { } ColumnLayout { - spacing: Style.marginMedium * scaling + spacing: Style.marginLarge * scaling Layout.fillWidth: true NText { diff --git a/Modules/Settings/Tabs/ScreenRecorder.qml b/Modules/Settings/Tabs/ScreenRecorder.qml index f9346a8..68dc670 100644 --- a/Modules/Settings/Tabs/ScreenRecorder.qml +++ b/Modules/Settings/Tabs/ScreenRecorder.qml @@ -94,7 +94,7 @@ ColumnLayout { // Video Settings ColumnLayout { - spacing: Style.marginTiny * scaling + spacing: Style.marginLarge * scaling Layout.fillWidth: true NText { @@ -106,125 +106,50 @@ ColumnLayout { } // Frame Rate - ColumnLayout { - spacing: Style.marginSmall * scaling - Layout.fillWidth: true - Layout.topMargin: Style.marginSmall * scaling - - ColumnLayout { - spacing: Style.marginTiny * scaling - NText { - text: "Frame Rate" - font.weight: Style.fontWeightBold - color: Colors.textPrimary - } - - NText { - text: "Target frame rate for screen recordings (default: 60)" - font.pointSize: Style.fontSizeSmall * scaling - color: Colors.textSecondary - wrapMode: Text.WordWrap - } - } - NComboBox { - optionsKeys: ["30", "60", "120", "2Style.marginLarge * scaling0"] - optionsLabels: ["30 FPS", "60 FPS", "120 FPS", "2Style.marginLarge * scaling0 FPS"] - currentKey: Settings.data.screenRecorder.frameRate - onSelected: function (key) { - Settings.data.screenRecorder.frameRate = key - } + NComboBox { + label: "Frame Rate" + description: "Target frame rate for screen recordings (default: 60)" + optionsKeys: ["30", "60", "120", "240"] + optionsLabels: ["30 FPS", "60 FPS", "120 FPS", "240 FPS"] + currentKey: Settings.data.screenRecorder.frameRate + onSelected: function (key) { + Settings.data.screenRecorder.frameRate = key } } // Video Quality - ColumnLayout { - spacing: Style.marginSmall * scaling - Layout.fillWidth: true - Layout.topMargin: Style.marginSmall * scaling - - NText { - text: "Video Quality" - font.pointSize: 13 - font.weight: Style.fontWeightBold - color: Colors.textPrimary - } - - NText { - text: "Higher quality results in larger file sizes" - font.pointSize: 12 - color: Colors.textSecondary - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - - NComboBox { - optionsKeys: ["medium", "high", "very_high", "ultra"] - optionsLabels: ["Medium", "High", "Very High", "Ultra"] - currentKey: Settings.data.screenRecorder.quality - onSelected: function (key) { - Settings.data.screenRecorder.quality = key - } + NComboBox { + label: "Video Quality" + description: "Higher quality results in larger file sizes" + optionsKeys: ["medium", "high", "very_high", "ultra"] + optionsLabels: ["Medium", "High", "Very High", "Ultra"] + currentKey: Settings.data.screenRecorder.quality + onSelected: function (key) { + Settings.data.screenRecorder.quality = key } } // Video Codec - ColumnLayout { - spacing: Style.marginTiny * scaling - Layout.fillWidth: true - Layout.topMargin: Style.marginSmall * scaling - - NText { - text: "Video Codec" - font.weight: Style.fontWeightBold - color: Colors.textPrimary - } - - NText { - text: "Different codecs offer different compression and compatibility" - font.pointSize: Style.fontSizeSmall * scaling - color: Colors.textSecondary - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - - NComboBox { - optionsKeys: ["h264", "hevc", "av1", "vp8", "vp9"] - optionsLabels: ["H264", "HEVC", "AV1", "VP8", "VP9"] - currentKey: Settings.data.screenRecorder.videoCodec - onSelected: function (key) { - Settings.data.screenRecorder.videoCodec = key - } + NComboBox { + label: "Video Codec" + description: "Different codecs offer different compression and compatibility" + optionsKeys: ["h264", "hevc", "av1", "vp8", "vp9"] + optionsLabels: ["H264", "HEVC", "AV1", "VP8", "VP9"] + currentKey: Settings.data.screenRecorder.videoCodec + onSelected: function (key) { + Settings.data.screenRecorder.videoCodec = key } } // Color Range - ColumnLayout { - spacing: Style.marginSmall * scaling - Layout.fillWidth: true - Layout.topMargin: Style.marginSmall * scaling - - NText { - text: "Color Range" - - font.weight: Style.fontWeightBold - color: Colors.textPrimary - } - - NText { - text: "Limited is recommended for better compatibility" - font.pointSize: Style.fontSizeSmall * scaling - color: Colors.textSecondary - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - - NComboBox { - optionsKeys: ["limited", "full"] - optionsLabels: ["Limited", "Full"] - currentKey: Settings.data.screenRecorder.colorRange - onSelected: function (key) { - Settings.data.screenRecorder.colorRange = key - } + NComboBox { + label: "Color Range" + description: "Limited is recommended for better compatibility" + optionsKeys: ["limited", "full"] + optionsLabels: ["Limited", "Full"] + currentKey: Settings.data.screenRecorder.colorRange + onSelected: function (key) { + Settings.data.screenRecorder.colorRange = key } } } @@ -237,7 +162,7 @@ ColumnLayout { // Audio Settings ColumnLayout { - spacing: Style.spacingTiny * scaling + spacing: Style.marginLarge * scaling Layout.fillWidth: true NText { @@ -249,64 +174,26 @@ ColumnLayout { } // Audio Source - ColumnLayout { - spacing: Style.marginSmall * scaling - Layout.fillWidth: true - Layout.topMargin: Style.marginSmall * scaling - - NText { - text: "Audio Source" - - font.weight: Style.fontWeightBold - color: Colors.textPrimary - } - - NText { - text: "Audio source to capture during recording" - font.pointSize: Style.fontSizeSmall * scaling - color: Colors.textSecondary - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - - NComboBox { - optionsKeys: ["default_output", "default_input", "both"] - optionsLabels: ["System Audio", "Microphone", "System Audio + Microphone"] - currentKey: Settings.data.screenRecorder.audioSource - onSelected: function (key) { - Settings.data.screenRecorder.audioSource = key - } + NComboBox { + label: "Audio Source" + description: "Audio source to capture during recording" + optionsKeys: ["default_output", "default_input", "both"] + optionsLabels: ["System Audio", "Microphone", "System Audio + Microphone"] + currentKey: Settings.data.screenRecorder.audioSource + onSelected: function (key) { + Settings.data.screenRecorder.audioSource = key } } // Audio Codec - ColumnLayout { - spacing: Style.marginSmall * scaling - Layout.fillWidth: true - Layout.topMargin: Style.marginSmall * scaling - - NText { - text: "Audio Codec" - font.pointSize: 13 - font.weight: Style.fontWeightBold - color: Colors.textPrimary - } - - NText { - text: "Opus is recommended for best performance and smallest audio size" - font.pointSize: 12 - color: Colors.textSecondary - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - - NComboBox { - optionsKeys: ["opus", "aac"] - optionsLabels: ["OPUS", "AAC"] - currentKey: Settings.data.screenRecorder.audioCodec - onSelected: function (key) { - Settings.data.screenRecorder.audioCodec = key - } + NComboBox { + label: "Audio Codec" + description: "Opus is recommended for best performance and smallest audio size" + optionsKeys: ["opus", "aac"] + optionsLabels: ["OPUS", "AAC"] + currentKey: Settings.data.screenRecorder.audioCodec + onSelected: function (key) { + Settings.data.screenRecorder.audioCodec = key } } } diff --git a/Modules/Settings/Tabs/TimeWeather.qml b/Modules/Settings/Tabs/TimeWeather.qml index 383eeba..e4e228c 100644 --- a/Modules/Settings/Tabs/TimeWeather.qml +++ b/Modules/Settings/Tabs/TimeWeather.qml @@ -64,7 +64,7 @@ ColumnLayout { // Time section ColumnLayout { - spacing: Style.marginMedium * scaling + spacing: Style.marginLarge * scaling Layout.fillWidth: true NText { diff --git a/Widgets/NComboBox.qml b/Widgets/NComboBox.qml index 0b27fa6..3be8183 100644 --- a/Widgets/NComboBox.qml +++ b/Widgets/NComboBox.qml @@ -4,99 +4,129 @@ import QtQuick.Layouts import qs.Services import qs.Widgets -ComboBox { +ColumnLayout { id: root readonly property real scaling: Scaling.scale(screen) readonly property real preferredHeight: Style.baseWidgetSize * 1.25 * scaling + property string label: "" + property string description: "" property list optionsKeys: [] property list optionsLabels: [] property string currentKey: '' signal selected(string key) + spacing: Style.marginSmall * scaling Layout.fillWidth: true - Layout.preferredHeight: height - model: optionsKeys - currentIndex: model.indexOf(currentKey) - onActivated: { - root.selected(model[currentIndex]) - } + ColumnLayout { + id: mainColumn + spacing: Style.marginTiniest * scaling + Layout.fillWidth: true - // Rounded background - background: Rectangle { - implicitWidth: 120 * scaling - implicitHeight: preferredHeight - color: Colors.surfaceVariant - border.color: root.activeFocus ? Colors.hover : Colors.outline - border.width: Math.max(1, Style.borderThin * scaling) - radius: Style.radiusMedium * scaling - } + NText { + text: label + font.pointSize: Style.fontSizeMedium * scaling + font.weight: Style.fontWeightBold + color: Colors.textPrimary + } - // Label (currently selected) - contentItem: NText { - leftPadding: Style.marginLarge * scaling - rightPadding: root.indicator.width + Style.marginLarge * scaling - font.pointSize: Style.fontSizeMedium * scaling - font.weight: Style.fontWeightBold - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - text: { - return root.optionsLabels[root.currentIndex] + NText { + text: description + font.pointSize: Style.fontSizeSmall * scaling + color: Colors.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true } } - // Drop down indicator - indicator: NText { - x: root.width - width - Style.marginMedium * scaling - y: root.topPadding + (root.availableHeight - height) / 2 - text: "arrow_drop_down" - font.family: "Material Symbols Outlined" - font.pointSize: Style.fontSizeXL * scaling - } + ComboBox { + id: combo - popup: Popup { - y: root.height - width: root.width - implicitHeight: Math.min(160 * scaling, contentItem.implicitHeight + Style.marginMedium * scaling * 2) - padding: Style.marginMedium * scaling + Layout.fillWidth: true + Layout.preferredHeight: height - contentItem: ListView { - clip: true - implicitHeight: contentHeight - model: root.popup.visible ? root.delegateModel : null - currentIndex: root.highlightedIndex - ScrollIndicator.vertical: ScrollIndicator {} + model: optionsKeys + currentIndex: model.indexOf(currentKey) + onActivated: { + root.selected(model[combo.currentIndex]) } + // Rounded background background: Rectangle { + implicitWidth: 120 * scaling + implicitHeight: preferredHeight color: Colors.surfaceVariant - border.color: Colors.outline + border.color: root.activeFocus ? Colors.hover : Colors.outline border.width: Math.max(1, Style.borderThin * scaling) radius: Style.radiusMedium * scaling } - } - - delegate: ItemDelegate { - width: root.width - highlighted: root.highlightedIndex === index + // Label (currently selected) contentItem: NText { - text: { - return root.optionsLabels[root.model.indexOf(modelData)] - } + leftPadding: Style.marginLarge * scaling + rightPadding: combo.indicator.width + Style.marginLarge * scaling font.pointSize: Style.fontSizeMedium * scaling - color: highlighted ? Colors.backgroundPrimary : Colors.textPrimary + font.weight: Style.fontWeightBold verticalAlignment: Text.AlignVCenter elide: Text.ElideRight + text: { + return root.optionsLabels[combo.currentIndex] + } } - background: Rectangle { - width: root.width - Style.marginMedium * scaling * 3 - color: highlighted ? Colors.hover : "transparent" - radius: Style.radiusSmall * scaling + // Drop down indicator + indicator: NText { + x: root.width - width - Style.marginMedium * scaling + y: root.topPadding + (root.availableHeight - height) / 2 + text: "arrow_drop_down" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXL * scaling + } + + popup: Popup { + y: root.height + width: root.width + implicitHeight: Math.min(160 * scaling, contentItem.implicitHeight + Style.marginMedium * scaling * 2) + padding: Style.marginMedium * scaling + + contentItem: ListView { + clip: true + implicitHeight: contentHeight + model: combo.popup.visible ? combo.delegateModel : null + currentIndex: combo.highlightedIndex + ScrollIndicator.vertical: ScrollIndicator {} + } + + background: Rectangle { + color: Colors.surfaceVariant + border.color: Colors.outline + border.width: Math.max(1, Style.borderThin * scaling) + radius: Style.radiusMedium * scaling + } + } + + delegate: ItemDelegate { + width: root.width + highlighted: root.highlightedIndex === index + + contentItem: NText { + text: { + return root.optionsLabels[combo.model.indexOf(modelData)] + } + font.pointSize: Style.fontSizeMedium * scaling + color: highlighted ? Colors.backgroundPrimary : Colors.textPrimary + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + background: Rectangle { + width: root.width - Style.marginMedium * scaling * 3 + color: highlighted ? Colors.hover : "transparent" + radius: Style.radiusSmall * scaling + } } } } From 0417590221d108a09bfad416932273d14849a62e Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 18:34:26 -0400 Subject: [PATCH 204/394] NCombox + NTextInput proper label/description support + Associated changes in window settings --- Modules/DemoPanel/DemoPanel.qml | 10 ++- Modules/Settings/Tabs/General.qml | 27 ++---- Modules/Settings/Tabs/Network.qml | 48 ++++------- Modules/Settings/Tabs/ScreenRecorder.qml | 22 +---- Modules/Settings/Tabs/TimeWeather.qml | 6 +- Modules/Settings/Tabs/Wallpaper.qml | 3 +- Widgets/NComboBox.qml | 17 ++-- Widgets/NTextInput.qml | 103 ++++++++++++++--------- 8 files changed, 111 insertions(+), 125 deletions(-) diff --git a/Modules/DemoPanel/DemoPanel.qml b/Modules/DemoPanel/DemoPanel.qml index 877cb4c..6710202 100644 --- a/Modules/DemoPanel/DemoPanel.qml +++ b/Modules/DemoPanel/DemoPanel.qml @@ -146,6 +146,8 @@ NLoader { } NComboBox { + label: "Animal" + description: "What's your favorite" optionsKeys: ["cat", "dog", "bird", "monkey", "fish", "turtle", "elephant", "tiger"] optionsLabels: ["Cat", "Dog", "Bird", "Monkey", "Fish", "Turtle", "Elephant", "Tiger"] currentKey: "cat" @@ -169,14 +171,16 @@ NLoader { } NTextInput { + label: "Input label" + description: "A cool description" text: "Type anything" Layout.fillWidth: true onEditingFinished: { } - NDivider { - Layout.fillWidth: true - } + } + NDivider { + Layout.fillWidth: true } } diff --git a/Modules/Settings/Tabs/General.qml b/Modules/Settings/Tabs/General.qml index 343e4ef..2718914 100644 --- a/Modules/Settings/Tabs/General.qml +++ b/Modules/Settings/Tabs/General.qml @@ -58,26 +58,15 @@ ColumnLayout { borderColor: Colors.accentPrimary borderWidth: Math.max(1, Style.borderMedium) } - ColumnLayout { + + NTextInput { + label: "Profile Picture" + description: "Your profile picture displayed in various places throughout the shell" + text: Settings.data.general.avatarImage + placeholderText: "/home/user/.face" Layout.fillWidth: true - spacing: Style.marginTiny * scaling - NText { - text: "Profile Picture" - color: Colors.textPrimary - font.weight: Style.fontWeightBold - } - NText { - text: "Your profile picture displayed in various places throughout the shell" - color: Colors.textSecondary - font.pointSize: Style.fontSizeSmall * scaling - } - NTextInput { - text: Settings.data.general.avatarImage - placeholderText: "/home/user/.face" - Layout.fillWidth: true - onEditingFinished: { - Settings.data.general.avatarImage = text - } + onEditingFinished: { + Settings.data.general.avatarImage = text } } } diff --git a/Modules/Settings/Tabs/Network.qml b/Modules/Settings/Tabs/Network.qml index 3c31d74..9229fa8 100644 --- a/Modules/Settings/Tabs/Network.qml +++ b/Modules/Settings/Tabs/Network.qml @@ -16,8 +16,7 @@ ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true - padding: 16 - rightPadding: 12 + padding: Style.marginMedium * scaling clip: true ScrollBar.horizontal.policy: ScrollBar.AlwaysOff ScrollBar.vertical.policy: ScrollBar.AsNeeded @@ -32,46 +31,31 @@ ColumnLayout { } ColumnLayout { - spacing: 4 + spacing: Style.marginLarge * scaling Layout.fillWidth: true NText { text: "Network Settings" - font.pointSize: 18 + font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold color: Colors.textPrimary - Layout.bottomMargin: 8 } - // Network interfaces section - ColumnLayout { - spacing: 8 - Layout.fillWidth: true - Layout.topMargin: 8 - - NText { - text: "Network Interfaces" - font.pointSize: 13 - font.weight: Style.fontWeightBold - color: Colors.textPrimary + NToggle { + label: "WiFi Enabled" + description: "Enable WiFi connectivity" + value: Settings.data.network.wifiEnabled + onToggled: function (newValue) { + Settings.data.network.wifiEnabled = newValue } + } - NToggle { - label: "WiFi Enabled" - description: "Enable WiFi connectivity" - value: Settings.data.network.wifiEnabled - onToggled: function (newValue) { - Settings.data.network.wifiEnabled = newValue - } - } - - NToggle { - label: "Bluetooth Enabled" - description: "Enable Bluetooth connectivity" - value: Settings.data.network.bluetoothEnabled - onToggled: function (newValue) { - Settings.data.network.bluetoothEnabled = newValue - } + NToggle { + label: "Bluetooth Enabled" + description: "Enable Bluetooth connectivity" + value: Settings.data.network.bluetoothEnabled + onToggled: function (newValue) { + Settings.data.network.bluetoothEnabled = newValue } } } diff --git a/Modules/Settings/Tabs/ScreenRecorder.qml b/Modules/Settings/Tabs/ScreenRecorder.qml index 68dc670..b18d2ed 100644 --- a/Modules/Settings/Tabs/ScreenRecorder.qml +++ b/Modules/Settings/Tabs/ScreenRecorder.qml @@ -33,7 +33,7 @@ ColumnLayout { Layout.fillWidth: true NText { - text: "Screen Recording" + text: "Recording" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold color: Colors.textPrimary @@ -46,24 +46,10 @@ ColumnLayout { Layout.fillWidth: true Layout.topMargin: Style.marginSmall * scaling - ColumnLayout { - spacing: Style.marginTiny * scaling - - NText { - text: "Output Directory" - font.weight: Style.fontWeightBold - color: Colors.textPrimary - } - - NText { - text: "Directory where screen recordings will be saved" - font.pointSize: Style.fontSizeSmall * scaling - color: Colors.textSecondary - wrapMode: Text.WordWrap - } - } - NTextInput { + label: "Output Directory" + description: "Directory where screen recordings will be saved" + placeholderText: "/home/xxx/Videos" text: Settings.data.screenRecorder.directory onEditingFinished: { Settings.data.screenRecorder.directory = text diff --git a/Modules/Settings/Tabs/TimeWeather.qml b/Modules/Settings/Tabs/TimeWeather.qml index e4e228c..5f511a0 100644 --- a/Modules/Settings/Tabs/TimeWeather.qml +++ b/Modules/Settings/Tabs/TimeWeather.qml @@ -47,8 +47,10 @@ ColumnLayout { Layout.topMargin: Style.marginSmall * scaling NTextInput { + label: "Location name" + description: "Choose a known location near you" text: Settings.data.location.name - placeholderText: "Enter city name" + placeholderText: "Enter the location name" Layout.fillWidth: true onEditingFinished: { Settings.data.location.name = text @@ -64,7 +66,7 @@ ColumnLayout { // Time section ColumnLayout { - spacing: Style.marginLarge * scaling + spacing: Style.marginLarge * scaling Layout.fillWidth: true NText { diff --git a/Modules/Settings/Tabs/Wallpaper.qml b/Modules/Settings/Tabs/Wallpaper.qml index c9bf5ae..36c4816 100644 --- a/Modules/Settings/Tabs/Wallpaper.qml +++ b/Modules/Settings/Tabs/Wallpaper.qml @@ -14,8 +14,7 @@ ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true - padding: 16 - rightPadding: 12 + padding: Style.marginMedium * scaling clip: true ScrollBar.horizontal.policy: ScrollBar.AlwaysOff ScrollBar.vertical.policy: ScrollBar.AsNeeded diff --git a/Widgets/NComboBox.qml b/Widgets/NComboBox.qml index 3be8183..55a63f4 100644 --- a/Widgets/NComboBox.qml +++ b/Widgets/NComboBox.qml @@ -59,7 +59,7 @@ ColumnLayout { implicitWidth: 120 * scaling implicitHeight: preferredHeight color: Colors.surfaceVariant - border.color: root.activeFocus ? Colors.hover : Colors.outline + border.color: combo.activeFocus ? Colors.hover : Colors.outline border.width: Math.max(1, Style.borderThin * scaling) radius: Style.radiusMedium * scaling } @@ -69,7 +69,6 @@ ColumnLayout { leftPadding: Style.marginLarge * scaling rightPadding: combo.indicator.width + Style.marginLarge * scaling font.pointSize: Style.fontSizeMedium * scaling - font.weight: Style.fontWeightBold verticalAlignment: Text.AlignVCenter elide: Text.ElideRight text: { @@ -79,16 +78,16 @@ ColumnLayout { // Drop down indicator indicator: NText { - x: root.width - width - Style.marginMedium * scaling - y: root.topPadding + (root.availableHeight - height) / 2 + x: combo.width - width - Style.marginMedium * scaling + y: combo.topPadding + (combo.availableHeight - height) / 2 text: "arrow_drop_down" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXL * scaling } popup: Popup { - y: root.height - width: root.width + y: combo.height + width: combo.width implicitHeight: Math.min(160 * scaling, contentItem.implicitHeight + Style.marginMedium * scaling * 2) padding: Style.marginMedium * scaling @@ -109,8 +108,8 @@ ColumnLayout { } delegate: ItemDelegate { - width: root.width - highlighted: root.highlightedIndex === index + width: combo.width + highlighted: combo.highlightedIndex === index contentItem: NText { text: { @@ -123,7 +122,7 @@ ColumnLayout { } background: Rectangle { - width: root.width - Style.marginMedium * scaling * 3 + width: combo.width - Style.marginMedium * scaling * 3 color: highlighted ? Colors.hover : "transparent" radius: Style.radiusSmall * scaling } diff --git a/Widgets/NTextInput.qml b/Widgets/NTextInput.qml index 2787b90..8a7de1a 100644 --- a/Widgets/NTextInput.qml +++ b/Widgets/NTextInput.qml @@ -7,58 +7,81 @@ Item { id: root readonly property real scaling: Scaling.scale(screen) - - // API - property alias text: input.text - property alias placeholderText: input.placeholderText + property string label: "" + property string description: "" property bool readOnly: false property bool enabled: true + property alias text: input.text + property alias placeholderText: input.placeholderText + signal editingFinished // Sizing implicitWidth: 320 * scaling - implicitHeight: Style.baseWidgetSize * 1.25 * scaling + implicitHeight: Style.baseWidgetSize * 2.75 * scaling - // Container - Rectangle { - id: frame - anchors.fill: parent - radius: Style.radiusMedium * scaling - color: Colors.surfaceVariant - border.color: Colors.outline - border.width: Math.max(1, Style.borderThin * scaling) + ColumnLayout { + spacing: Style.marginTiniest * scaling + Layout.fillWidth: true - // Focus ring - Rectangle { - anchors.fill: parent - radius: frame.radius - color: "transparent" - border.color: input.activeFocus ? Colors.hover : "transparent" - border.width: input.activeFocus ? Math.max(1, Style.borderThin * scaling) : 0 + NText { + text: label + font.pointSize: Style.fontSizeMedium * scaling + font.weight: Style.fontWeightBold + color: Colors.textPrimary } - RowLayout { - anchors.fill: parent - anchors.leftMargin: Style.marginMedium * scaling - anchors.rightMargin: Style.marginMedium * scaling - spacing: Style.marginSmall * scaling + NText { + text: description + font.pointSize: Style.fontSizeSmall * scaling + color: Colors.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } - // 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: Colors.textPrimary - placeholderTextColor: Colors.textSecondary - background: null - font.pointSize: Style.fontSizeSmall * 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. + // Container + Rectangle { + id: frame + Layout.topMargin: Style.marginTiny * scaling + implicitWidth: root.width + implicitHeight: Style.baseWidgetSize * 1.35 * scaling + radius: Style.radiusMedium * scaling + color: Colors.surfaceVariant + border.color: Colors.outline + border.width: Math.max(1, Style.borderThin * scaling) + + // Focus ring + Rectangle { + anchors.fill: parent + radius: frame.radius + color: "transparent" + border.color: input.activeFocus ? Colors.hover : "transparent" + border.width: input.activeFocus ? Math.max(1, Style.borderThin * scaling) : 0 + } + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Style.marginMedium * scaling + anchors.rightMargin: Style.marginMedium * scaling + spacing: Style.marginSmall * 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: Colors.textPrimary + placeholderTextColor: Colors.textSecondary + background: null + font.pointSize: Style.fontSizeSmall * 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. + } } } } From b3002b42b4b7bb47008b3b6d5611f35c724c0dd5 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 18:58:22 -0400 Subject: [PATCH 205/394] SettingsWindow Loading - reworked the way we load the settings to avoid using Qt.createComponent which breaks hot reload and more - introduced a new PanelManager singleton to hold some references to some panels and later will be used to force having a single panel opened at all time --- Modules/Settings/SettingsWindow.qml | 3 ++- Modules/Settings/Tabs/Network.qml | 2 +- Modules/SidePanel/Cards/ProfileCard.qml | 16 +--------------- Services/PanelManager.qml | 10 ++++++++++ shell.qml | 9 +++++++++ 5 files changed, 23 insertions(+), 17 deletions(-) create mode 100644 Services/PanelManager.qml diff --git a/Modules/Settings/SettingsWindow.qml b/Modules/Settings/SettingsWindow.qml index ca8a4da..e388186 100644 --- a/Modules/Settings/SettingsWindow.qml +++ b/Modules/Settings/SettingsWindow.qml @@ -3,9 +3,10 @@ import QtQuick.Controls import QtQuick.Layouts import Quickshell import Quickshell.Wayland +import qs.Modules.Settings.Tabs as Tabs import qs.Services import qs.Widgets -import "Tabs" as Tabs + NLoader { id: root diff --git a/Modules/Settings/Tabs/Network.qml b/Modules/Settings/Tabs/Network.qml index 9229fa8..e3d66d5 100644 --- a/Modules/Settings/Tabs/Network.qml +++ b/Modules/Settings/Tabs/Network.qml @@ -35,7 +35,7 @@ ColumnLayout { Layout.fillWidth: true NText { - text: "Network Settings" + text: "Interfaces" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold color: Colors.textPrimary diff --git a/Modules/SidePanel/Cards/ProfileCard.qml b/Modules/SidePanel/Cards/ProfileCard.qml index ac9041a..83f39c4 100644 --- a/Modules/SidePanel/Cards/ProfileCard.qml +++ b/Modules/SidePanel/Cards/ProfileCard.qml @@ -60,21 +60,7 @@ NBox { NIconButton { icon: "settings" onClicked: { - if (!root.settingsWindow) { - const comp = Qt.createComponent("../../Settings/SettingsWindow.qml") - if (comp.status === Component.Ready) { - root.settingsWindow = comp.createObject(root) - } else { - comp.statusChanged.connect(function () { - if (comp.status === Component.Ready) { - root.settingsWindow = comp.createObject(root) - } - }) - } - } - if (root.settingsWindow) { - root.settingsWindow.isLoaded = !root.settingsWindow.isLoaded - } + PanelManager.settingsWindow.isLoaded = !PanelManager.settingsWindow.isLoaded } } NIconButton { diff --git a/Services/PanelManager.qml b/Services/PanelManager.qml new file mode 100644 index 0000000..b7cbde5 --- /dev/null +++ b/Services/PanelManager.qml @@ -0,0 +1,10 @@ +pragma Singleton + +import Quickshell +import qs.Modules.Settings + +Singleton { + id: root + + property SettingsWindow settingsWindow: null +} diff --git a/shell.qml b/shell.qml index 4248b32..fa36846 100644 --- a/shell.qml +++ b/shell.qml @@ -12,6 +12,7 @@ import qs.Modules.DemoPanel import qs.Modules.Background import qs.Modules.SidePanel import qs.Modules.Notification +import qs.Modules.Settings import qs.Services ShellRoot { @@ -38,9 +39,17 @@ ShellRoot { id: calendar } + SettingsWindow { + id: settingsWindow + } + Component.onCompleted: { + PanelManager.settingsWindow = settingsWindow + // Ensure our singleton is created as soon as possible // so we start fetching weather asap if necessary Location.init() + + } } From c5b7be411926979535b7d3b21dc3ea9b676df501 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 19:04:16 -0400 Subject: [PATCH 206/394] PanelManager autoclose working! --- Services/PanelManager.qml | 1 + Services/Settings.qml | 2 -- Widgets/NPanel.qml | 6 +++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Services/PanelManager.qml b/Services/PanelManager.qml index b7cbde5..254ea0f 100644 --- a/Services/PanelManager.qml +++ b/Services/PanelManager.qml @@ -6,5 +6,6 @@ import qs.Modules.Settings Singleton { id: root + property var openedPanel: null property SettingsWindow settingsWindow: null } diff --git a/Services/Settings.qml b/Services/Settings.qml index 39df007..7cf43a3 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -26,8 +26,6 @@ Singleton { // Used to access via Settings.data.xxx.yyy property var data: adapter - // Needed to only have one NPanel loaded at a time. <--- VERY BROKEN - //property var openPanel: null Item { Component.onCompleted: { diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 704defc..97b3eb0 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -21,10 +21,10 @@ PanelWindow { function show() { // Ensure only one panel is visible at a time using Settings as ephemeral store try { - if (Settings.openPanel && Settings.openPanel !== root && Settings.openPanel.hide) { - Settings.openPanel.hide() + if (PanelManager.openedPanel && PanelManager.openedPanel !== root && PanelManager.openedPanel.hide) { + PanelManager.openedPanel.hide() } - Settings.openPanel = root + PanelManager.openedPanel = root } catch (e) { // ignore From 9a70d8028ab70f5fe4e146be8184e97fe02b6088 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 21:32:16 -0400 Subject: [PATCH 207/394] NTooltip: Subtle gradient --- Widgets/NTooltip.qml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Widgets/NTooltip.qml b/Widgets/NTooltip.qml index 7d6bda2..9ced0b8 100644 --- a/Widgets/NTooltip.qml +++ b/Widgets/NTooltip.qml @@ -100,10 +100,18 @@ Window { Rectangle { anchors.fill: parent radius: Style.radiusMedium * scaling - color: Colors.backgroundTertiary + gradient: Gradient { + GradientStop { + position: 0.0 + color: Colors.backgroundTertiary + } + GradientStop { + position: 1.0 + color: Colors.backgroundSecondary + } + } border.color: Colors.outline border.width: Math.max(1, Style.borderThin * scaling) - opacity: Style.opacityFull z: 1 } From ce22559ee2677d0c182266c05de62a730d18d659 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 21:33:22 -0400 Subject: [PATCH 208/394] Better panel naming and removed ref to SettingsWindow --- Modules/Bar/Clock.qml | 4 +-- Modules/{DemoPanel => Demo}/DemoPanel.qml | 0 .../{SettingsWindow.qml => SettingsPanel.qml} | 17 ++++++------- Modules/SidePanel/Cards/ProfileCard.qml | 6 ++--- Services/PanelManager.qml | 1 - shell.qml | 25 ++++++++----------- 6 files changed, 22 insertions(+), 31 deletions(-) rename Modules/{DemoPanel => Demo}/DemoPanel.qml (100%) rename Modules/Settings/{SettingsWindow.qml => SettingsPanel.qml} (92%) diff --git a/Modules/Bar/Clock.qml b/Modules/Bar/Clock.qml index 19d25e8..916687f 100644 --- a/Modules/Bar/Clock.qml +++ b/Modules/Bar/Clock.qml @@ -13,7 +13,7 @@ NClock { } onEntered: { - if (!calendar.isLoaded) { + if (!calendarPanel.isLoaded) { tooltip.show() } } @@ -22,6 +22,6 @@ NClock { } onClicked: { tooltip.hide() - calendar.isLoaded = !calendar.isLoaded + calendarPanel.isLoaded = !calendarPanel.isLoaded } } diff --git a/Modules/DemoPanel/DemoPanel.qml b/Modules/Demo/DemoPanel.qml similarity index 100% rename from Modules/DemoPanel/DemoPanel.qml rename to Modules/Demo/DemoPanel.qml diff --git a/Modules/Settings/SettingsWindow.qml b/Modules/Settings/SettingsPanel.qml similarity index 92% rename from Modules/Settings/SettingsWindow.qml rename to Modules/Settings/SettingsPanel.qml index e388186..a9f0ca5 100644 --- a/Modules/Settings/SettingsWindow.qml +++ b/Modules/Settings/SettingsPanel.qml @@ -13,7 +13,7 @@ NLoader { content: Component { NPanel { - id: settingsPanel + id: panel WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand @@ -105,11 +105,11 @@ NLoader { Repeater { id: sections - model: settingsPanel.tabsModel + model: panel.tabsModel delegate: Rectangle { id: tabItem - readonly property bool selected: index === settingsPanel.currentTabIndex + readonly property bool selected: index === panel.currentTabIndex width: parent.width height: 32 * scaling // Back to original height radius: Style.radiusSmall * scaling @@ -149,7 +149,7 @@ NLoader { onEntered: tabItem.hovering = true onExited: tabItem.hovering = false onCanceled: tabItem.hovering = false - onClicked: settingsPanel.currentTabIndex = index + onClicked: panel.currentTabIndex = index } } } @@ -178,19 +178,18 @@ NLoader { Layout.fillWidth: true spacing: Style.marginSmall * scaling NText { - text: settingsPanel.tabsModel[settingsPanel.currentTabIndex].label + text: panel.tabsModel[panel.currentTabIndex].label font.pointSize: Style.fontSizeLarge * scaling font.weight: Style.fontWeightBold color: Colors.textPrimary Layout.fillWidth: true } NIconButton { - id: demoPanelToggle icon: "close" - tooltipText: "Close settings panel" + tooltipText: "Close" Layout.alignment: Qt.AlignVCenter onClicked: { - settingsWindow.isLoaded = !settingsWindow.isLoaded + settingsPanel.isLoaded = !settingsPanel.isLoaded } } } @@ -203,7 +202,7 @@ NLoader { id: stack Layout.fillWidth: true Layout.fillHeight: true - currentIndex: settingsPanel.currentTabIndex + currentIndex: panel.currentTabIndex Tabs.General {} Tabs.Bar {} diff --git a/Modules/SidePanel/Cards/ProfileCard.qml b/Modules/SidePanel/Cards/ProfileCard.qml index 83f39c4..90a23db 100644 --- a/Modules/SidePanel/Cards/ProfileCard.qml +++ b/Modules/SidePanel/Cards/ProfileCard.qml @@ -12,9 +12,6 @@ NBox { id: root readonly property real scaling: Scaling.scale(screen) - // Hold a single instance of the Settings window (root is NLoader) - property var settingsWindow: null - property string uptimeText: "--" Layout.fillWidth: true @@ -59,8 +56,9 @@ NBox { } NIconButton { icon: "settings" + tooltipText: "Open settings" onClicked: { - PanelManager.settingsWindow.isLoaded = !PanelManager.settingsWindow.isLoaded + settingsPanel.isLoaded = !settingsPanel.isLoaded } } NIconButton { diff --git a/Services/PanelManager.qml b/Services/PanelManager.qml index 254ea0f..1c50d60 100644 --- a/Services/PanelManager.qml +++ b/Services/PanelManager.qml @@ -7,5 +7,4 @@ Singleton { id: root property var openedPanel: null - property SettingsWindow settingsWindow: null } diff --git a/shell.qml b/shell.qml index fa36846..f1e589b 100644 --- a/shell.qml +++ b/shell.qml @@ -8,7 +8,7 @@ import qs.Widgets import qs.Modules.Audio import qs.Modules.Bar import qs.Modules.Calendar -import qs.Modules.DemoPanel +import qs.Modules.Demo import qs.Modules.Background import qs.Modules.SidePanel import qs.Modules.Notification @@ -31,25 +31,20 @@ ShellRoot { id: sidePanel } + Calendar { + id: calendarPanel + } + + SettingsPanel { + id: settingsPanel + } + Notification { id: notification } - Calendar { - id: calendar - } - - SettingsWindow { - id: settingsWindow - } - Component.onCompleted: { - PanelManager.settingsWindow = settingsWindow - - // Ensure our singleton is created as soon as possible - // so we start fetching weather asap if necessary + // Ensure our singleton is created as soon as possible so we start fetching weather asap Location.init() - - } } From 5c9c61cf81820d03001b87f079525d1e747a1bfa Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 23:04:43 -0400 Subject: [PATCH 209/394] NTooltip: subtle gradient, NToggle: onEnter, onExited --- Widgets/NComboBox.qml | 2 -- Widgets/NPanel.qml | 2 +- Widgets/NToggle.qml | 12 ++++++++++-- Widgets/NTooltip.qml | 18 +++++++++--------- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/Widgets/NComboBox.qml b/Widgets/NComboBox.qml index 55a63f4..7219391 100644 --- a/Widgets/NComboBox.qml +++ b/Widgets/NComboBox.qml @@ -22,7 +22,6 @@ ColumnLayout { Layout.fillWidth: true ColumnLayout { - id: mainColumn spacing: Style.marginTiniest * scaling Layout.fillWidth: true @@ -38,7 +37,6 @@ ColumnLayout { font.pointSize: Style.fontSizeSmall * scaling color: Colors.textSecondary wrapMode: Text.WordWrap - Layout.fillWidth: true } } diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 97b3eb0..58353b1 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -19,7 +19,7 @@ PanelWindow { } function show() { - // Ensure only one panel is visible at a time using Settings as ephemeral store + // Ensure only one panel is visible at a time using PanelManager as ephemeral storage try { if (PanelManager.openedPanel && PanelManager.openedPanel !== root && PanelManager.openedPanel.hide) { PanelManager.openedPanel.hide() diff --git a/Widgets/NToggle.qml b/Widgets/NToggle.qml index 9073f7c..28c766c 100644 --- a/Widgets/NToggle.qml +++ b/Widgets/NToggle.qml @@ -14,6 +14,8 @@ RowLayout { property int baseSize: Style.baseWidgetSize signal toggled(bool balue) + signal entered + signal exited Layout.fillWidth: true @@ -69,8 +71,14 @@ RowLayout { anchors.fill: parent cursorShape: Qt.PointingHandCursor hoverEnabled: true - onEntered: hovering = true - onExited: hovering = false + onEntered: { + hovering = true + root.entered() + } + onExited: { + hovering = false + root.exited() + } onClicked: { value = !value root.toggled(value) diff --git a/Widgets/NTooltip.qml b/Widgets/NTooltip.qml index 9ced0b8..361b2ce 100644 --- a/Widgets/NTooltip.qml +++ b/Widgets/NTooltip.qml @@ -101,15 +101,15 @@ Window { anchors.fill: parent radius: Style.radiusMedium * scaling gradient: Gradient { - GradientStop { - position: 0.0 - color: Colors.backgroundTertiary - } - GradientStop { - position: 1.0 - color: Colors.backgroundSecondary - } - } + GradientStop { + position: 0.0 + color: Colors.backgroundTertiary + } + GradientStop { + position: 1.0 + color: Colors.backgroundSecondary + } + } border.color: Colors.outline border.width: Math.max(1, Style.borderThin * scaling) z: 1 From d06679aad6dc1c0b8bdfb2b8c2a2b9efd155adbc Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 23:05:38 -0400 Subject: [PATCH 210/394] NSlider: now support label and description - was a little tricky, got many crashed due to layout issue with Layout.fillWidth: true --- Modules/Demo/DemoPanel.qml | 23 ++-- Modules/Settings/SettingsPanel.qml | 1 - Modules/Settings/Tabs/Wallpaper.qml | 162 ++++++----------------- Widgets/NSlider.qml | 191 +++++++++++++++++++--------- 4 files changed, 180 insertions(+), 197 deletions(-) diff --git a/Modules/Demo/DemoPanel.qml b/Modules/Demo/DemoPanel.qml index 6710202..5d59e23 100644 --- a/Modules/Demo/DemoPanel.qml +++ b/Modules/Demo/DemoPanel.qml @@ -62,22 +62,18 @@ NLoader { } RowLayout { spacing: Style.marginSmall * scaling - NText { - text: `${Math.round(Scaling.overrideScale * 100)}%` - Layout.alignment: Qt.AlignVCenter - } NSlider { - id: scaleSlider - from: 0.6 - to: 1.8 - stepSize: 0.01 - value: Scaling.overrideScale + label: "Scaling" + description: "Scaling goes brrrr" + valueSuffix: "%" + from: 60 + to: 180 + stepSize: 1 + value: Scaling.overrideScale * 100 implicitWidth: bgRect.width * 0.75 - onMoved: { - Scaling.overrideScale = value - } - onPressedChanged: { + onPressedChanged: function (pressed, value) { Scaling.overrideEnabled = true + Scaling.overrideScale = value / 100 } } NIconButton { @@ -86,6 +82,7 @@ NLoader { onClicked: { Scaling.overrideEnabled = false Scaling.overrideScale = 1.0 + console.log("Reset!") } } } diff --git a/Modules/Settings/SettingsPanel.qml b/Modules/Settings/SettingsPanel.qml index a9f0ca5..856c2b6 100644 --- a/Modules/Settings/SettingsPanel.qml +++ b/Modules/Settings/SettingsPanel.qml @@ -7,7 +7,6 @@ import qs.Modules.Settings.Tabs as Tabs import qs.Services import qs.Widgets - NLoader { id: root diff --git a/Modules/Settings/Tabs/Wallpaper.qml b/Modules/Settings/Tabs/Wallpaper.qml index 36c4816..59eb6bf 100644 --- a/Modules/Settings/Tabs/Wallpaper.qml +++ b/Modules/Settings/Tabs/Wallpaper.qml @@ -116,49 +116,19 @@ ColumnLayout { } // Wallpaper Interval - ColumnLayout { - spacing: 8 + NSlider { + label: "Wallpaper Interval" + description: "How often to change wallpapers automatically (in seconds)" + valueSuffix: "s" + from: 10 + to: 900 + stepSize: 10 + value: Settings.data.wallpaper.randomInterval + onPressedChanged: function (pressed, value) { + Settings.data.wallpaper.randomInterval = Math.round(value) + } + cutoutColor: Colors.backgroundPrimary Layout.fillWidth: true - Layout.topMargin: 8 - - NText { - text: "Wallpaper Interval" - font.pointSize: 13 - font.weight: Style.fontWeightBold - color: Colors.textPrimary - } - - NText { - text: "How often to change wallpapers automatically (in seconds)" - font.pointSize: 12 - color: Colors.textSecondary - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - - RowLayout { - Layout.fillWidth: true - - NText { - text: Settings.data.wallpaper.randomInterval + " seconds" - font.pointSize: 13 - color: Colors.textPrimary - } - - Item { - Layout.fillWidth: true - } - } - - NSlider { - Layout.fillWidth: true - from: 10 - to: 900 - stepSize: 10 - value: Settings.data.wallpaper.randomInterval - onPressedChanged: Settings.data.wallpaper.randomInterval = Math.round(value) - cutoutColor: Colors.backgroundPrimary - } } } @@ -259,95 +229,35 @@ ColumnLayout { } // Transition FPS - ColumnLayout { - spacing: 8 + NSlider { + label: "Transition FPS" + description: "Frames per second for transition animations" + valueSuffix: " FPS" + from: 30 + to: 500 + stepSize: 5 + value: Settings.data.wallpaper.swww.transitionFps + onPressedChanged: function (pressed, value) { + Settings.data.wallpaper.swww.transitionFps = Math.round(value) + } + cutoutColor: Colors.backgroundPrimary Layout.fillWidth: true - Layout.topMargin: 8 - - NText { - text: "Transition FPS" - font.pointSize: 13 - font.weight: Style.fontWeightBold - color: Colors.textPrimary - } - - NText { - text: "Frames per second for transition animations" - font.pointSize: 12 - color: Colors.textSecondary - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - - RowLayout { - Layout.fillWidth: true - - NText { - text: Settings.data.wallpaper.swww.transitionFps + " FPS" - font.pointSize: 13 - color: Colors.textPrimary - } - - Item { - Layout.fillWidth: true - } - } - - NSlider { - Layout.fillWidth: true - from: 30 - to: 500 - stepSize: 5 - value: Settings.data.wallpaper.swww.transitionFps - onPressedChanged: Settings.data.wallpaper.swww.transitionFps = Math.round(value) - cutoutColor: Colors.backgroundPrimary - } } // Transition Duration - ColumnLayout { - spacing: 8 + NSlider { + label: "Transition Duration" + description: "Duration of transition animations in seconds" + valueSuffix: "s" + from: 0.25 + to: 10 + stepSize: 0.05 + value: Settings.data.wallpaper.swww.transitionDuration + onPressedChanged: function (pressed, value) { + Settings.data.wallpaper.swww.transitionDuration = value + } + cutoutColor: Colors.backgroundPrimary Layout.fillWidth: true - Layout.topMargin: 8 - - NText { - text: "Transition Duration" - font.pointSize: 13 - font.weight: Style.fontWeightBold - color: Colors.textPrimary - } - - NText { - text: "Duration of transition animations in seconds" - font.pointSize: 12 - color: Colors.textSecondary - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - - RowLayout { - Layout.fillWidth: true - - NText { - text: Settings.data.wallpaper.swww.transitionDuration.toFixed(3) + " seconds" - font.pointSize: 13 - color: Colors.textPrimary - } - - Item { - Layout.fillWidth: true - } - } - - NSlider { - Layout.fillWidth: true - from: 0.25 - to: 10 - stepSize: 0.05 - value: Settings.data.wallpaper.swww.transitionDuration - onPressedChanged: Settings.data.wallpaper.swww.transitionDuration = value - cutoutColor: Colors.backgroundPrimary - } } } } diff --git a/Widgets/NSlider.qml b/Widgets/NSlider.qml index 2e9e6d1..4911099 100644 --- a/Widgets/NSlider.qml +++ b/Widgets/NSlider.qml @@ -1,9 +1,10 @@ import QtQuick import QtQuick.Controls import QtQuick.Effects +import QtQuick.Layouts import qs.Services -Slider { +ColumnLayout { id: root readonly property real scaling: Scaling.scale(screen) @@ -11,80 +12,156 @@ Slider { readonly property real trackHeight: knobDiameter * 0.5 readonly property real cutoutExtra: Style.baseWidgetSize * 0.1 * scaling + property string label: "" + property string description: "" + property string valueSuffix: "" + property real from: 0.0 + property real to: 1.0 + property real stepSize: 0.01 + property real value: 0.0 + // Optional color to cut the track beneath the knob (should match surrounding background) property var cutoutColor property var screen property bool snapAlways: true - snapMode: snapAlways ? Slider.SnapAlways : Slider.SnapOnRelease - implicitHeight: Math.max(trackHeight, knobDiameter) + signal pressedChanged(bool pressed, real value) + signal moved(real value) - background: Rectangle { - x: root.leftPadding - y: root.topPadding + root.availableHeight / 2 - height / 2 - implicitWidth: Style.sliderWidth - implicitHeight: trackHeight - width: root.availableWidth - height: implicitHeight - radius: height / 2 - color: Colors.surfaceVariant + Layout.fillWidth: true + spacing: Style.marginSmall * scaling - Rectangle { - id: activeTrack - width: root.visualPosition * parent.width - height: parent.height - color: Colors.accentPrimary - radius: parent.radius + RowLayout { + Layout.fillWidth: true + + ColumnLayout { + spacing: Style.marginTiniest * scaling + + NText { + text: label + font.pointSize: Style.fontSizeMedium * scaling + font.weight: Style.fontWeightBold + color: Colors.textPrimary + Layout.fillWidth: true + } + + NText { + text: description + font.pointSize: Style.fontSizeSmall * scaling + color: Colors.textSecondary + wrapMode: Text.WordWrap + } } - // Circular cutout - Rectangle { - id: knobCutout - width: knobDiameter + cutoutExtra - height: knobDiameter + cutoutExtra - radius: width / 2 - color: root.cutoutColor !== undefined ? root.cutoutColor : Colors.backgroundPrimary - x: Math.max(0, Math.min(parent.width - width, - root.visualPosition * (parent.width - root.knobDiameter) - cutoutExtra / 2)) - y: (parent.height - height) / 2 + NText { + text: { + var v + if (Number.isInteger(value)) { + v = value + } else { + v = value.toFixed(2) + } + + if (valueSuffix != "") { + return v + valueSuffix + } else { + return v + } + } + font.pointSize: Style.fontSizeMedium * scaling + font.weight: Style.fontWeightBold + color: Colors.textPrimary + Layout.alignment: Qt.AlignBottom | Qt.AlignRight + } } - handle: Item { - width: knob.implicitWidth - height: knob.implicitHeight - x: root.leftPadding + root.visualPosition * (root.availableWidth - width) - y: root.topPadding + root.availableHeight / 2 - height / 2 + Slider { + id: slider - // Subtle shadow for a more polished look (keeps theme colors) - MultiEffect { - anchors.fill: knob - source: knob - shadowEnabled: true - shadowColor: Colors.shadow - shadowOpacity: 0.25 - shadowHorizontalOffset: 0 - shadowVerticalOffset: 1 - shadowBlur: 8 + Layout.fillWidth: true + from: root.from + to: root.to + stepSize: root.stepSize + value: root.value + snapMode: snapAlways ? Slider.SnapAlways : Slider.SnapOnRelease + implicitWidth: root.width + implicitHeight: Math.max(trackHeight, knobDiameter) + onPressedChanged: { + root.pressedChanged(slider.pressed, slider.value) + } + onMoved: { + root.value = slider.value + root.moved(value) } - Rectangle { - id: knob - implicitWidth: knobDiameter - implicitHeight: knobDiameter - radius: width * 0.5 - color: root.pressed ? Colors.surfaceVariant : Colors.surface - border.color: Colors.accentPrimary - border.width: Math.max(1, Style.borderThick * scaling) + background: Rectangle { + x: slider.leftPadding + y: slider.topPadding + slider.availableHeight / 2 - height / 2 + implicitWidth: Style.sliderWidth + implicitHeight: trackHeight + width: slider.availableWidth + height: implicitHeight + radius: height / 2 + color: Colors.surfaceVariant - // Press feedback halo (using accent color, low opacity) Rectangle { - anchors.centerIn: parent - width: parent.width + 8 * scaling - height: parent.height + 8 * scaling - radius: width / 2 + id: activeTrack + width: slider.visualPosition * parent.width + height: parent.height color: Colors.accentPrimary - opacity: root.pressed ? 0.16 : 0.0 + radius: parent.radius + } + + // Circular cutout + Rectangle { + id: knobCutout + width: knobDiameter + cutoutExtra + height: knobDiameter + cutoutExtra + radius: width / 2 + color: slider.cutoutColor !== undefined ? slider.cutoutColor : Colors.backgroundPrimary + x: Math.max(0, Math.min(parent.width - width, + slider.visualPosition * (parent.width - knobDiameter) - cutoutExtra / 2)) + y: (parent.height - height) / 2 + } + } + + handle: Item { + width: knob.implicitWidth + height: knob.implicitHeight + x: slider.leftPadding + slider.visualPosition * (slider.availableWidth - width) + y: slider.topPadding + slider.availableHeight / 2 - height / 2 + + // Subtle shadow for a more polished look (keeps theme colors) + MultiEffect { + anchors.fill: knob + source: knob + shadowEnabled: true + shadowColor: Colors.shadow + shadowOpacity: 0.25 + shadowHorizontalOffset: 0 + shadowVerticalOffset: 1 + shadowBlur: 8 + } + + Rectangle { + id: knob + implicitWidth: knobDiameter + implicitHeight: knobDiameter + radius: width * 0.5 + color: slider.pressed ? Colors.surfaceVariant : Colors.surface + border.color: Colors.accentPrimary + border.width: Math.max(1, Style.borderThick * scaling) + + // Press feedback halo (using accent color, low opacity) + Rectangle { + anchors.centerIn: parent + width: parent.width + 8 * scaling + height: parent.height + 8 * scaling + radius: width / 2 + color: Colors.accentPrimary + opacity: slider.pressed ? 0.16 : 0.0 + } } } } From 25c9217b6bfc2d5b72dc68d1bd579c6aa42c13a0 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 23:20:31 -0400 Subject: [PATCH 211/394] DemoPanel tweaks --- Modules/Demo/DemoPanel.qml | 266 ++++++++++++++++++------------------- Services/Style.qml | 2 +- Widgets/NSlider.qml | 1 - 3 files changed, 134 insertions(+), 135 deletions(-) diff --git a/Modules/Demo/DemoPanel.qml b/Modules/Demo/DemoPanel.qml index 5d59e23..3a9b303 100644 --- a/Modules/Demo/DemoPanel.qml +++ b/Modules/Demo/DemoPanel.qml @@ -36,164 +36,164 @@ NLoader { anchors.fill: parent } - // Debug: Add a simple text to see if content is visible - NText { - text: "DemoPanel is working!" - color: Colors.accentPrimary - font.pointSize: Style.fontSizeLarge * scaling - anchors.top: parent.top - anchors.horizontalCenter: parent.horizontalCenter - anchors.topMargin: 20 * scaling - } - ColumnLayout { anchors.fill: parent - anchors.margins: Style.marginMedium * scaling - anchors.topMargin: (Style.marginMedium + 40) * scaling - spacing: Style.marginMedium * scaling + anchors.margins: Style.marginXL * scaling + + NText { + text: "DemoPanel" + color: Colors.accentPrimary + font.pointSize: Style.fontSizeXL* scaling + font.weight: Style.fontWeightBold + Layout.alignment: Qt.AlignHCenter + } - // NSlider ColumnLayout { - spacing: 16 * scaling - NText { - text: "Scaling" - color: Colors.accentSecondary - font.weight: Style.fontWeightBold - } - RowLayout { - spacing: Style.marginSmall * scaling - NSlider { - label: "Scaling" - description: "Scaling goes brrrr" - valueSuffix: "%" - from: 60 - to: 180 - stepSize: 1 - value: Scaling.overrideScale * 100 - implicitWidth: bgRect.width * 0.75 - onPressedChanged: function (pressed, value) { - Scaling.overrideEnabled = true - Scaling.overrideScale = value / 100 + + spacing: Style.marginMedium * scaling + + // NSlider + ColumnLayout { + spacing: 16 * scaling + NText { + text: "Scaling" + color: Colors.accentSecondary + font.weight: Style.fontWeightBold + } + RowLayout { + spacing: Style.marginSmall * scaling + NSlider { + label: "Scaling" + description: "Scaling goes brrrr" + valueSuffix: "%" + from: 60 + to: 180 + stepSize: 1 + value: Scaling.overrideScale * 100 + implicitWidth: bgRect.width * 0.75 + onPressedChanged: function (pressed, value) { + Scaling.overrideEnabled = true + Scaling.overrideScale = value / 100 + } + } + NIconButton { + icon: "refresh" + fontPointSize: Style.fontSizeLarge * scaling + onClicked: { + Scaling.overrideEnabled = false + Scaling.overrideScale = 1.0 + } } } + NDivider { + Layout.fillWidth: true + } + } + + // NIconButton + ColumnLayout { + spacing: 16 * scaling + NText { + text: "NIconButton" + color: Colors.accentSecondary + font.weight: Style.fontWeightBold + } + NIconButton { - icon: "refresh" - fontPointSize: Style.fontSizeXL * scaling - onClicked: { - Scaling.overrideEnabled = false - Scaling.overrideScale = 1.0 - console.log("Reset!") + id: myIconButton + icon: "celebration" + fontPointSize: Style.fontSizeLarge * scaling + } + + NDivider { + Layout.fillWidth: true + } + } + + // NToggle + ColumnLayout { + spacing: Style.marginMedium * scaling + NText { + text: "NToggle" + color: Colors.accentSecondary + font.weight: Style.fontWeightBold + } + + NToggle { + label: "Label" + description: "Description" + onToggled: function (value) { + console.log("[DemoPanel] NToggle:", value) } } - } - NDivider { - Layout.fillWidth: true - } - } - // NIconButton - ColumnLayout { - spacing: 16 * scaling - NText { - text: "NIconButton" - color: Colors.accentSecondary - font.weight: Style.fontWeightBold - } - - NIconButton { - id: myIconButton - icon: "celebration" - fontPointSize: Style.fontSizeXL * scaling - } - - NDivider { - Layout.fillWidth: true - } - } - - // NToggle - ColumnLayout { - spacing: Style.marginMedium * scaling - NText { - text: "NToggle" - color: Colors.accentSecondary - font.weight: Style.fontWeightBold - } - - NToggle { - label: "Label" - description: "Description" - onToggled: function (value) { - console.log("[DemoPanel] NToggle:", value) + NDivider { + Layout.fillWidth: true } } - NDivider { - Layout.fillWidth: true - } - } + // NComboBox + ColumnLayout { + spacing: Style.marginMedium * scaling + NText { + text: "NComboBox" + color: Colors.accentSecondary + font.weight: Style.fontWeightBold + } - // NComboBox - ColumnLayout { - spacing: Style.marginMedium * scaling - NText { - text: "NComboBox" - color: Colors.accentSecondary - font.weight: Style.fontWeightBold - } + NComboBox { + label: "Animal" + description: "What's your favorite" + optionsKeys: ["cat", "dog", "bird", "monkey", "fish", "turtle", "elephant", "tiger"] + optionsLabels: ["Cat", "Dog", "Bird", "Monkey", "Fish", "Turtle", "Elephant", "Tiger"] + currentKey: "cat" + onSelected: function (value) { + console.log("[DemoPanel] NComboBox: selected ", value) + } + } - NComboBox { - label: "Animal" - description: "What's your favorite" - optionsKeys: ["cat", "dog", "bird", "monkey", "fish", "turtle", "elephant", "tiger"] - optionsLabels: ["Cat", "Dog", "Bird", "Monkey", "Fish", "Turtle", "Elephant", "Tiger"] - currentKey: "cat" - onSelected: function (value) { - console.log("[DemoPanel] NComboBox: selected ", value) + NDivider { + Layout.fillWidth: true } } - NDivider { - Layout.fillWidth: true - } - } + // NTextInput + ColumnLayout { + spacing: Style.marginMedium * scaling + NText { + text: "NTextInput" + color: Colors.accentSecondary + font.weight: Style.fontWeightBold + } - // NTextInput - ColumnLayout { - spacing: Style.marginMedium * scaling - NText { - text: "NTextInput" - color: Colors.accentSecondary - font.weight: Style.fontWeightBold - } - - NTextInput { - label: "Input label" - description: "A cool description" - text: "Type anything" - Layout.fillWidth: true - onEditingFinished: { + NTextInput { + label: "Input label" + description: "A cool description" + text: "Type anything" + Layout.fillWidth: true + onEditingFinished: { + } + } + NDivider { + Layout.fillWidth: true } } - NDivider { - Layout.fillWidth: true - } - } - // NBusyIndicator - ColumnLayout { - spacing: Style.marginMedium * scaling - NText { - text: "NBusyIndicator" - color: Colors.accentSecondary - font.weight: Style.fontWeightBold - } + // NBusyIndicator + ColumnLayout { + spacing: Style.marginMedium * scaling + NText { + text: "NBusyIndicator" + color: Colors.accentSecondary + font.weight: Style.fontWeightBold + } - NBusyIndicator {} + NBusyIndicator {} - NDivider { - Layout.fillWidth: true + NDivider { + Layout.fillWidth: true + } } } } diff --git a/Services/Style.qml b/Services/Style.qml index 7a3f6fc..03ee2d1 100644 --- a/Services/Style.qml +++ b/Services/Style.qml @@ -58,7 +58,7 @@ Singleton { property int marginSmall: 8 property int marginMedium: 12 property int marginLarge: 16 - property int marginExtraLarge: 20 + property int marginXL: 24 // Opacity property real opacityNone: 0.0 diff --git a/Widgets/NSlider.qml b/Widgets/NSlider.qml index 4911099..8075473 100644 --- a/Widgets/NSlider.qml +++ b/Widgets/NSlider.qml @@ -72,7 +72,6 @@ ColumnLayout { font.weight: Style.fontWeightBold color: Colors.textPrimary Layout.alignment: Qt.AlignBottom | Qt.AlignRight - } } From 050877bcb0a8cb1b7f3cd8bc6ed98fa35a6fec68 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 12 Aug 2025 23:46:35 -0400 Subject: [PATCH 212/394] Reverted the labeled NSlider, which tends to trigger hard crashes when opening the settings --- Modules/Demo/DemoPanel.qml | 31 +++-- Modules/Settings/SettingsPanel.qml | 8 +- Modules/Settings/Tabs/Wallpaper.qml | 84 ++++++------ Widgets/NSlider.qml | 192 +++++++++------------------- 4 files changed, 123 insertions(+), 192 deletions(-) diff --git a/Modules/Demo/DemoPanel.qml b/Modules/Demo/DemoPanel.qml index 3a9b303..29139e3 100644 --- a/Modules/Demo/DemoPanel.qml +++ b/Modules/Demo/DemoPanel.qml @@ -43,7 +43,7 @@ NLoader { NText { text: "DemoPanel" color: Colors.accentPrimary - font.pointSize: Style.fontSizeXL* scaling + font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold Layout.alignment: Qt.AlignHCenter } @@ -60,22 +60,27 @@ NLoader { color: Colors.accentSecondary font.weight: Style.fontWeightBold } + NText { + text: `${Math.round(Scaling.overrideScale * 100)}%` + Layout.alignment: Qt.AlignVCenter + } RowLayout { spacing: Style.marginSmall * scaling NSlider { - label: "Scaling" - description: "Scaling goes brrrr" - valueSuffix: "%" - from: 60 - to: 180 - stepSize: 1 - value: Scaling.overrideScale * 100 - implicitWidth: bgRect.width * 0.75 - onPressedChanged: function (pressed, value) { - Scaling.overrideEnabled = true - Scaling.overrideScale = value / 100 - } + id: scaleSlider + from: 0.6 + to: 1.8 + stepSize: 0.01 + value: Scaling.overrideScale + implicitWidth: bgRect.width * 0.75 + onMoved: { + } + onPressedChanged: { + Scaling.overrideScale = value + Scaling.overrideEnabled = true + } + } NIconButton { icon: "refresh" fontPointSize: Style.fontSizeLarge * scaling diff --git a/Modules/Settings/SettingsPanel.qml b/Modules/Settings/SettingsPanel.qml index 856c2b6..2d965dc 100644 --- a/Modules/Settings/SettingsPanel.qml +++ b/Modules/Settings/SettingsPanel.qml @@ -10,13 +10,15 @@ import qs.Widgets NLoader { id: root + readonly property real scaling: Scaling.scale(screen) + content: Component { NPanel { id: panel WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand - readonly property real scaling: Scaling.scale(screen) + property int currentTabIndex: 0 property var tabsModel: [{ "label": "General", @@ -74,8 +76,8 @@ NLoader { border.color: Colors.backgroundTertiary border.width: Math.max(1, Style.borderMedium * scaling) layer.enabled: true - width: (screen.width / 2) * scaling - height: (screen.height / 2) * scaling + width: (screen.width * 0.5) * scaling + height: (screen.height * 0.5) * scaling anchors.centerIn: parent MouseArea { diff --git a/Modules/Settings/Tabs/Wallpaper.qml b/Modules/Settings/Tabs/Wallpaper.qml index 59eb6bf..32d0f5c 100644 --- a/Modules/Settings/Tabs/Wallpaper.qml +++ b/Modules/Settings/Tabs/Wallpaper.qml @@ -116,20 +116,20 @@ ColumnLayout { } // Wallpaper Interval - NSlider { - label: "Wallpaper Interval" - description: "How often to change wallpapers automatically (in seconds)" - valueSuffix: "s" - from: 10 - to: 900 - stepSize: 10 - value: Settings.data.wallpaper.randomInterval - onPressedChanged: function (pressed, value) { - Settings.data.wallpaper.randomInterval = Math.round(value) - } - cutoutColor: Colors.backgroundPrimary - Layout.fillWidth: true - } + // NSlider { + // label: "Wallpaper Interval" + // description: "How often to change wallpapers automatically (in seconds)" + // valueSuffix: "s" + // from: 10 + // to: 900 + // stepSize: 10 + // value: Settings.data.wallpaper.randomInterval + // onPressedChanged: function (pressed, value) { + // Settings.data.wallpaper.randomInterval = Math.round(value) + // } + // cutoutColor: Colors.backgroundPrimary + // Layout.fillWidth: true + // } } NDivider { @@ -229,36 +229,36 @@ ColumnLayout { } // Transition FPS - NSlider { - label: "Transition FPS" - description: "Frames per second for transition animations" - valueSuffix: " FPS" - from: 30 - to: 500 - stepSize: 5 - value: Settings.data.wallpaper.swww.transitionFps - onPressedChanged: function (pressed, value) { - Settings.data.wallpaper.swww.transitionFps = Math.round(value) - } - cutoutColor: Colors.backgroundPrimary - Layout.fillWidth: true - } + // NSlider { + // label: "Transition FPS" + // description: "Frames per second for transition animations" + // valueSuffix: " FPS" + // from: 30 + // to: 500 + // stepSize: 5 + // value: Settings.data.wallpaper.swww.transitionFps + // onPressedChanged: function (pressed, value) { + // Settings.data.wallpaper.swww.transitionFps = Math.round(value) + // } + // cutoutColor: Colors.backgroundPrimary + // Layout.fillWidth: true + // } // Transition Duration - NSlider { - label: "Transition Duration" - description: "Duration of transition animations in seconds" - valueSuffix: "s" - from: 0.25 - to: 10 - stepSize: 0.05 - value: Settings.data.wallpaper.swww.transitionDuration - onPressedChanged: function (pressed, value) { - Settings.data.wallpaper.swww.transitionDuration = value - } - cutoutColor: Colors.backgroundPrimary - Layout.fillWidth: true - } + // NSlider { + // label: "Transition Duration" + // description: "Duration of transition animations in seconds" + // valueSuffix: "s" + // from: 0.25 + // to: 10 + // stepSize: 0.05 + // value: Settings.data.wallpaper.swww.transitionDuration + // onPressedChanged: function (pressed, value) { + // Settings.data.wallpaper.swww.transitionDuration = value + // } + // cutoutColor: Colors.backgroundPrimary + // Layout.fillWidth: true + // } } } } diff --git a/Widgets/NSlider.qml b/Widgets/NSlider.qml index 8075473..2d77f3e 100644 --- a/Widgets/NSlider.qml +++ b/Widgets/NSlider.qml @@ -1,10 +1,9 @@ import QtQuick import QtQuick.Controls import QtQuick.Effects -import QtQuick.Layouts import qs.Services -ColumnLayout { +Slider { id: root readonly property real scaling: Scaling.scale(screen) @@ -12,156 +11,81 @@ ColumnLayout { readonly property real trackHeight: knobDiameter * 0.5 readonly property real cutoutExtra: Style.baseWidgetSize * 0.1 * scaling - property string label: "" - property string description: "" - property string valueSuffix: "" - property real from: 0.0 - property real to: 1.0 - property real stepSize: 0.01 - property real value: 0.0 - // Optional color to cut the track beneath the knob (should match surrounding background) property var cutoutColor property var screen property bool snapAlways: true - signal pressedChanged(bool pressed, real value) - signal moved(real value) + snapMode: snapAlways ? Slider.SnapAlways : Slider.SnapOnRelease + implicitHeight: Math.max(trackHeight, knobDiameter) - Layout.fillWidth: true - spacing: Style.marginSmall * scaling + background: Rectangle { + x: root.leftPadding + y: root.topPadding + root.availableHeight / 2 - height / 2 + implicitWidth: Style.sliderWidth + implicitHeight: trackHeight + width: root.availableWidth + height: implicitHeight + radius: height / 2 + color: Colors.surfaceVariant - RowLayout { - Layout.fillWidth: true - - ColumnLayout { - spacing: Style.marginTiniest * scaling - - NText { - text: label - font.pointSize: Style.fontSizeMedium * scaling - font.weight: Style.fontWeightBold - color: Colors.textPrimary - Layout.fillWidth: true - } - - NText { - text: description - font.pointSize: Style.fontSizeSmall * scaling - color: Colors.textSecondary - wrapMode: Text.WordWrap - } + Rectangle { + id: activeTrack + width: root.visualPosition * parent.width + height: parent.height + color: Colors.accentPrimary + radius: parent.radius } - NText { - text: { - var v - if (Number.isInteger(value)) { - v = value - } else { - v = value.toFixed(2) - } - - if (valueSuffix != "") { - return v + valueSuffix - } else { - return v - } - } - font.pointSize: Style.fontSizeMedium * scaling - font.weight: Style.fontWeightBold - color: Colors.textPrimary - Layout.alignment: Qt.AlignBottom | Qt.AlignRight + // Circular cutout + Rectangle { + id: knobCutout + width: knobDiameter + cutoutExtra + height: knobDiameter + cutoutExtra + radius: width / 2 + color: root.cutoutColor !== undefined ? root.cutoutColor : Colors.backgroundPrimary + x: Math.max(0, Math.min(parent.width - width, + root.visualPosition * (parent.width - root.knobDiameter) - cutoutExtra / 2)) + y: (parent.height - height) / 2 } } - Slider { - id: slider + handle: Item { + width: knob.implicitWidth + height: knob.implicitHeight + x: root.leftPadding + root.visualPosition * (root.availableWidth - width) + y: root.topPadding + root.availableHeight / 2 - height / 2 - Layout.fillWidth: true - from: root.from - to: root.to - stepSize: root.stepSize - value: root.value - snapMode: snapAlways ? Slider.SnapAlways : Slider.SnapOnRelease - implicitWidth: root.width - implicitHeight: Math.max(trackHeight, knobDiameter) - onPressedChanged: { - root.pressedChanged(slider.pressed, slider.value) - } - onMoved: { - root.value = slider.value - root.moved(value) + // Subtle shadow for a more polished look (keeps theme colors) + MultiEffect { + anchors.fill: knob + source: knob + shadowEnabled: true + shadowColor: Colors.shadow + shadowOpacity: 0.25 + shadowHorizontalOffset: 0 + shadowVerticalOffset: 1 + shadowBlur: 8 } - background: Rectangle { - x: slider.leftPadding - y: slider.topPadding + slider.availableHeight / 2 - height / 2 - implicitWidth: Style.sliderWidth - implicitHeight: trackHeight - width: slider.availableWidth - height: implicitHeight - radius: height / 2 - color: Colors.surfaceVariant + Rectangle { + id: knob + implicitWidth: knobDiameter + implicitHeight: knobDiameter + radius: width * 0.5 + color: root.pressed ? Colors.surfaceVariant : Colors.surface + border.color: Colors.accentPrimary + border.width: Math.max(1, Style.borderThick * scaling) + // Press feedback halo (using accent color, low opacity) Rectangle { - id: activeTrack - width: slider.visualPosition * parent.width - height: parent.height - color: Colors.accentPrimary - radius: parent.radius - } - - // Circular cutout - Rectangle { - id: knobCutout - width: knobDiameter + cutoutExtra - height: knobDiameter + cutoutExtra + anchors.centerIn: parent + width: parent.width + 8 * scaling + height: parent.height + 8 * scaling radius: width / 2 - color: slider.cutoutColor !== undefined ? slider.cutoutColor : Colors.backgroundPrimary - x: Math.max(0, Math.min(parent.width - width, - slider.visualPosition * (parent.width - knobDiameter) - cutoutExtra / 2)) - y: (parent.height - height) / 2 - } - } - - handle: Item { - width: knob.implicitWidth - height: knob.implicitHeight - x: slider.leftPadding + slider.visualPosition * (slider.availableWidth - width) - y: slider.topPadding + slider.availableHeight / 2 - height / 2 - - // Subtle shadow for a more polished look (keeps theme colors) - MultiEffect { - anchors.fill: knob - source: knob - shadowEnabled: true - shadowColor: Colors.shadow - shadowOpacity: 0.25 - shadowHorizontalOffset: 0 - shadowVerticalOffset: 1 - shadowBlur: 8 - } - - Rectangle { - id: knob - implicitWidth: knobDiameter - implicitHeight: knobDiameter - radius: width * 0.5 - color: slider.pressed ? Colors.surfaceVariant : Colors.surface - border.color: Colors.accentPrimary - border.width: Math.max(1, Style.borderThick * scaling) - - // Press feedback halo (using accent color, low opacity) - Rectangle { - anchors.centerIn: parent - width: parent.width + 8 * scaling - height: parent.height + 8 * scaling - radius: width / 2 - color: Colors.accentPrimary - opacity: slider.pressed ? 0.16 : 0.0 - } + color: Colors.accentPrimary + opacity: root.pressed ? 0.16 : 0.0 } } } -} +} \ No newline at end of file From 27e77455492182cf1d53ac01b721644cc5c64d43 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 13 Aug 2025 00:13:19 -0400 Subject: [PATCH 213/394] Settings: Wallpaper Tabs cleanup --- Modules/Demo/DemoPanel.qml | 26 +-- Modules/Settings/SettingsPanel.qml | 3 +- Modules/Settings/Tabs/Wallpaper.qml | 265 +++++++++++++++------------- Widgets/NSlider.qml | 2 +- 4 files changed, 158 insertions(+), 138 deletions(-) diff --git a/Modules/Demo/DemoPanel.qml b/Modules/Demo/DemoPanel.qml index 29139e3..98ba412 100644 --- a/Modules/Demo/DemoPanel.qml +++ b/Modules/Demo/DemoPanel.qml @@ -67,20 +67,20 @@ NLoader { RowLayout { spacing: Style.marginSmall * scaling NSlider { - id: scaleSlider - from: 0.6 - to: 1.8 - stepSize: 0.01 - value: Scaling.overrideScale - implicitWidth: bgRect.width * 0.75 - onMoved: { - + id: scaleSlider + from: 0.6 + to: 1.8 + stepSize: 0.01 + value: Scaling.overrideScale + implicitWidth: bgRect.width * 0.75 + onMoved: { + + } + onPressedChanged: { + Scaling.overrideScale = value + Scaling.overrideEnabled = true + } } - onPressedChanged: { - Scaling.overrideScale = value - Scaling.overrideEnabled = true - } - } NIconButton { icon: "refresh" fontPointSize: Style.fontSizeLarge * scaling diff --git a/Modules/Settings/SettingsPanel.qml b/Modules/Settings/SettingsPanel.qml index 2d965dc..3fe205d 100644 --- a/Modules/Settings/SettingsPanel.qml +++ b/Modules/Settings/SettingsPanel.qml @@ -10,7 +10,7 @@ import qs.Widgets NLoader { id: root - readonly property real scaling: Scaling.scale(screen) + readonly property real scaling: Scaling.scale(screen) content: Component { NPanel { @@ -18,7 +18,6 @@ NLoader { WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand - property int currentTabIndex: 0 property var tabsModel: [{ "label": "General", diff --git a/Modules/Settings/Tabs/Wallpaper.qml b/Modules/Settings/Tabs/Wallpaper.qml index 32d0f5c..7b57eb7 100644 --- a/Modules/Settings/Tabs/Wallpaper.qml +++ b/Modules/Settings/Tabs/Wallpaper.qml @@ -29,44 +29,31 @@ ColumnLayout { } ColumnLayout { - spacing: 4 + spacing: Style.marginTiny * scaling Layout.fillWidth: true NText { - text: "Wallpaper Settings" - font.pointSize: 18 + text: "Directory" + font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold color: Colors.textPrimary - Layout.bottomMargin: 8 + Layout.bottomMargin: Style.marginSmall * scaling } // Wallpaper Settings Category ColumnLayout { spacing: 8 Layout.fillWidth: true - Layout.topMargin: 8 + Layout.topMargin: Style.marginSmall * scaling // Wallpaper Folder ColumnLayout { - spacing: 8 + spacing: Style.marginSmall * scaling Layout.fillWidth: true - NText { - text: "Wallpaper Folder" - font.pointSize: 13 - font.weight: Style.fontWeightBold - color: Colors.textPrimary - } - - NText { - text: "Path to your wallpaper folder" - font.pointSize: 12 - color: Colors.textSecondary - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - NTextInput { + label: "Wallpaper Directory" + description: "Path to your wallpaper directory" text: Settings.data.wallpaper.directory Layout.fillWidth: true onEditingFinished: { @@ -79,12 +66,12 @@ ColumnLayout { NDivider { Layout.fillWidth: true - Layout.topMargin: 26 - Layout.bottomMargin: 18 + Layout.topMargin: Style.marginLarge * 2 * scaling + Layout.bottomMargin: Style.marginLarge * scaling } ColumnLayout { - spacing: 4 + spacing: Style.marginLarge * scaling Layout.fillWidth: true NText { @@ -115,29 +102,54 @@ ColumnLayout { } } - // Wallpaper Interval - // NSlider { - // label: "Wallpaper Interval" - // description: "How often to change wallpapers automatically (in seconds)" - // valueSuffix: "s" - // from: 10 - // to: 900 - // stepSize: 10 - // value: Settings.data.wallpaper.randomInterval - // onPressedChanged: function (pressed, value) { - // Settings.data.wallpaper.randomInterval = Math.round(value) - // } - // cutoutColor: Colors.backgroundPrimary - // Layout.fillWidth: true - // } + // Interval + ColumnLayout { + RowLayout { + Layout.fillWidth: true + + ColumnLayout { + NText { + text: "Wallpaper Interval" + font.weight: Style.fontWeightBold + color: Colors.textPrimary + } + + NText { + text: "How often to change wallpapers automatically (in seconds)" + font.pointSize: Style.fontSizeSmall * scaling + color: Colors.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + + NText { + text: sliderWpInterval.value + " seconds" + Layout.alignment: Qt.AlignBottom | Qt.AlignRight + } + } + + NSlider { + id: sliderWpInterval + Layout.fillWidth: true + from: 10 + to: 900 + stepSize: 10 + value: Settings.data.wallpaper.randomInterval + onPressedChanged: Settings.data.wallpaper.randomInterval = Math.round(value) + cutoutColor: Colors.backgroundPrimary + } + } } NDivider { Layout.fillWidth: true - Layout.topMargin: 26 - Layout.bottomMargin: 18 + Layout.topMargin: Style.marginLarge * 2 * scaling + Layout.bottomMargin: Style.marginLarge * scaling } + // ------------------------------- + // SWWW ColumnLayout { spacing: 4 Layout.fillWidth: true @@ -168,97 +180,106 @@ ColumnLayout { visible: Settings.data.wallpaper.swww.enabled // Resize Mode - ColumnLayout { - spacing: 8 - Layout.fillWidth: true - - NText { - text: "Resize Mode" - font.pointSize: 13 - font.weight: Style.fontWeightBold - color: Colors.textPrimary - } - - NText { - text: "How SWWW should resize wallpapers to fit the screen" - font.pointSize: 12 - color: Colors.textSecondary - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - - NComboBox { - optionsKeys: ["no", "crop", "fit", "stretch"] - optionsLabels: ["No", "Crop", "Fit", "Stretch"] - currentKey: Settings.data.wallpaper.swww.resizeMethod - onSelected: function (key) { - Settings.data.wallpaper.swww.resizeMethod = key - } + NComboBox { + label: "Resize Mode" + description: "How SWWW should resize wallpapers to fit the screen" + optionsKeys: ["no", "crop", "fit", "stretch"] + optionsLabels: ["No", "Crop", "Fit", "Stretch"] + currentKey: Settings.data.wallpaper.swww.resizeMethod + onSelected: function (key) { + Settings.data.wallpaper.swww.resizeMethod = key } } // Transition Type - ColumnLayout { - spacing: 8 - Layout.fillWidth: true - Layout.topMargin: 8 - - NText { - text: "Transition Type" - font.pointSize: 13 - font.weight: Style.fontWeightBold - color: Colors.textPrimary - } - - NText { - text: "Animation type when switching between wallpapers" - font.pointSize: 12 - color: Colors.textSecondary - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - - NComboBox { - optionsKeys: ["none", "simple", "fade", "left", "right", "top", "bottom", "wipe", "wave", "grow", "center", "any", "outer", "random"] - optionsLabels: ["None", "Simple", "Fade", "Left", "Right", "Top", "Bottom", "Wipe", "Wave", "Grow", "Center", "Any", "Outer", "Random"] - currentKey: Settings.data.wallpaper.swww.transitionType - onSelected: function (key) { - Settings.data.wallpaper.swww.transitionType = key - } + NComboBox { + label: "Transition Type" + description: "Animation type when switching between wallpapers" + optionsKeys: ["none", "simple", "fade", "left", "right", "top", "bottom", "wipe", "wave", "grow", "center", "any", "outer", "random"] + optionsLabels: ["None", "Simple", "Fade", "Left", "Right", "Top", "Bottom", "Wipe", "Wave", "Grow", "Center", "Any", "Outer", "Random"] + currentKey: Settings.data.wallpaper.swww.transitionType + onSelected: function (key) { + Settings.data.wallpaper.swww.transitionType = key } } // Transition FPS - // NSlider { - // label: "Transition FPS" - // description: "Frames per second for transition animations" - // valueSuffix: " FPS" - // from: 30 - // to: 500 - // stepSize: 5 - // value: Settings.data.wallpaper.swww.transitionFps - // onPressedChanged: function (pressed, value) { - // Settings.data.wallpaper.swww.transitionFps = Math.round(value) - // } - // cutoutColor: Colors.backgroundPrimary - // Layout.fillWidth: true - // } + ColumnLayout { + RowLayout { + Layout.fillWidth: true + + ColumnLayout { + NText { + text: "Transition FPS" + font.weight: Style.fontWeightBold + color: Colors.textPrimary + } + + NText { + text: "Frames per second for transition animations" + font.pointSize: Style.fontSizeSmall * scaling + color: Colors.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + + NText { + text: sliderWpTransitionFps.value + " FPS" + Layout.alignment: Qt.AlignBottom | Qt.AlignRight + } + } + + NSlider { + id: sliderWpTransitionFps + Layout.fillWidth: true + from: 30 + to: 500 + stepSize: 5 + value: Settings.data.wallpaper.swww.transitionFps + onPressedChanged: Settings.data.wallpaper.swww.transitionFps = Math.round(value) + cutoutColor: Colors.backgroundPrimary + } + } // Transition Duration - // NSlider { - // label: "Transition Duration" - // description: "Duration of transition animations in seconds" - // valueSuffix: "s" - // from: 0.25 - // to: 10 - // stepSize: 0.05 - // value: Settings.data.wallpaper.swww.transitionDuration - // onPressedChanged: function (pressed, value) { - // Settings.data.wallpaper.swww.transitionDuration = value - // } - // cutoutColor: Colors.backgroundPrimary - // Layout.fillWidth: true - // } + ColumnLayout { + RowLayout { + Layout.fillWidth: true + + ColumnLayout { + NText { + text: "Transition Duration" + font.weight: Style.fontWeightBold + color: Colors.textPrimary + } + + NText { + text: "Duration of transition animations in seconds" + font.pointSize: Style.fontSizeSmall * scaling + color: Colors.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + + NText { + text: sliderWpTransitionDuration.value.toFixed(2) + "s" + Layout.alignment: Qt.AlignBottom | Qt.AlignRight + } + } + + NSlider { + id: sliderWpTransitionDuration + Layout.fillWidth: true + from: 0.25 + to: 10 + stepSize: 0.05 + value: Settings.data.wallpaper.swww.transitionDuration + onPressedChanged: Settings.data.wallpaper.swww.transitionDuration = value + cutoutColor: Colors.backgroundPrimary + } + } } } } diff --git a/Widgets/NSlider.qml b/Widgets/NSlider.qml index 2d77f3e..2e9e6d1 100644 --- a/Widgets/NSlider.qml +++ b/Widgets/NSlider.qml @@ -88,4 +88,4 @@ Slider { } } } -} \ No newline at end of file +} From dc962a294a05b7a631779c90be5fd2f6da0a1d59 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 13 Aug 2025 00:24:56 -0400 Subject: [PATCH 214/394] More settings --- Modules/Settings/SettingsPanel.qml | 4 +- Modules/Settings/Tabs/Misc.qml | 36 ++++-------- Modules/Settings/Tabs/Wallpaper.qml | 16 +++--- Modules/Settings/Tabs/WallpaperSelector.qml | 61 ++++++++++++--------- 4 files changed, 56 insertions(+), 61 deletions(-) diff --git a/Modules/Settings/SettingsPanel.qml b/Modules/Settings/SettingsPanel.qml index 3fe205d..793ae46 100644 --- a/Modules/Settings/SettingsPanel.qml +++ b/Modules/Settings/SettingsPanel.qml @@ -10,11 +10,11 @@ import qs.Widgets NLoader { id: root - readonly property real scaling: Scaling.scale(screen) - content: Component { NPanel { id: panel + + readonly property real scaling: Scaling.scale(screen) WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand diff --git a/Modules/Settings/Tabs/Misc.qml b/Modules/Settings/Tabs/Misc.qml index f99cdfa..6b98781 100644 --- a/Modules/Settings/Tabs/Misc.qml +++ b/Modules/Settings/Tabs/Misc.qml @@ -14,8 +14,7 @@ ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true - padding: 16 - rightPadding: 12 + padding: Style.marginMedium * scaling clip: true ScrollBar.horizontal.policy: ScrollBar.AlwaysOff ScrollBar.vertical.policy: ScrollBar.AsNeeded @@ -30,37 +29,26 @@ ColumnLayout { } ColumnLayout { - spacing: 4 + spacing: Style.marginLarge * scaling Layout.fillWidth: true NText { text: "Miscellaneous Settings" - font.pointSize: 18 + font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold color: Colors.textPrimary - Layout.bottomMargin: 8 + Layout.bottomMargin: Style.marginSmall * scaling } // Audio Visualizer section - ColumnLayout { - spacing: 8 - Layout.fillWidth: true - Layout.topMargin: 8 - - NText { - text: "Audio Visualizer" - font.pointSize: 13 - font.weight: Style.fontWeightBold - color: Colors.textPrimary - } - - NComboBox { - optionsKeys: ["radial", "bars", "wave"] - optionsLabels: ["Radial", "Bars", "Wave"] - currentKey: Settings.data.audioVisualizer.type - onSelected: function (key) { - Settings.data.audioVisualizer.type = key - } + NComboBox { + label: "Audio Visualizer" + description: "Choose a visualization type" + optionsKeys: ["radial", "bars", "wave"] + optionsLabels: ["Radial", "Bars", "Wave"] + currentKey: Settings.data.audioVisualizer.type + onSelected: function (key) { + Settings.data.audioVisualizer.type = key } } } diff --git a/Modules/Settings/Tabs/Wallpaper.qml b/Modules/Settings/Tabs/Wallpaper.qml index 7b57eb7..f5f92d2 100644 --- a/Modules/Settings/Tabs/Wallpaper.qml +++ b/Modules/Settings/Tabs/Wallpaper.qml @@ -29,7 +29,7 @@ ColumnLayout { } ColumnLayout { - spacing: Style.marginTiny * scaling + spacing: Style.marginLarge * scaling Layout.fillWidth: true NText { @@ -42,7 +42,7 @@ ColumnLayout { // Wallpaper Settings Category ColumnLayout { - spacing: 8 + spacing: Style.marginSmall * scaling Layout.fillWidth: true Layout.topMargin: Style.marginSmall * scaling @@ -76,10 +76,10 @@ ColumnLayout { NText { text: "Automation" - font.pointSize: 18 + font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold color: Colors.textPrimary - Layout.bottomMargin: 8 + Layout.bottomMargin: Style.marginSmall * scaling } // Random Wallpaper @@ -151,12 +151,12 @@ ColumnLayout { // ------------------------------- // SWWW ColumnLayout { - spacing: 4 + spacing: Style.marginLarge * scaling Layout.fillWidth: true NText { text: "SWWW" - font.pointSize: 18 + font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold color: Colors.textPrimary Layout.bottomMargin: 8 @@ -174,9 +174,9 @@ ColumnLayout { // SWWW Settings (only visible when useSWWW is enabled) ColumnLayout { - spacing: 8 + spacing: Style.marginSmall * scaling Layout.fillWidth: true - Layout.topMargin: 8 + Layout.topMargin: Style.marginSmall * scaling visible: Settings.data.wallpaper.swww.enabled // Resize Mode diff --git a/Modules/Settings/Tabs/WallpaperSelector.qml b/Modules/Settings/Tabs/WallpaperSelector.qml index 2f743d0..7a6204e 100644 --- a/Modules/Settings/Tabs/WallpaperSelector.qml +++ b/Modules/Settings/Tabs/WallpaperSelector.qml @@ -69,31 +69,34 @@ Item { Layout.fillWidth: true } - // Wallpaper grid - NText { - text: "Available Wallpapers" - font.weight: Style.fontWeightBold - color: Colors.textPrimary - } - - NText { - text: "Click on a wallpaper to set it as your current wallpaper" - color: Colors.textSecondary - wrapMode: Text.WordWrap - } - - NText { - text: Settings.data.wallpaper.swww.enabled ? "Wallpapers will change with " + Settings.data.wallpaper.swww.transitionType - + " transition" : "Wallpapers will change instantly" - color: Colors.textSecondary - font.pointSize: Style.fontSizeSmall * scaling - visible: Settings.data.wallpaper.swww.enabled - } - - // Refresh button and status RowLayout { Layout.fillWidth: true - spacing: Style.marginSmall * scaling + + ColumnLayout { + Layout.fillWidth: true + + // Wallpaper grid + NText { + text: "Available Wallpapers" + font.weight: Style.fontWeightBold + color: Colors.textPrimary + } + + NText { + text: "Click on a wallpaper to set it as your current wallpaper" + color: Colors.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + NText { + text: Settings.data.wallpaper.swww.enabled ? "Wallpapers will change with " + Settings.data.wallpaper.swww.transitionType + + " transition" : "Wallpapers will change instantly" + color: Colors.textSecondary + font.pointSize: Style.fontSizeSmall * scaling + visible: Settings.data.wallpaper.swww.enabled + } + } NIconButton { icon: "refresh" @@ -101,12 +104,16 @@ Item { onClicked: { Wallpapers.loadWallpapers() } + Layout.alignment: Qt.AlignTop | Qt.AlignRight } + } + + // Refresh button and status + RowLayout { + Layout.fillWidth: true + spacing: Style.marginSmall * scaling + - NText { - text: "Refresh" - color: Colors.textSecondary - } } // Wallpaper grid container From a4ce7eb9616baada5624e016fe71513ca21705d2 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 13 Aug 2025 00:36:46 -0400 Subject: [PATCH 215/394] WallPaper selector polishing --- Modules/Settings/SettingsPanel.qml | 9 +- Modules/Settings/Tabs/WallpaperSelector.qml | 397 ++++++++++---------- 2 files changed, 198 insertions(+), 208 deletions(-) diff --git a/Modules/Settings/SettingsPanel.qml b/Modules/Settings/SettingsPanel.qml index 793ae46..b001c39 100644 --- a/Modules/Settings/SettingsPanel.qml +++ b/Modules/Settings/SettingsPanel.qml @@ -13,7 +13,7 @@ NLoader { content: Component { NPanel { id: panel - + readonly property real scaling: Scaling.scale(screen) WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand @@ -134,10 +134,11 @@ NLoader { font.pointSize: Style.fontSizeLarge * scaling color: selected ? Colors.onAccent : (tabItem.hovering ? Colors.onAccent : Colors.textSecondary) } + // Tab label on the left side NText { text: modelData.label color: selected ? Colors.onAccent : (tabItem.hovering ? Colors.onAccent : Colors.textPrimary) - font.pointSize: Style.fontSizeInter * scaling + font.pointSize: Style.fontSizeLarge * scaling font.weight: Style.fontWeightBold Layout.fillWidth: true } @@ -177,11 +178,13 @@ NLoader { id: headerRow Layout.fillWidth: true spacing: Style.marginSmall * scaling + + // Tab label on the main right NText { text: panel.tabsModel[panel.currentTabIndex].label font.pointSize: Style.fontSizeLarge * scaling font.weight: Style.fontWeightBold - color: Colors.textPrimary + color: Colors.accentPrimary Layout.fillWidth: true } NIconButton { diff --git a/Modules/Settings/Tabs/WallpaperSelector.qml b/Modules/Settings/Tabs/WallpaperSelector.qml index 7a6204e..e96fede 100644 --- a/Modules/Settings/Tabs/WallpaperSelector.qml +++ b/Modules/Settings/Tabs/WallpaperSelector.qml @@ -22,235 +22,222 @@ Item { ColumnLayout { width: parent.width - spacing: Style.marginMedium * scaling - Layout.fillWidth: true - - NText { - text: "Wallpaper Selector" - font.weight: Style.fontWeightBold - color: Colors.accentSecondary - } - - NText { - text: "Select a wallpaper from your configured directory" - color: Colors.textSecondary - wrapMode: Text.WordWrap - } - - // Current wallpaper display - NText { - text: "Current Wallpaper" - font.weight: Style.fontWeightBold - color: Colors.textPrimary - } - - Rectangle { + ColumnLayout { + spacing: Style.marginLarge * scaling + Layout.margins: Style.marginLarge * scaling Layout.fillWidth: true - Layout.preferredHeight: 120 * scaling - radius: Style.radiusMedium * scaling - color: Colors.backgroundSecondary - border.color: Colors.outline - border.width: Math.max(1, Style.borderThin * scaling) - clip: true - NImageRounded { - id: currentWallpaperImage - anchors.fill: parent - anchors.margins: Style.marginSmall * scaling - imagePath: Wallpapers.currentWallpaper - fallbackIcon: "image" - borderColor: Colors.outline - borderWidth: Math.max(1, Style.borderThin * scaling) - imageRadius: Style.radiusMedium * scaling + // Current wallpaper display + NText { + text: "Current Wallpaper" + font.weight: Style.fontWeightBold + color: Colors.textPrimary + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 120 * scaling + radius: Style.radiusMedium * scaling + color: Colors.backgroundSecondary + border.color: Colors.outline + border.width: Math.max(1, Style.borderThin * scaling) + clip: true + + NImageRounded { + id: currentWallpaperImage + anchors.fill: parent + anchors.margins: Style.marginSmall * scaling + imagePath: Wallpapers.currentWallpaper + fallbackIcon: "image" + borderColor: Colors.outline + borderWidth: Math.max(1, Style.borderThin * scaling) + imageRadius: Style.radiusMedium * scaling + } } - } NDivider { Layout.fillWidth: true + Layout.topMargin: Style.marginLarge * scaling + Layout.bottomMargin: Style.marginLarge * scaling } - RowLayout { - Layout.fillWidth: true - ColumnLayout { + RowLayout { Layout.fillWidth: true - // Wallpaper grid - NText { - text: "Available Wallpapers" - font.weight: Style.fontWeightBold - color: Colors.textPrimary - } - - NText { - text: "Click on a wallpaper to set it as your current wallpaper" - color: Colors.textSecondary - wrapMode: Text.WordWrap + ColumnLayout { Layout.fillWidth: true - } - NText { - text: Settings.data.wallpaper.swww.enabled ? "Wallpapers will change with " + Settings.data.wallpaper.swww.transitionType - + " transition" : "Wallpapers will change instantly" - color: Colors.textSecondary - font.pointSize: Style.fontSizeSmall * scaling - visible: Settings.data.wallpaper.swww.enabled - } - } - - NIconButton { - icon: "refresh" - tooltipText: "Refresh wallpaper list" - onClicked: { - Wallpapers.loadWallpapers() - } - Layout.alignment: Qt.AlignTop | Qt.AlignRight - } - } - - // Refresh button and status - RowLayout { - Layout.fillWidth: true - spacing: Style.marginSmall * scaling - - - } - - // Wallpaper grid container - Item { - Layout.fillWidth: true - Layout.preferredHeight: 400 * scaling - - FolderListModel { - id: folderModel - folder: "file://" + (Settings.data.wallpaper.directory !== undefined ? Settings.data.wallpaper.directory : "") - nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.pnm", "*.bmp"] - showDirs: false - sortField: FolderListModel.Name - } - - GridView { - id: wallpaperGridView - anchors.fill: parent - clip: true - model: folderModel - - // Fixed 5 items per row - more aggressive sizing - property int itemSize: Math.floor((width - leftMargin - rightMargin - (4 * Style.marginSmall * scaling)) / 5) - - cellWidth: Math.floor((width - leftMargin - rightMargin) / 5) - cellHeight: Math.floor(itemSize * 0.67) + Style.marginSmall * scaling - - leftMargin: Style.marginSmall * scaling - rightMargin: Style.marginSmall * scaling - topMargin: Style.marginSmall * scaling - bottomMargin: Style.marginSmall * scaling - - delegate: Rectangle { - id: wallpaperItem - property string wallpaperPath: Settings.data.wallpaper.directory + "/" + fileName - property bool isSelected: wallpaperPath === Wallpapers.currentWallpaper - - width: wallpaperGridView.itemSize - height: Math.floor(wallpaperGridView.itemSize * 0.67) - radius: Style.radiusMedium * scaling - color: isSelected ? Colors.accentPrimary : Colors.backgroundSecondary - border.color: isSelected ? Colors.accentSecondary : Colors.outline - border.width: Math.max(1, Style.borderThin * scaling) - clip: true - - NImageRounded { - anchors.fill: parent - anchors.margins: Style.marginTiny * scaling - imagePath: wallpaperPath - fallbackIcon: "image" - borderColor: "transparent" - borderWidth: 0 - imageRadius: Style.radiusMedium * scaling - } - - // Selection indicator - Rectangle { - anchors.top: parent.top - anchors.right: parent.right - anchors.margins: Style.marginTiny * scaling - width: 20 * scaling - height: 20 * scaling - radius: width / 2 - color: Colors.accentPrimary - border.color: Colors.onAccent - border.width: Math.max(1, Style.borderThin * scaling) - visible: isSelected - - NText { - anchors.centerIn: parent - text: "check" - font.family: "Material Symbols Outlined" - font.pointSize: Style.fontSizeSmall * scaling - color: Colors.onAccent - } - } - - // Hover effect - Rectangle { - anchors.fill: parent + // Wallpaper grid + NText { + text: "Wallpaper Selector" + font.weight: Style.fontWeightBold color: Colors.textPrimary - opacity: mouseArea.containsMouse ? 0.1 : 0 - radius: parent.radius + } - Behavior on opacity { - NumberAnimation { - duration: 150 + NText { + text: "Click on a wallpaper to set it as your current wallpaper" + color: Colors.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + NText { + text: Settings.data.wallpaper.swww.enabled ? "Wallpapers will change with " + Settings.data.wallpaper.swww.transitionType + + " transition" : "Wallpapers will change instantly" + color: Colors.textSecondary + font.pointSize: Style.fontSizeSmall * scaling + visible: Settings.data.wallpaper.swww.enabled + } + } + + NIconButton { + icon: "refresh" + tooltipText: "Refresh wallpaper list" + onClicked: { + Wallpapers.loadWallpapers() + } + Layout.alignment: Qt.AlignTop | Qt.AlignRight + } + } + + // Wallpaper grid container + Item { + Layout.fillWidth: true + Layout.preferredHeight: 400 * scaling + + FolderListModel { + id: folderModel + folder: "file://" + (Settings.data.wallpaper.directory !== undefined ? Settings.data.wallpaper.directory : "") + nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.pnm", "*.bmp"] + showDirs: false + sortField: FolderListModel.Name + } + + GridView { + id: wallpaperGridView + anchors.fill: parent + clip: true + model: folderModel + + // Fixed 5 items per row - more aggressive sizing + property int itemSize: Math.floor( + (width - leftMargin - rightMargin - (4 * Style.marginSmall * scaling)) / 5) + + cellWidth: Math.floor((width - leftMargin - rightMargin) / 5) + cellHeight: Math.floor(itemSize * 0.67) + Style.marginSmall * scaling + + leftMargin: Style.marginSmall * scaling + rightMargin: Style.marginSmall * scaling + topMargin: Style.marginSmall * scaling + bottomMargin: Style.marginSmall * scaling + + delegate: Rectangle { + id: wallpaperItem + property string wallpaperPath: Settings.data.wallpaper.directory + "/" + fileName + property bool isSelected: wallpaperPath === Wallpapers.currentWallpaper + + width: wallpaperGridView.itemSize + height: Math.floor(wallpaperGridView.itemSize * 0.67) + radius: Style.radiusMedium * scaling + color: isSelected ? Colors.accentPrimary : Colors.backgroundSecondary + border.color: isSelected ? Colors.accentSecondary : Colors.outline + border.width: Math.max(1, Style.borderThin * scaling) + clip: true + + NImageRounded { + anchors.fill: parent + anchors.margins: Style.marginTiny * scaling + imagePath: wallpaperPath + fallbackIcon: "image" + borderColor: "transparent" + borderWidth: 0 + imageRadius: Style.radiusMedium * scaling + } + + // Selection indicator + Rectangle { + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: Style.marginTiny * scaling + width: 20 * scaling + height: 20 * scaling + radius: width / 2 + color: Colors.accentPrimary + border.color: Colors.onAccent + border.width: Math.max(1, Style.borderThin * scaling) + visible: isSelected + + NText { + anchors.centerIn: parent + text: "check" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeSmall * scaling + color: Colors.onAccent + } + } + + // Hover effect + Rectangle { + anchors.fill: parent + color: Colors.textPrimary + opacity: mouseArea.containsMouse ? 0.1 : 0 + radius: parent.radius + + Behavior on opacity { + NumberAnimation { + duration: 150 + } + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + acceptedButtons: Qt.LeftButton + hoverEnabled: true + onClicked: { + Wallpapers.changeWallpaper(wallpaperPath) } } } - - MouseArea { - id: mouseArea - anchors.fill: parent - acceptedButtons: Qt.LeftButton - hoverEnabled: true - onClicked: { - Wallpapers.changeWallpaper(wallpaperPath) - } - } } - } - // Empty state - Rectangle { - anchors.fill: parent - color: Colors.backgroundSecondary - radius: Style.radiusMedium * scaling - border.color: Colors.outline - border.width: Math.max(1, Style.borderThin * scaling) - visible: folderModel.count === 0 && !Wallpapers.scanning + // Empty state + Rectangle { + anchors.fill: parent + color: Colors.backgroundSecondary + radius: Style.radiusMedium * scaling + border.color: Colors.outline + border.width: Math.max(1, Style.borderThin * scaling) + visible: folderModel.count === 0 && !Wallpapers.scanning - ColumnLayout { - anchors.centerIn: parent - spacing: Style.marginMedium * scaling + ColumnLayout { + anchors.centerIn: parent + spacing: Style.marginMedium * scaling - NText { - text: "folder_open" - font.family: "Material Symbols Outlined" - font.pointSize: Style.fontSizeLarge * scaling - color: Colors.textSecondary - Layout.alignment: Qt.AlignHCenter - } + NText { + text: "folder_open" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeLarge * scaling + color: Colors.textSecondary + Layout.alignment: Qt.AlignHCenter + } - NText { - text: "No wallpapers found" - color: Colors.textSecondary - font.weight: Style.fontWeightBold - Layout.alignment: Qt.AlignHCenter - } + NText { + text: "No wallpapers found" + color: Colors.textSecondary + font.weight: Style.fontWeightBold + Layout.alignment: Qt.AlignHCenter + } - NText { - text: "Make sure your wallpaper directory is configured and contains image files" - color: Colors.textSecondary - wrapMode: Text.WordWrap - horizontalAlignment: Text.AlignHCenter - Layout.preferredWidth: 300 * scaling + NText { + text: "Make sure your wallpaper directory is configured and contains image files" + color: Colors.textSecondary + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + Layout.preferredWidth: 300 * scaling + } } } } From fe6a3944481d45f06a2fed301fe1df61ec844d19 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 13 Aug 2025 00:50:53 -0400 Subject: [PATCH 216/394] Settings: Harmonized display tab --- Modules/Settings/Tabs/Display.qml | 160 +++++++++++--------- Modules/Settings/Tabs/WallpaperSelector.qml | 13 +- 2 files changed, 94 insertions(+), 79 deletions(-) diff --git a/Modules/Settings/Tabs/Display.qml b/Modules/Settings/Tabs/Display.qml index ea587d7..66f4a72 100644 --- a/Modules/Settings/Tabs/Display.qml +++ b/Modules/Settings/Tabs/Display.qml @@ -1,5 +1,6 @@ import QtQuick import QtQuick.Layouts +import QtQuick.Controls import Quickshell import qs.Services import qs.Widgets @@ -25,95 +26,108 @@ Item { }) } - ColumnLayout { + ScrollView { anchors.fill: parent - spacing: Style.marginMedium * scaling + clip: true + ScrollBar.vertical.policy: ScrollBar.AsNeeded + ScrollBar.horizontal.policy: ScrollBar.AsNeeded + contentWidth: parent.width - NText { - text: "Per‑monitor configuration" - font.weight: Style.fontWeightBold - color: Colors.accentSecondary - } - - Repeater { - model: Quickshell.screens || [] - delegate: Rectangle { + ColumnLayout { + width: parent.width + ColumnLayout { + spacing: Style.marginLarge * scaling + Layout.margins: Style.marginLarge * scaling Layout.fillWidth: true - radius: Style.radiusMedium * scaling - color: Colors.surface - border.color: Colors.outline - border.width: Math.max(1, Style.borderThin * scaling) - implicitHeight: contentCol.implicitHeight + Style.marginLarge * scaling - ColumnLayout { - id: contentCol - anchors.fill: parent - anchors.margins: Style.marginMedium * scaling - spacing: Style.marginSmall * scaling + NText { + text: "Per‑monitor configuration" + font.pointSize: Style.fontSizeXL * scaling + font.weight: Style.fontWeightBold + color: Colors.textPrimary + } - NText { - text: (modelData.name || "Unknown") - font.weight: Style.fontWeightBold - color: Colors.accentPrimary - } + Repeater { + model: Quickshell.screens || [] + delegate: Rectangle { + Layout.fillWidth: true + radius: Style.radiusMedium * scaling + color: Colors.surface + border.color: Colors.outline + border.width: Math.max(1, Style.borderThin * scaling) + implicitHeight: contentCol.implicitHeight + Style.marginXL * 2 * scaling - RowLayout { - spacing: Style.marginMedium * scaling - NText { - text: `Resolution: ${modelData.width}x${modelData.height}` - color: Colors.textSecondary - } - NText { - text: `Position: (${modelData.x}, ${modelData.y})` - color: Colors.textSecondary - } - } + ColumnLayout { + id: contentCol + anchors.fill: parent + anchors.margins: Style.marginLarge * scaling + spacing: Style.marginTiniest * scaling - NToggle { - label: "Bar" - description: "Display the top bar on this monitor" - value: (Settings.data.bar.monitors || []).indexOf(modelData.name) !== -1 - onToggled: function (newValue) { - if (newValue) { - Settings.data.bar.monitors = addMonitor(Settings.data.bar.monitors, modelData.name) - } else { - Settings.data.bar.monitors = removeMonitor(Settings.data.bar.monitors, modelData.name) + NText { + text: (modelData.name || "Unknown") + font.pointSize: Style.fontSizeLarge * scaling + font.weight: Style.fontWeightBold + color: Colors.accentSecondary } - } - } - NToggle { - label: "Dock" - description: "Display the dock on this monitor" - value: (Settings.data.dock.monitors || []).indexOf(modelData.name) !== -1 - onToggled: function (newValue) { - if (newValue) { - Settings.data.dock.monitors = addMonitor(Settings.data.dock.monitors, modelData.name) - } else { - Settings.data.dock.monitors = removeMonitor(Settings.data.dock.monitors, modelData.name) + NText { + text: `Resolution: ${modelData.width}x${modelData.height} - Position: (${modelData.x}, ${modelData.y})` + font.pointSize: Style.fontSizeSmall * scaling + color: Colors.textSecondary } - } - } - NToggle { - label: "Notifications" - description: "Display notifications on this monitor" - value: (Settings.data.notifications.monitors || []).indexOf(modelData.name) !== -1 - onToggled: function (newValue) { - if (newValue) { - Settings.data.notifications.monitors = addMonitor(Settings.data.notifications.monitors, modelData.name) - } else { - Settings.data.notifications.monitors = removeMonitor(Settings.data.notifications.monitors, - modelData.name) + ColumnLayout { + spacing: Style.marginLarge * scaling + + NToggle { + label: "Bar" + description: "Display the top bar on this monitor" + value: (Settings.data.bar.monitors || []).indexOf(modelData.name) !== -1 + onToggled: function (newValue) { + if (newValue) { + Settings.data.bar.monitors = addMonitor(Settings.data.bar.monitors, modelData.name) + } else { + Settings.data.bar.monitors = removeMonitor(Settings.data.bar.monitors, modelData.name) + } + } + } + + NToggle { + label: "Dock" + description: "Display the dock on this monitor" + value: (Settings.data.dock.monitors || []).indexOf(modelData.name) !== -1 + onToggled: function (newValue) { + if (newValue) { + Settings.data.dock.monitors = addMonitor(Settings.data.dock.monitors, modelData.name) + } else { + Settings.data.dock.monitors = removeMonitor(Settings.data.dock.monitors, modelData.name) + } + } + } + + NToggle { + label: "Notifications" + description: "Display notifications on this monitor" + value: (Settings.data.notifications.monitors || []).indexOf(modelData.name) !== -1 + onToggled: function (newValue) { + if (newValue) { + Settings.data.notifications.monitors = addMonitor(Settings.data.notifications.monitors, + modelData.name) + } else { + Settings.data.notifications.monitors = removeMonitor(Settings.data.notifications.monitors, + modelData.name) + } + } + } } } } } - } - } - Item { - Layout.fillHeight: true + Item { + Layout.fillHeight: true + } + } } } } diff --git a/Modules/Settings/Tabs/WallpaperSelector.qml b/Modules/Settings/Tabs/WallpaperSelector.qml index e96fede..ecfaba5 100644 --- a/Modules/Settings/Tabs/WallpaperSelector.qml +++ b/Modules/Settings/Tabs/WallpaperSelector.qml @@ -30,6 +30,7 @@ Item { // Current wallpaper display NText { text: "Current Wallpaper" + font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold color: Colors.textPrimary } @@ -55,12 +56,11 @@ Item { } } - NDivider { - Layout.fillWidth: true - Layout.topMargin: Style.marginLarge * scaling - Layout.bottomMargin: Style.marginLarge * scaling - } - + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginLarge * scaling + Layout.bottomMargin: Style.marginLarge * scaling + } RowLayout { Layout.fillWidth: true @@ -71,6 +71,7 @@ Item { // Wallpaper grid NText { text: "Wallpaper Selector" + font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold color: Colors.textPrimary } From 5b6ed3577aa3ae1443dcb539c9ab2cd4f033c9a8 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 13 Aug 2025 00:56:37 -0400 Subject: [PATCH 217/394] Wallpaper Selector: Improved GridView preferredHeight computation --- Modules/Settings/Tabs/WallpaperSelector.qml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Modules/Settings/Tabs/WallpaperSelector.qml b/Modules/Settings/Tabs/WallpaperSelector.qml index ecfaba5..052838f 100644 --- a/Modules/Settings/Tabs/WallpaperSelector.qml +++ b/Modules/Settings/Tabs/WallpaperSelector.qml @@ -105,7 +105,9 @@ Item { // Wallpaper grid container Item { Layout.fillWidth: true - Layout.preferredHeight: 400 * scaling + Layout.preferredHeight: { + return folderModel.count / wallpaperGridView.columns * wallpaperGridView.cellHeight + } FolderListModel { id: folderModel @@ -122,10 +124,11 @@ Item { model: folderModel // Fixed 5 items per row - more aggressive sizing + property int columns: 5 property int itemSize: Math.floor( - (width - leftMargin - rightMargin - (4 * Style.marginSmall * scaling)) / 5) + (width - leftMargin - rightMargin - (4 * Style.marginSmall * scaling)) / columns) - cellWidth: Math.floor((width - leftMargin - rightMargin) / 5) + cellWidth: Math.floor((width - leftMargin - rightMargin) / columns) cellHeight: Math.floor(itemSize * 0.67) + Style.marginSmall * scaling leftMargin: Style.marginSmall * scaling From f2b7bd04db544d449795d692884e58aff296ff58 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 13 Aug 2025 00:58:12 -0400 Subject: [PATCH 218/394] Wallpaper Selector: preferredHeight rounded up --- Modules/Settings/Tabs/WallpaperSelector.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Settings/Tabs/WallpaperSelector.qml b/Modules/Settings/Tabs/WallpaperSelector.qml index 052838f..2cc6d94 100644 --- a/Modules/Settings/Tabs/WallpaperSelector.qml +++ b/Modules/Settings/Tabs/WallpaperSelector.qml @@ -106,7 +106,7 @@ Item { Item { Layout.fillWidth: true Layout.preferredHeight: { - return folderModel.count / wallpaperGridView.columns * wallpaperGridView.cellHeight + return Math.ceil(folderModel.count / wallpaperGridView.columns) * wallpaperGridView.cellHeight } FolderListModel { From cc0ea7f37a47e74c6134c3fc35089165e0793ccb Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Wed, 13 Aug 2025 11:37:36 +0200 Subject: [PATCH 219/394] Conditional Overview.qml loading --- Modules/Background/Overview.qml | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/Modules/Background/Overview.qml b/Modules/Background/Overview.qml index 1f64f7c..3a6f05c 100644 --- a/Modules/Background/Overview.qml +++ b/Modules/Background/Overview.qml @@ -3,11 +3,23 @@ import QtQuick.Effects import Quickshell import Quickshell.Wayland import qs.Services +import qs.Widgets -Variants { - model: Quickshell.screens +NLoader { + active: Workspaces.isNiri + + Component.onCompleted: { + if (Workspaces.isNiri) { + console.log("[Overview] Loading Overview component (Niri detected)") + } else { + console.log("[Overview] Skipping Overview component (Niri not detected)") + } + } + + sourceComponent: Variants { + model: Quickshell.screens - delegate: PanelWindow { + delegate: PanelWindow { required property ShellScreen modelData property string wallpaperSource: Wallpapers.currentWallpaper !== "" && !Settings.data.wallpaper.swww.enabled ? Wallpapers.currentWallpaper : "" @@ -53,4 +65,5 @@ Variants { color: Qt.rgba(Colors.backgroundPrimary.r, Colors.backgroundPrimary.g, Colors.backgroundPrimary.b, 0.5) } } + } } From 9008d6bab9db686daf6ef6ae6d31915c5a36089c Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Wed, 13 Aug 2025 12:16:30 +0200 Subject: [PATCH 220/394] Add notification history --- Modules/Bar/Bar.qml | 5 + Modules/Notification/Notification.qml | 4 +- Modules/Notification/NotificationHistory.qml | 180 +++++++++++++++++++ Services/NotificationService.qml | 101 +++++++++++ 4 files changed, 288 insertions(+), 2 deletions(-) create mode 100644 Modules/Notification/NotificationHistory.qml diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 9ad9815..a494664 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -4,6 +4,7 @@ import QtQuick.Layouts import Quickshell import qs.Services import qs.Widgets +import qs.Modules.Notification Variants { model: Quickshell.screens @@ -83,6 +84,10 @@ Variants { } // TODO: Notification Icon + NotificationHistory { + anchors.verticalCenter: parent.verticalCenter + } + WiFi { anchors.verticalCenter: parent.verticalCenter } diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index 40a3cff..36ae1da 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -24,8 +24,8 @@ PanelWindow { WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.exclusionMode: ExclusionMode.Ignore - // Use the notification service - property var notificationService: NotificationService {} + // Use the notification service singleton + property var notificationService: NotificationService // Access the notification model from the service property ListModel notificationModel: notificationService.notificationModel diff --git a/Modules/Notification/NotificationHistory.qml b/Modules/Notification/NotificationHistory.qml new file mode 100644 index 0000000..80fa1d6 --- /dev/null +++ b/Modules/Notification/NotificationHistory.qml @@ -0,0 +1,180 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland +import Quickshell.Services.Notifications +import qs.Services +import qs.Widgets + +NIconButton { + id: root + + readonly property real scaling: Scaling.scale(screen) + sizeMultiplier: 0.8 + showBorder: false + icon: "notifications" + tooltipText: "Notification History" + onClicked: { + notificationHistoryLoader.active = !notificationHistoryLoader.active + } + + // Loader for Notification History menu + NLoader { + id: notificationHistoryLoader + active: false + + content: Component { + NPanel { + id: notificationPanel + + Connections { + target: notificationPanel + ignoreUnknownSignals: true + function onDismissed() { + notificationHistoryLoader.active = false + } + } + + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + + Rectangle { + color: Colors.backgroundSecondary + radius: Style.radiusMedium * scaling + border.color: Colors.backgroundTertiary + border.width: Math.max(1, Style.borderMedium * scaling) + width: 400 * scaling + height: 500 * scaling + anchors.top: parent.top + anchors.right: parent.right + anchors.topMargin: Style.marginTiny * scaling + anchors.rightMargin: Style.marginTiny * scaling + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginLarge * scaling + spacing: Style.marginMedium * scaling + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginMedium * scaling + + NText { + text: "notifications" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXL * scaling + color: Colors.accentPrimary + } + + NText { + text: "Notification History" + font.pointSize: Style.fontSizeLarge * scaling + font.bold: true + color: Colors.textPrimary + Layout.fillWidth: true + } + + NIconButton { + icon: "delete" + sizeMultiplier: 0.8 + tooltipText: "Clear history" + onClicked: NotificationService.clearHistory() + } + + NIconButton { + icon: "close" + sizeMultiplier: 0.8 + onClicked: { + notificationHistoryLoader.active = false + } + } + } + + NDivider {} + + ListView { + id: notificationList + Layout.fillWidth: true + Layout.fillHeight: true + model: NotificationService.historyModel + spacing: Style.marginMedium * scaling + clip: true + boundsBehavior: Flickable.StopAtBounds + + delegate: Rectangle { + width: notificationList ? (notificationList.width - 20) : 380 * scaling + height: 80 + radius: Style.radiusMedium * scaling + color: notificationMouseArea.containsMouse ? Colors.accentPrimary : "transparent" + + RowLayout { + anchors { + fill: parent + margins: 15 + } + spacing: 15 + + + + // Notification content + Column { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + spacing: 5 + + NText { + text: (summary || "No summary").substring(0, 100) + font.pointSize: Style.fontSizeMedium * scaling + font.weight: Font.Medium + color: notificationMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary + wrapMode: Text.Wrap + width: parent.width - 30 + maximumLineCount: 2 + elide: Text.ElideRight + } + + NText { + text: (body || "").substring(0, 150) + font.pointSize: Style.fontSizeSmall * scaling + color: notificationMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.textSecondary + wrapMode: Text.Wrap + width: parent.width - 30 + maximumLineCount: 3 + elide: Text.ElideRight + } + + NText { + text: NotificationService.formatTimestamp(timestamp) + font.pointSize: Style.fontSizeSmall * scaling + color: notificationMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.textSecondary + } + } + + + } + + MouseArea { + id: notificationMouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + console.log("[NotificationHistory] Removing notification:", summary) + NotificationService.historyModel.remove(index) + NotificationService.saveHistory() + } + } + } + + ScrollBar.vertical: ScrollBar { + active: true + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/Services/NotificationService.qml b/Services/NotificationService.qml index 74533bc..9c80c2c 100644 --- a/Services/NotificationService.qml +++ b/Services/NotificationService.qml @@ -1,6 +1,9 @@ import QtQuick +import Quickshell +import Quickshell.Io import qs.Services import Quickshell.Services.Notifications +pragma Singleton QtObject { id: root @@ -34,12 +37,45 @@ QtObject { // Add to our model root.addNotification(notification) + // Also add to history + root.addToHistory(notification) } } // List model to hold notifications property ListModel notificationModel: ListModel {} + // Persistent history of notifications (most recent first) + property ListModel historyModel: ListModel {} + property int maxHistory: 100 + + // Cached history file path + property string historyFile: Quickshell.env("NOCTALIA_NOTIF_HISTORY_FILE") || (Settings.cacheDir + "notifications.json") + + // Persisted storage for history + property FileView historyFileView: FileView { + id: historyFileView + objectName: "notificationHistoryFileView" + path: historyFile + watchChanges: true + onFileChanged: reload() + onAdapterUpdated: writeAdapter() + Component.onCompleted: reload() + onLoaded: loadFromHistory() + onLoadFailed: function (error) { + // Create file on first use + if (error.toString().includes("No such file") || error === 2) { + writeAdapter() + } + } + + JsonAdapter { + id: historyAdapter + property var history: [] + property double timestamp: 0 + } + } + // Maximum visible notifications property int maxVisible: 5 @@ -84,6 +120,71 @@ QtObject { } } + // Add a simplified copy into persistent history + function addToHistory(notification) { + historyModel.insert(0, { + "summary": notification.summary, + "body": notification.body, + "appName": notification.appName, + "urgency": notification.urgency, + "timestamp": new Date() + }) + while (historyModel.count > maxHistory) { + historyModel.remove(historyModel.count - 1) + } + saveHistory() + } + + function clearHistory() { + historyModel.clear() + saveHistory() + } + + function loadFromHistory() { + // Populate in-memory model from adapter + try { + historyModel.clear() + const items = historyAdapter.history || [] + for (var i = 0; i < items.length; i++) { + const it = items[i] + historyModel.append({ + "summary": it.summary || "", + "body": it.body || "", + "appName": it.appName || "", + "urgency": it.urgency, + "timestamp": it.timestamp ? new Date(it.timestamp) : new Date() + }) + } + } catch (e) { + console.error("[Notifications] Failed to load history:", e) + } + } + + function saveHistory() { + try { + // Serialize model back to adapter + var arr = [] + for (var i = 0; i < historyModel.count; i++) { + const n = historyModel.get(i) + arr.push({ + summary: n.summary, + body: n.body, + appName: n.appName, + urgency: n.urgency, + timestamp: (n.timestamp instanceof Date) ? n.timestamp.getTime() : n.timestamp + }) + } + historyAdapter.history = arr + historyAdapter.timestamp = Time.timestamp + + Qt.callLater(function () { + historyFileView.writeAdapter() + }) + } catch (e) { + console.error("[Notifications] Failed to save history:", e) + } + } + // Signal to trigger animation before removal signal animateAndRemove(var notification, int index) From 0babca7b56f2aba3f60b3175766632e5479cbe1f Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Wed, 13 Aug 2025 12:19:16 +0200 Subject: [PATCH 221/394] Fix tray scaling --- Modules/Bar/TrayMenu.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Bar/TrayMenu.qml b/Modules/Bar/TrayMenu.qml index aaf42e4..75073d6 100644 --- a/Modules/Bar/TrayMenu.qml +++ b/Modules/Bar/TrayMenu.qml @@ -15,7 +15,7 @@ PopupWindow { property real anchorY implicitWidth: 180 * scaling - implicitHeight: Math.max(40 * scaling, listView.contentHeight + (Style.marginMedium * 2 * scaling)) + implicitHeight: Math.max(60 * scaling, listView.contentHeight + (Style.marginMedium * 2 * scaling)) visible: false color: "transparent" From 53901f2c9f06889d150b1880c04caeea2fb067b9 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 13 Aug 2025 07:39:08 -0400 Subject: [PATCH 222/394] Battery: disabled debug test mode --- Modules/Bar/Battery.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Bar/Battery.qml b/Modules/Bar/Battery.qml index ff80f00..97bc807 100644 --- a/Modules/Bar/Battery.qml +++ b/Modules/Bar/Battery.qml @@ -9,7 +9,7 @@ NPill { id: root // Test mode - property bool testMode: true + property bool testMode: false property int testPercent: 49 property bool testCharging: false From faa6bcd2225dded0ad467a408ac0349fdbe7d84b Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Wed, 13 Aug 2025 14:00:06 +0200 Subject: [PATCH 223/394] Add Audio Settings, split NotificationHistory --- Modules/Bar/Bar.qml | 2 + Modules/Bar/NotificationHistory.qml | 29 ++ Modules/Bar/NotificationHistoryPanel.qml | 162 ++++++++++ Modules/Bar/Volume.qml | 17 +- Modules/Notification/NotificationHistory.qml | 180 ----------- Modules/Settings/SettingsPanel.qml | 5 + Modules/Settings/Tabs/Audio.qml | 308 +++++++++++++++++++ Modules/Settings/Tabs/Misc.qml | 12 +- Services/NotificationService.qml | 7 + Services/Settings.qml | 42 ++- 10 files changed, 555 insertions(+), 209 deletions(-) create mode 100644 Modules/Bar/NotificationHistory.qml create mode 100644 Modules/Bar/NotificationHistoryPanel.qml delete mode 100644 Modules/Notification/NotificationHistory.qml create mode 100644 Modules/Settings/Tabs/Audio.qml diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index a494664..4948ddb 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -15,6 +15,8 @@ Variants { required property ShellScreen modelData readonly property real scaling: Scaling.scale(screen) + property var settingsPanel: null + screen: modelData implicitHeight: Style.barHeight * scaling color: "transparent" diff --git a/Modules/Bar/NotificationHistory.qml b/Modules/Bar/NotificationHistory.qml new file mode 100644 index 0000000..2d0b638 --- /dev/null +++ b/Modules/Bar/NotificationHistory.qml @@ -0,0 +1,29 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland +import qs.Services +import qs.Widgets + +NIconButton { + id: root + + readonly property real scaling: Scaling.scale(screen) + sizeMultiplier: 0.8 + showBorder: false + icon: "notifications" + tooltipText: "Notification History" + onClicked: { + if (!notificationHistoryPanelLoader.active) { + notificationHistoryPanelLoader.isLoaded = true + } + if (notificationHistoryPanelLoader.item) { + notificationHistoryPanelLoader.item.visible = !notificationHistoryPanelLoader.item.visible + } + } + + NotificationHistoryPanel { + id: notificationHistoryPanelLoader + } +} \ No newline at end of file diff --git a/Modules/Bar/NotificationHistoryPanel.qml b/Modules/Bar/NotificationHistoryPanel.qml new file mode 100644 index 0000000..db1fd42 --- /dev/null +++ b/Modules/Bar/NotificationHistoryPanel.qml @@ -0,0 +1,162 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland +import Quickshell.Services.Notifications +import qs.Services +import qs.Widgets + +// Loader for Notification History panel +NLoader { + id: root + + content: Component { + NPanel { + id: notificationPanel + + Connections { + target: notificationPanel + ignoreUnknownSignals: true + function onDismissed() { + notificationPanel.visible = false + } + } + + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + + Rectangle { + color: Colors.backgroundSecondary + radius: Style.radiusMedium * scaling + border.color: Colors.backgroundTertiary + border.width: Math.max(1, Style.borderMedium * scaling) + width: 400 * scaling + height: 500 * scaling + anchors.top: parent.top + anchors.right: parent.right + anchors.topMargin: Style.marginTiny * scaling + anchors.rightMargin: Style.marginTiny * scaling + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginLarge * scaling + spacing: Style.marginMedium * scaling + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginMedium * scaling + + NText { + text: "notifications" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXL * scaling + color: Colors.accentPrimary + } + + NText { + text: "Notification History" + font.pointSize: Style.fontSizeLarge * scaling + font.bold: true + color: Colors.textPrimary + Layout.fillWidth: true + } + + NIconButton { + icon: "delete" + sizeMultiplier: 0.8 + tooltipText: "Clear history" + onClicked: NotificationService.clearHistory() + } + + NIconButton { + icon: "close" + sizeMultiplier: 0.8 + onClicked: { + notificationPanel.visible = false + } + } + } + + NDivider {} + + ListView { + id: notificationList + Layout.fillWidth: true + Layout.fillHeight: true + model: NotificationService.historyModel + spacing: Style.marginMedium * scaling + clip: true + boundsBehavior: Flickable.StopAtBounds + + delegate: Rectangle { + width: notificationList ? (notificationList.width - 20) : 380 * scaling + height: 80 + radius: Style.radiusMedium * scaling + color: notificationMouseArea.containsMouse ? Colors.accentPrimary : "transparent" + + RowLayout { + anchors { + fill: parent + margins: 15 + } + spacing: 15 + + // Notification content + Column { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + spacing: 5 + + NText { + text: (summary || "No summary").substring(0, 100) + font.pointSize: Style.fontSizeMedium * scaling + font.weight: Font.Medium + color: notificationMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary + wrapMode: Text.Wrap + width: parent.width - 30 + maximumLineCount: 2 + elide: Text.ElideRight + } + + NText { + text: (body || "").substring(0, 150) + font.pointSize: Style.fontSizeSmall * scaling + color: notificationMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.textSecondary + wrapMode: Text.Wrap + width: parent.width - 30 + maximumLineCount: 3 + elide: Text.ElideRight + } + + NText { + text: NotificationService.formatTimestamp(timestamp) + font.pointSize: Style.fontSizeSmall * scaling + color: notificationMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.textSecondary + } + } + } + + MouseArea { + id: notificationMouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + console.log("[NotificationHistory] Removing notification:", summary) + NotificationService.historyModel.remove(index) + NotificationService.saveHistory() + } + } + } + + ScrollBar.vertical: ScrollBar { + active: true + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + } + } + } + } + } + } +} \ No newline at end of file diff --git a/Modules/Bar/Volume.qml b/Modules/Bar/Volume.qml index ee7b690..3f14d22 100644 --- a/Modules/Bar/Volume.qml +++ b/Modules/Bar/Volume.qml @@ -10,6 +10,13 @@ Item { width: pill.width height: pill.height + // Reference to settings panel + property var settingsPanel: null + + Component.onCompleted: { + console.log("[Volume] settingsPanel received:", !!settingsPanel) + } + // Used to avoid opening the pill on Quickshell startup property bool firstVolumeReceived: false @@ -71,7 +78,15 @@ Item { } } onClicked: { - audioDeviceSelector.isLoaded = !audioDeviceSelector.isLoaded + // Open settings panel and navigate to Audio tab + console.log("[Volume] Attempting to open settings panel...") + try { + settingsPanel.isLoaded = true + settingsPanel.content.currentTabIndex = 5 // Audio tab index + console.log("[Volume] Settings panel opened successfully") + } catch (error) { + console.log("[Volume] Error opening settings panel:", error) + } } } } diff --git a/Modules/Notification/NotificationHistory.qml b/Modules/Notification/NotificationHistory.qml deleted file mode 100644 index 80fa1d6..0000000 --- a/Modules/Notification/NotificationHistory.qml +++ /dev/null @@ -1,180 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls -import Quickshell -import Quickshell.Wayland -import Quickshell.Services.Notifications -import qs.Services -import qs.Widgets - -NIconButton { - id: root - - readonly property real scaling: Scaling.scale(screen) - sizeMultiplier: 0.8 - showBorder: false - icon: "notifications" - tooltipText: "Notification History" - onClicked: { - notificationHistoryLoader.active = !notificationHistoryLoader.active - } - - // Loader for Notification History menu - NLoader { - id: notificationHistoryLoader - active: false - - content: Component { - NPanel { - id: notificationPanel - - Connections { - target: notificationPanel - ignoreUnknownSignals: true - function onDismissed() { - notificationHistoryLoader.active = false - } - } - - WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand - - Rectangle { - color: Colors.backgroundSecondary - radius: Style.radiusMedium * scaling - border.color: Colors.backgroundTertiary - border.width: Math.max(1, Style.borderMedium * scaling) - width: 400 * scaling - height: 500 * scaling - anchors.top: parent.top - anchors.right: parent.right - anchors.topMargin: Style.marginTiny * scaling - anchors.rightMargin: Style.marginTiny * scaling - - ColumnLayout { - anchors.fill: parent - anchors.margins: Style.marginLarge * scaling - spacing: Style.marginMedium * scaling - - RowLayout { - Layout.fillWidth: true - spacing: Style.marginMedium * scaling - - NText { - text: "notifications" - font.family: "Material Symbols Outlined" - font.pointSize: Style.fontSizeXL * scaling - color: Colors.accentPrimary - } - - NText { - text: "Notification History" - font.pointSize: Style.fontSizeLarge * scaling - font.bold: true - color: Colors.textPrimary - Layout.fillWidth: true - } - - NIconButton { - icon: "delete" - sizeMultiplier: 0.8 - tooltipText: "Clear history" - onClicked: NotificationService.clearHistory() - } - - NIconButton { - icon: "close" - sizeMultiplier: 0.8 - onClicked: { - notificationHistoryLoader.active = false - } - } - } - - NDivider {} - - ListView { - id: notificationList - Layout.fillWidth: true - Layout.fillHeight: true - model: NotificationService.historyModel - spacing: Style.marginMedium * scaling - clip: true - boundsBehavior: Flickable.StopAtBounds - - delegate: Rectangle { - width: notificationList ? (notificationList.width - 20) : 380 * scaling - height: 80 - radius: Style.radiusMedium * scaling - color: notificationMouseArea.containsMouse ? Colors.accentPrimary : "transparent" - - RowLayout { - anchors { - fill: parent - margins: 15 - } - spacing: 15 - - - - // Notification content - Column { - Layout.fillWidth: true - Layout.alignment: Qt.AlignVCenter - spacing: 5 - - NText { - text: (summary || "No summary").substring(0, 100) - font.pointSize: Style.fontSizeMedium * scaling - font.weight: Font.Medium - color: notificationMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary - wrapMode: Text.Wrap - width: parent.width - 30 - maximumLineCount: 2 - elide: Text.ElideRight - } - - NText { - text: (body || "").substring(0, 150) - font.pointSize: Style.fontSizeSmall * scaling - color: notificationMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.textSecondary - wrapMode: Text.Wrap - width: parent.width - 30 - maximumLineCount: 3 - elide: Text.ElideRight - } - - NText { - text: NotificationService.formatTimestamp(timestamp) - font.pointSize: Style.fontSizeSmall * scaling - color: notificationMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.textSecondary - } - } - - - } - - MouseArea { - id: notificationMouseArea - anchors.fill: parent - hoverEnabled: true - onClicked: { - console.log("[NotificationHistory] Removing notification:", summary) - NotificationService.historyModel.remove(index) - NotificationService.saveHistory() - } - } - } - - ScrollBar.vertical: ScrollBar { - active: true - anchors.right: parent.right - anchors.top: parent.top - anchors.bottom: parent.bottom - } - } - } - } - } - } - } -} \ No newline at end of file diff --git a/Modules/Settings/SettingsPanel.qml b/Modules/Settings/SettingsPanel.qml index b001c39..189e969 100644 --- a/Modules/Settings/SettingsPanel.qml +++ b/Modules/Settings/SettingsPanel.qml @@ -39,6 +39,10 @@ NLoader { "label": "Network", "icon": "wifi", "source": "Tabs/Network.qml" + }, { + "label": "Audio", + "icon": "volume_up", + "source": "Tabs/Audio.qml" }, { "label": "Display", "icon": "monitor", @@ -212,6 +216,7 @@ NLoader { Tabs.TimeWeather {} Tabs.ScreenRecorder {} Tabs.Network {} + Tabs.Audio {} Tabs.Display {} Tabs.Wallpaper {} Tabs.WallpaperSelector {} diff --git a/Modules/Settings/Tabs/Audio.qml b/Modules/Settings/Tabs/Audio.qml new file mode 100644 index 0000000..1874607 --- /dev/null +++ b/Modules/Settings/Tabs/Audio.qml @@ -0,0 +1,308 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell.Services.Pipewire + +import qs.Modules.Settings +import qs.Widgets +import qs.Services + +ColumnLayout { + id: root + + spacing: 0 + + ScrollView { + id: scrollView + + Layout.fillWidth: true + Layout.fillHeight: true + padding: Style.marginMedium * scaling + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + + ColumnLayout { + width: scrollView.availableWidth + spacing: 0 + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 0 + } + + ColumnLayout { + spacing: Style.marginTiny * scaling + Layout.fillWidth: true + + NText { + text: "Audio" + font.pointSize: Style.fontSizeXL * scaling + font.weight: Style.fontWeightBold + color: Colors.textPrimary + Layout.bottomMargin: Style.marginSmall * scaling + } + + // Volume Controls + ColumnLayout { + spacing: Style.marginSmall * scaling + Layout.fillWidth: true + Layout.topMargin: Style.marginSmall * scaling + + // Master Volume + ColumnLayout { + spacing: Style.marginSmall * scaling + Layout.fillWidth: true + + NText { + text: "Master Volume" + font.weight: Style.fontWeightBold + color: Colors.textPrimary + } + + NText { + text: "System-wide volume level" + font.pointSize: Style.fontSizeSmall * scaling + color: Colors.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + RowLayout { + NSlider { + id: masterVolumeSlider + Layout.fillWidth: true + from: 0 + to: allowOverdrive.value ? 200 : 100 + value: (Audio.volume || 0) * 100 + stepSize: 5 + onValueChanged: { + Audio.volumeSet(value / 100) + } + } + + NText { + text: Math.round(masterVolumeSlider.value) + "%" + Layout.alignment: Qt.AlignVCenter + color: Colors.textSecondary + } + } + + NToggle { + id: allowOverdrive + label: "Allow Volume Overdrive" + description: "Enable volume levels above 100% (up to 200%)" + value: Settings.data.audio ? Settings.data.audio.volumeOverdrive : false + onToggled: function (checked) { + Settings.data.audio.volumeOverdrive = checked + + // If overdrive is disabled and current volume is above 100%, cap it + if (!checked && Audio.volume > 1.0) { + Audio.volumeSet(1.0) + } + } + } + } + + // Mute Toggle + ColumnLayout { + spacing: Style.marginSmall * scaling + Layout.fillWidth: true + Layout.topMargin: Style.marginMedium * scaling + + NToggle { + label: "Mute Audio" + description: "Mute or unmute the default audio output" + value: Audio.muted + onToggled: function (newValue) { + if (Audio.sink && Audio.sink.audio) { + Audio.sink.audio.muted = newValue + } + } + } + } + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginLarge * 2 * scaling + Layout.bottomMargin: Style.marginLarge * scaling + } + + // Audio Devices + ColumnLayout { + spacing: Style.marginLarge * scaling + Layout.fillWidth: true + + NText { + text: "Audio Devices" + font.pointSize: Style.fontSizeXL * scaling + font.weight: Style.fontWeightBold + color: Colors.textPrimary + Layout.bottomMargin: Style.marginSmall * scaling + } + + // Output Device + NComboBox { + id: outputDeviceCombo + label: "Output Device" + description: "Default audio output device" + optionsKeys: outputDeviceKeys + optionsLabels: outputDeviceLabels + currentKey: Audio.sink ? Audio.sink.id.toString() : "" + onSelected: function (key) { + // Find the node by ID and set it as preferred + for (let i = 0; i < Pipewire.nodes.count; i++) { + let node = Pipewire.nodes.get(i) + if (node.id.toString() === key && node.isSink) { + Pipewire.preferredDefaultAudioSink = node + break + } + } + } + } + + // Input Device + NComboBox { + id: inputDeviceCombo + label: "Input Device" + description: "Default audio input device" + optionsKeys: inputDeviceKeys + optionsLabels: inputDeviceLabels + currentKey: Audio.source ? Audio.source.id.toString() : "" + onSelected: function (key) { + // Find the node by ID and set it as preferred + for (let i = 0; i < Pipewire.nodes.count; i++) { + let node = Pipewire.nodes.get(i) + if (node.id.toString() === key && !node.isSink) { + Pipewire.preferredDefaultAudioSource = node + break + } + } + } + } + + + } + + // Divider + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginLarge * scaling + Layout.bottomMargin: Style.marginMedium * scaling + } + + // Audio Visualizer Category + ColumnLayout { + spacing: Style.marginSmall * scaling + Layout.fillWidth: true + + NText { + text: "Audio Visualizer" + font.pointSize: Style.fontSizeXL * scaling + font.weight: Style.fontWeightBold + color: Colors.textPrimary + Layout.bottomMargin: Style.marginSmall * scaling + } + + // Audio Visualizer section + NComboBox { + id: audioVisualizerCombo + label: "Visualization Type" + description: "Choose a visualization type for media playback" + optionsKeys: ["radial", "bars", "wave"] + optionsLabels: ["Radial", "Bars", "Wave"] + currentKey: Settings.data.audio ? Settings.data.audio.audioVisualizer.type : "radial" + onSelected: function (key) { + if (!Settings.data.audio) { + Settings.data.audio = {} + } + if (!Settings.data.audio.audioVisualizer) { + Settings.data.audio.audioVisualizer = {} + } + Settings.data.audio.audioVisualizer.type = key + } + } + } + } + } + } + + // Device list properties + property var outputDeviceKeys: ["default"] + property var outputDeviceLabels: ["Default Output"] + property var inputDeviceKeys: ["default"] + property var inputDeviceLabels: ["Default Input"] + + // Bind Pipewire nodes + PwObjectTracker { + id: nodeTracker + objects: [Pipewire.nodes] + } + + // Update device lists when component is completed + Component.onCompleted: { + updateDeviceLists() + } + + // Timer to check if pipewire is ready and update device lists + Timer { + id: deviceUpdateTimer + interval: 100 + repeat: true + running: !(Pipewire && Pipewire.ready) + onTriggered: { + if (Pipewire && Pipewire.ready) { + updateDeviceLists() + running = false + } + } + } + + // Update device lists when nodes change + Connections { + target: nodeTracker + function onObjectsChanged() { + updateDeviceLists() + } + } + + Repeater { + id: nodesRepeater + model: Pipewire.nodes + delegate: Item { + Component.onCompleted: { + if (modelData && modelData.isSink && modelData.audio) { + // Add to output devices + let key = modelData.id.toString() + if (!outputDeviceKeys.includes(key)) { + outputDeviceKeys.push(key) + outputDeviceLabels.push(modelData.description || modelData.name || "Unknown Device") + } + } else if (modelData && !modelData.isSink && modelData.audio) { + // Add to input devices + let key = modelData.id.toString() + if (!inputDeviceKeys.includes(key)) { + inputDeviceKeys.push(key) + inputDeviceLabels.push(modelData.description || modelData.name || "Unknown Device") + } + } + } + } + } + + function updateDeviceLists() { + if (Pipewire && Pipewire.ready) { + // Update comboboxes + if (outputDeviceCombo) { + outputDeviceCombo.optionsKeys = outputDeviceKeys + outputDeviceCombo.optionsLabels = outputDeviceLabels + } + + if (inputDeviceCombo) { + inputDeviceCombo.optionsKeys = inputDeviceKeys + inputDeviceCombo.optionsLabels = inputDeviceLabels + } + } + } +} \ No newline at end of file diff --git a/Modules/Settings/Tabs/Misc.qml b/Modules/Settings/Tabs/Misc.qml index 6b98781..6ffa0a3 100644 --- a/Modules/Settings/Tabs/Misc.qml +++ b/Modules/Settings/Tabs/Misc.qml @@ -40,17 +40,7 @@ ColumnLayout { Layout.bottomMargin: Style.marginSmall * scaling } - // Audio Visualizer section - NComboBox { - label: "Audio Visualizer" - description: "Choose a visualization type" - optionsKeys: ["radial", "bars", "wave"] - optionsLabels: ["Radial", "Bars", "Wave"] - currentKey: Settings.data.audioVisualizer.type - onSelected: function (key) { - Settings.data.audioVisualizer.type = key - } - } + } } } diff --git a/Services/NotificationService.qml b/Services/NotificationService.qml index 9c80c2c..16ddb38 100644 --- a/Services/NotificationService.qml +++ b/Services/NotificationService.qml @@ -27,6 +27,13 @@ QtObject { // Signal when notification is received onNotification: function (notification) { + // Check if notifications are suppressed + if (Settings.data.notifications && Settings.data.notifications.suppressed) { + // Still add to history but don't show notification + root.addToHistory(notification) + return + } + // Track the notification notification.tracked = true diff --git a/Services/Settings.qml b/Services/Settings.qml index 7cf43a3..7305ef0 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -26,6 +26,8 @@ Singleton { // Used to access via Settings.data.xxx.yyy property var data: adapter + // Needed to only have one NPanel loaded at a time. <--- VERY BROKEN + //property var openPanel: null Item { Component.onCompleted: { @@ -36,17 +38,22 @@ Singleton { } FileView { + + // TBC ? needed for SWWW only ? + // Qt.callLater(function () { + // WallpaperManager.setCurrentWallpaper(settings.currentWallpaper, true); + // }) path: settingsFile watchChanges: true onFileChanged: reload() onAdapterUpdated: writeAdapter() - Component.onCompleted: { + Component.onCompleted: function () { reload() } - onLoaded: { + onLoaded: function () { Qt.callLater(function () { if (adapter.wallpaper.current !== "") { - + console.log("Settings: Initializing wallpaper to:", adapter.wallpaper.current) Wallpapers.setCurrentWallpaper(adapter.wallpaper.current, true) } }) @@ -80,6 +87,7 @@ Singleton { property string avatarImage: defaultAvatar property bool dimDesktop: true property bool showScreenCorners: false + property bool showDock: false } // location @@ -102,8 +110,9 @@ Singleton { property string videoCodec: "h264" property string quality: "very_high" property string colorRange: "limited" - property string audioSource: "default_output" property bool showCursor: true + // New: optional audio source selection (default: system output) + property string audioSource: "default_output" } // wallpaper @@ -160,11 +169,16 @@ Singleton { property list monitors: [] } - // audioVisualizer - property JsonObject audioVisualizer + // audio + property JsonObject audio - audioVisualizer: JsonObject { - property string type: "radial" + audio: JsonObject { + property bool volumeOverdrive: false + property JsonObject audioVisualizer + + audioVisualizer: JsonObject { + property string type: "radial" + } } // ui @@ -179,14 +193,8 @@ Singleton { Connections { target: adapter.wallpaper - function onIsRandomChanged() { - Wallpapers.toggleRandomWallpaper() - } - function onRandomIntervalChanged() { - Wallpapers.restartRandomWallpaperTimer() - } - function onDirectoryChanged() { - Wallpapers.loadWallpapers() - } + function onIsRandomChanged() { Wallpapers.toggleRandomWallpaper() } + function onRandomIntervalChanged() { Wallpapers.restartRandomWallpaperTimer() } + function onDirectoryChanged() { Wallpapers.loadWallpapers() } } } From e2a0e491a0e649457604d0348f126e26c1dd9c64 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Wed, 13 Aug 2025 14:03:02 +0200 Subject: [PATCH 224/394] Add audoOverdrive to Volume Symbol in bar --- Modules/Bar/Volume.qml | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/Modules/Bar/Volume.qml b/Modules/Bar/Volume.qml index 3f14d22..5e02908 100644 --- a/Modules/Bar/Volume.qml +++ b/Modules/Bar/Volume.qml @@ -28,17 +28,17 @@ Item { } function getIconColor() { - return (Audio.volume <= 1.0) ? Colors.textPrimary : getVolumeColor() + return (getDisplayVolume() <= 1.0) ? Colors.textPrimary : getVolumeColor() } function getVolumeColor() { - if (Audio.volume <= 1.0) { + if (getDisplayVolume() <= 1.0) { return Colors.accentPrimary } // Indicate that the volume is over 100% // Calculate interpolation factor (0 at 100%, 1.0 at 200%) - let factor = (Audio.volume - 1.0) + let factor = (getDisplayVolume() - 1.0) // Blend between accent and warning colors return Qt.rgba(Colors.accentPrimary.r + (Colors.error.r - Colors.accentPrimary.r) * factor, @@ -46,6 +46,15 @@ Item { Colors.accentPrimary.b + (Colors.error.b - Colors.accentPrimary.b) * factor, 1) } + function getDisplayVolume() { + // If volumeOverdrive is false, clamp to 100% + if (!Settings.data.audio || !Settings.data.audio.volumeOverdrive) { + return Math.min(Audio.volume, 1.0) + } + // If volumeOverdrive is true, allow up to 200% + return Math.min(Audio.volume, 2.0) + } + // Connection used to open the pill when volume changes Connections { target: Audio.sink?.audio ? Audio.sink?.audio : null @@ -66,9 +75,9 @@ Item { iconCircleColor: getVolumeColor() collapsedIconColor: getIconColor() autoHide: true - text: Math.round(Audio.volume * 100) + "%" + text: Math.round(getDisplayVolume() * 100) + "%" tooltipText: "Volume: " + Math.round( - Audio.volume * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume." + getDisplayVolume() * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume." onWheel: function (angle) { if (angle > 0) { From b53cc0466d10600686f3ad393d4f3df0f80a61d5 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 13 Aug 2025 08:04:26 -0400 Subject: [PATCH 225/394] Bar: SysMon --- Bin/system-stats.sh | 15 +++---- Modules/Bar/Bar.qml | 13 +++--- Modules/Bar/SystemMonitor.qml | 84 +++++++++++++++++++++++++++++++++++ Services/SystemStats.qml | 2 + 4 files changed, 100 insertions(+), 14 deletions(-) create mode 100644 Modules/Bar/SystemMonitor.qml diff --git a/Bin/system-stats.sh b/Bin/system-stats.sh index 7bd1170..da966df 100755 --- a/Bin/system-stats.sh +++ b/Bin/system-stats.sh @@ -28,7 +28,7 @@ TEMP_SENSOR_TYPE="" # --- Data Collection Functions --- # -# Gets memory usage in GB and as a percentage. +# Gets memory usage in GB, MB, and as a percentage. # get_memory_info() { awk ' @@ -39,11 +39,10 @@ get_memory_info() { usage_kb = total - available usage_gb = usage_kb / 1000000 usage_percent = (usage_kb / total) * 100 - # MODIFIED: Round the memory percentage to the nearest integer. printf "%.1f %.0f\n", usage_gb, usage_percent } else { # Fallback if /proc/meminfo is unreadable or empty. - print "0.0 0.0" + print "0.0 0 0" } } ' /proc/meminfo @@ -95,10 +94,8 @@ get_cpu_usage() { if (total > 0) { # Formula: 100 * (Total - Idle) / Total usage = 100 * (total - idle) / total - # MODIFIED: Changed format from "%.2f" back to "%.1f" for one decimal place. printf "%.1f\n", usage } else { - # MODIFIED: Changed output back to "0.0" to match the precision. print "0.0" } }' @@ -171,7 +168,7 @@ get_cpu_temp() { # This loop runs indefinitely, gathering and printing stats. while true; do # Call the functions to gather all the data. - # 'read' is used to capture the two output values from get_memory_info. + # get_memory_info read -r mem_gb mem_per <<< "$(get_memory_info)" # Command substitution captures the single output from the other functions. @@ -179,11 +176,11 @@ while true; do cpu_usage=$(get_cpu_usage) cpu_temp=$(get_cpu_temp) - # Use printf to format the final JSON output string, matching the Zig program. - printf '{"mem":"%s", "cpu": "%s", "cputemp": "%s", "memper": "%s", "diskper": "%s"}\n' \ - "$mem_gb" \ + # Use printf to format the final JSON output string, adding the mem_mb key. + printf '{"cpu": "%s", "cputemp": "%s", "memgb":"%s", "memper": "%s", "diskper": "%s"}\n' \ "$cpu_usage" \ "$cpu_temp" \ + "$mem_gb" \ "$mem_per" \ "$disk_per" diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index a494664..3d4c79f 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -50,11 +50,14 @@ Variants { anchors.verticalCenter: parent.verticalCenter spacing: Style.marginSmall * scaling - NText { - text: screen.name - anchors.verticalCenter: parent.verticalCenter - font.weight: Style.fontWeightBold - } + // Debug show monitor name + // NText { + // text: screen.name + // anchors.verticalCenter: parent.verticalCenter + // font.weight: Style.fontWeightBold + // } + + SystemMonitor {} } // Center diff --git a/Modules/Bar/SystemMonitor.qml b/Modules/Bar/SystemMonitor.qml new file mode 100644 index 0000000..ccac84b --- /dev/null +++ b/Modules/Bar/SystemMonitor.qml @@ -0,0 +1,84 @@ +import QtQuick +import Quickshell +import qs.Services +import qs.Widgets + +Row { + id: layout + anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginSmall * scaling + visible: Settings.data.bar.showSystemInfo + + // Ensure our width is an integer + width: Math.floor(cpuUsageLayout.width + cpuTempLayout.width + memoryUsageLayout.width + (2 * 10)) + + Row { + id: cpuUsageLayout + spacing: Style.marginTiny * scaling + + NText { + id: cpuUsageIcon + text: "speed" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeLarge * scaling + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + color: Colors.accentPrimary + } + + NText { + id: cpuUsageText + text: `${SystemStats.cpuUsage}%` + font.pointSize: Style.fontSizeSmall * scaling + font.weight: Style.fontWeightBold + anchors.verticalCenter: parent.verticalCenter + verticalAlignment: Text.AlignVCenter + } + } + + // CPU Temperature Component + Row { + id: cpuTempLayout + spacing: Style.marginTiny * scaling + + NText { + text: "thermometer" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeLarge * scaling + color: Colors.accentPrimary + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + } + + NText { + text: `${SystemStats.cpuTemp}°C` + font.pointSize: Style.fontSizeSmall * scaling + font.weight: Style.fontWeightBold + anchors.verticalCenter: parent.verticalCenter + verticalAlignment: Text.AlignVCenter + } + } + + // Memory Usage Component + Row { + id: memoryUsageLayout + spacing: Style.marginTiny * scaling + + NText { + text: "memory" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeLarge * scaling + color: Colors.accentPrimary + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + } + + NText { + text: `${SystemStats.memoryUsageGb}G` + font.pointSize: Style.fontSizeSmall * scaling + font.weight: Style.fontWeightBold + anchors.verticalCenter: parent.verticalCenter + verticalAlignment: Text.AlignVCenter + } + } +} diff --git a/Services/SystemStats.qml b/Services/SystemStats.qml index add1e38..4c9d3b0 100644 --- a/Services/SystemStats.qml +++ b/Services/SystemStats.qml @@ -11,6 +11,7 @@ Singleton { // Public values property real cpuUsage: 0 property real cpuTemp: 0 + property real memoryUsageGb: 0 property real memoryUsagePer: 0 property real diskUsage: 0 @@ -25,6 +26,7 @@ Singleton { const data = JSON.parse(line) root.cpuUsage = data.cpu root.cpuTemp = data.cputemp + root.memoryUsageGb = data.memgb root.memoryUsagePer = data.memper root.diskUsage = data.diskper } catch (e) { From a57f2f5d683ebc2d026d1fd9dbe58855529cdea2 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 13 Aug 2025 08:05:29 -0400 Subject: [PATCH 226/394] Formatting --- Modules/Background/Overview.qml | 88 ++++++++--------- Modules/Bar/Bar.qml | 1 - Modules/Bar/NotificationHistory.qml | 2 +- Modules/Bar/NotificationHistoryPanel.qml | 2 +- Modules/Settings/Tabs/Audio.qml | 118 +++++++++++------------ Modules/Settings/Tabs/Misc.qml | 2 - Services/NotificationService.qml | 37 +++---- Services/Settings.qml | 12 ++- 8 files changed, 132 insertions(+), 130 deletions(-) diff --git a/Modules/Background/Overview.qml b/Modules/Background/Overview.qml index 3a6f05c..b44f05e 100644 --- a/Modules/Background/Overview.qml +++ b/Modules/Background/Overview.qml @@ -7,7 +7,7 @@ import qs.Widgets NLoader { active: Workspaces.isNiri - + Component.onCompleted: { if (Workspaces.isNiri) { console.log("[Overview] Loading Overview component (Niri detected)") @@ -15,55 +15,55 @@ NLoader { console.log("[Overview] Skipping Overview component (Niri not detected)") } } - + sourceComponent: Variants { model: Quickshell.screens delegate: PanelWindow { - required property ShellScreen modelData - property string wallpaperSource: Wallpapers.currentWallpaper !== "" - && !Settings.data.wallpaper.swww.enabled ? Wallpapers.currentWallpaper : "" + required property ShellScreen modelData + property string wallpaperSource: Wallpapers.currentWallpaper !== "" + && !Settings.data.wallpaper.swww.enabled ? Wallpapers.currentWallpaper : "" - visible: wallpaperSource !== "" && !Settings.data.wallpaper.swww.enabled - color: "transparent" - screen: modelData - WlrLayershell.layer: WlrLayer.Background - WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.namespace: "quickshell-overview" + visible: wallpaperSource !== "" && !Settings.data.wallpaper.swww.enabled + color: "transparent" + screen: modelData + WlrLayershell.layer: WlrLayer.Background + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.namespace: "quickshell-overview" - anchors { - top: true - bottom: true - right: true - left: true + anchors { + top: true + bottom: true + right: true + left: true + } + + Image { + id: bgImage + + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + source: wallpaperSource + cache: true + smooth: true + mipmap: false + visible: wallpaperSource !== "" + } + + MultiEffect { + id: overviewBgBlur + + anchors.fill: parent + source: bgImage + blurEnabled: true + blur: 0.48 + blurMax: 128 + } + + Rectangle { + anchors.fill: parent + color: Qt.rgba(Colors.backgroundPrimary.r, Colors.backgroundPrimary.g, Colors.backgroundPrimary.b, 0.5) + } } - - Image { - id: bgImage - - anchors.fill: parent - fillMode: Image.PreserveAspectCrop - source: wallpaperSource - cache: true - smooth: true - mipmap: false - visible: wallpaperSource !== "" - } - - MultiEffect { - id: overviewBgBlur - - anchors.fill: parent - source: bgImage - blurEnabled: true - blur: 0.48 - blurMax: 128 - } - - Rectangle { - anchors.fill: parent - color: Qt.rgba(Colors.backgroundPrimary.r, Colors.backgroundPrimary.g, Colors.backgroundPrimary.b, 0.5) - } - } } } diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 821464b..3a0a1ab 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -58,7 +58,6 @@ Variants { // anchors.verticalCenter: parent.verticalCenter // font.weight: Style.fontWeightBold // } - SystemMonitor {} } diff --git a/Modules/Bar/NotificationHistory.qml b/Modules/Bar/NotificationHistory.qml index 2d0b638..ccbc019 100644 --- a/Modules/Bar/NotificationHistory.qml +++ b/Modules/Bar/NotificationHistory.qml @@ -26,4 +26,4 @@ NIconButton { NotificationHistoryPanel { id: notificationHistoryPanelLoader } -} \ No newline at end of file +} diff --git a/Modules/Bar/NotificationHistoryPanel.qml b/Modules/Bar/NotificationHistoryPanel.qml index db1fd42..44f791e 100644 --- a/Modules/Bar/NotificationHistoryPanel.qml +++ b/Modules/Bar/NotificationHistoryPanel.qml @@ -159,4 +159,4 @@ NLoader { } } } -} \ No newline at end of file +} diff --git a/Modules/Settings/Tabs/Audio.qml b/Modules/Settings/Tabs/Audio.qml index 1874607..3f783bb 100644 --- a/Modules/Settings/Tabs/Audio.qml +++ b/Modules/Settings/Tabs/Audio.qml @@ -88,14 +88,14 @@ ColumnLayout { } } - NToggle { - id: allowOverdrive - label: "Allow Volume Overdrive" - description: "Enable volume levels above 100% (up to 200%)" - value: Settings.data.audio ? Settings.data.audio.volumeOverdrive : false - onToggled: function (checked) { - Settings.data.audio.volumeOverdrive = checked - + NToggle { + id: allowOverdrive + label: "Allow Volume Overdrive" + description: "Enable volume levels above 100% (up to 200%)" + value: Settings.data.audio ? Settings.data.audio.volumeOverdrive : false + onToggled: function (checked) { + Settings.data.audio.volumeOverdrive = checked + // If overdrive is disabled and current volume is above 100%, cap it if (!checked && Audio.volume > 1.0) { Audio.volumeSet(1.0) @@ -140,58 +140,56 @@ ColumnLayout { font.weight: Style.fontWeightBold color: Colors.textPrimary Layout.bottomMargin: Style.marginSmall * scaling - } - - // Output Device - NComboBox { - id: outputDeviceCombo - label: "Output Device" - description: "Default audio output device" - optionsKeys: outputDeviceKeys - optionsLabels: outputDeviceLabels - currentKey: Audio.sink ? Audio.sink.id.toString() : "" - onSelected: function (key) { - // Find the node by ID and set it as preferred - for (let i = 0; i < Pipewire.nodes.count; i++) { - let node = Pipewire.nodes.get(i) - if (node.id.toString() === key && node.isSink) { - Pipewire.preferredDefaultAudioSink = node - break - } - } - } - } - - // Input Device - NComboBox { - id: inputDeviceCombo - label: "Input Device" - description: "Default audio input device" - optionsKeys: inputDeviceKeys - optionsLabels: inputDeviceLabels - currentKey: Audio.source ? Audio.source.id.toString() : "" - onSelected: function (key) { - // Find the node by ID and set it as preferred - for (let i = 0; i < Pipewire.nodes.count; i++) { - let node = Pipewire.nodes.get(i) - if (node.id.toString() === key && !node.isSink) { - Pipewire.preferredDefaultAudioSource = node - break - } - } - } - } - - - } - - // Divider - NDivider { - Layout.fillWidth: true - Layout.topMargin: Style.marginLarge * scaling - Layout.bottomMargin: Style.marginMedium * scaling } + // Output Device + NComboBox { + id: outputDeviceCombo + label: "Output Device" + description: "Default audio output device" + optionsKeys: outputDeviceKeys + optionsLabels: outputDeviceLabels + currentKey: Audio.sink ? Audio.sink.id.toString() : "" + onSelected: function (key) { + // Find the node by ID and set it as preferred + for (var i = 0; i < Pipewire.nodes.count; i++) { + let node = Pipewire.nodes.get(i) + if (node.id.toString() === key && node.isSink) { + Pipewire.preferredDefaultAudioSink = node + break + } + } + } + } + + // Input Device + NComboBox { + id: inputDeviceCombo + label: "Input Device" + description: "Default audio input device" + optionsKeys: inputDeviceKeys + optionsLabels: inputDeviceLabels + currentKey: Audio.source ? Audio.source.id.toString() : "" + onSelected: function (key) { + // Find the node by ID and set it as preferred + for (var i = 0; i < Pipewire.nodes.count; i++) { + let node = Pipewire.nodes.get(i) + if (node.id.toString() === key && !node.isSink) { + Pipewire.preferredDefaultAudioSource = node + break + } + } + } + } + } + + // Divider + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginLarge * scaling + Layout.bottomMargin: Style.marginMedium * scaling + } + // Audio Visualizer Category ColumnLayout { spacing: Style.marginSmall * scaling @@ -298,11 +296,11 @@ ColumnLayout { outputDeviceCombo.optionsKeys = outputDeviceKeys outputDeviceCombo.optionsLabels = outputDeviceLabels } - + if (inputDeviceCombo) { inputDeviceCombo.optionsKeys = inputDeviceKeys inputDeviceCombo.optionsLabels = inputDeviceLabels } } } -} \ No newline at end of file +} diff --git a/Modules/Settings/Tabs/Misc.qml b/Modules/Settings/Tabs/Misc.qml index 6ffa0a3..22958a8 100644 --- a/Modules/Settings/Tabs/Misc.qml +++ b/Modules/Settings/Tabs/Misc.qml @@ -39,8 +39,6 @@ ColumnLayout { color: Colors.textPrimary Layout.bottomMargin: Style.marginSmall * scaling } - - } } } diff --git a/Services/NotificationService.qml b/Services/NotificationService.qml index 16ddb38..f771ef3 100644 --- a/Services/NotificationService.qml +++ b/Services/NotificationService.qml @@ -57,7 +57,8 @@ QtObject { property int maxHistory: 100 // Cached history file path - property string historyFile: Quickshell.env("NOCTALIA_NOTIF_HISTORY_FILE") || (Settings.cacheDir + "notifications.json") + property string historyFile: Quickshell.env("NOCTALIA_NOTIF_HISTORY_FILE") + || (Settings.cacheDir + "notifications.json") // Persisted storage for history property FileView historyFileView: FileView { @@ -130,12 +131,12 @@ QtObject { // Add a simplified copy into persistent history function addToHistory(notification) { historyModel.insert(0, { - "summary": notification.summary, - "body": notification.body, - "appName": notification.appName, - "urgency": notification.urgency, - "timestamp": new Date() - }) + "summary": notification.summary, + "body": notification.body, + "appName": notification.appName, + "urgency": notification.urgency, + "timestamp": new Date() + }) while (historyModel.count > maxHistory) { historyModel.remove(historyModel.count - 1) } @@ -155,12 +156,12 @@ QtObject { for (var i = 0; i < items.length; i++) { const it = items[i] historyModel.append({ - "summary": it.summary || "", - "body": it.body || "", - "appName": it.appName || "", - "urgency": it.urgency, - "timestamp": it.timestamp ? new Date(it.timestamp) : new Date() - }) + "summary": it.summary || "", + "body": it.body || "", + "appName": it.appName || "", + "urgency": it.urgency, + "timestamp": it.timestamp ? new Date(it.timestamp) : new Date() + }) } } catch (e) { console.error("[Notifications] Failed to load history:", e) @@ -174,11 +175,11 @@ QtObject { for (var i = 0; i < historyModel.count; i++) { const n = historyModel.get(i) arr.push({ - summary: n.summary, - body: n.body, - appName: n.appName, - urgency: n.urgency, - timestamp: (n.timestamp instanceof Date) ? n.timestamp.getTime() : n.timestamp + "summary": n.summary, + "body": n.body, + "appName": n.appName, + "urgency": n.urgency, + "timestamp": (n.timestamp instanceof Date) ? n.timestamp.getTime() : n.timestamp }) } historyAdapter.history = arr diff --git a/Services/Settings.qml b/Services/Settings.qml index 7305ef0..392f42e 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -193,8 +193,14 @@ Singleton { Connections { target: adapter.wallpaper - function onIsRandomChanged() { Wallpapers.toggleRandomWallpaper() } - function onRandomIntervalChanged() { Wallpapers.restartRandomWallpaperTimer() } - function onDirectoryChanged() { Wallpapers.loadWallpapers() } + function onIsRandomChanged() { + Wallpapers.toggleRandomWallpaper() + } + function onRandomIntervalChanged() { + Wallpapers.restartRandomWallpaperTimer() + } + function onDirectoryChanged() { + Wallpapers.loadWallpapers() + } } } From a01d730f3dcf1424ac9d68af850f4fe95db89469 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 13 Aug 2025 08:21:47 -0400 Subject: [PATCH 227/394] NComBox: added safe guards if not options or invalid option index --- Widgets/NComboBox.qml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Widgets/NComboBox.qml b/Widgets/NComboBox.qml index 7219391..1950f0e 100644 --- a/Widgets/NComboBox.qml +++ b/Widgets/NComboBox.qml @@ -69,9 +69,7 @@ ColumnLayout { font.pointSize: Style.fontSizeMedium * scaling verticalAlignment: Text.AlignVCenter elide: Text.ElideRight - text: { - return root.optionsLabels[combo.currentIndex] - } + text: (combo.currentIndex < root.optionsLabels.length) ? root.optionsLabels[combo.currentIndex] : ""; } // Drop down indicator @@ -110,9 +108,7 @@ ColumnLayout { highlighted: combo.highlightedIndex === index contentItem: NText { - text: { - return root.optionsLabels[combo.model.indexOf(modelData)] - } + text: (combo.model.indexOf(modelData)< root.optionsLabels.length) ? root.optionsLabels[combo.model.indexOf(modelData)] : "" font.pointSize: Style.fontSizeMedium * scaling color: highlighted ? Colors.backgroundPrimary : Colors.textPrimary verticalAlignment: Text.AlignVCenter From 07b29ee8730650b5acacdca319218173ad1a34ff Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 13 Aug 2025 08:30:52 -0400 Subject: [PATCH 228/394] NComboBox: better safe guards --- Widgets/NComboBox.qml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Widgets/NComboBox.qml b/Widgets/NComboBox.qml index 1950f0e..e34e9f0 100644 --- a/Widgets/NComboBox.qml +++ b/Widgets/NComboBox.qml @@ -69,7 +69,8 @@ ColumnLayout { font.pointSize: Style.fontSizeMedium * scaling verticalAlignment: Text.AlignVCenter elide: Text.ElideRight - text: (combo.currentIndex < root.optionsLabels.length) ? root.optionsLabels[combo.currentIndex] : ""; + text: (combo.currentIndex >= 0 + && combo.currentIndex < root.optionsLabels.length) ? root.optionsLabels[combo.currentIndex] : "" } // Drop down indicator @@ -108,7 +109,8 @@ ColumnLayout { highlighted: combo.highlightedIndex === index contentItem: NText { - text: (combo.model.indexOf(modelData)< root.optionsLabels.length) ? root.optionsLabels[combo.model.indexOf(modelData)] : "" + text: (combo.model.indexOf(modelData) >= 0 && combo.model.indexOf( + modelData) < root.optionsLabels.length) ? root.optionsLabels[combo.model.indexOf(modelData)] : "" font.pointSize: Style.fontSizeMedium * scaling color: highlighted ? Colors.backgroundPrimary : Colors.textPrimary verticalAlignment: Text.AlignVCenter From 2fe3ad48a79258211b3690ea37da8511289fedb9 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 13 Aug 2025 08:34:30 -0400 Subject: [PATCH 229/394] Bar Volume: OnClick open SettingsPanel with audio tab focused --- Modules/Bar/Bar.qml | 2 -- Modules/Bar/Volume.qml | 14 ++------------ Modules/Settings/SettingsPanel.qml | 16 ++++++---------- 3 files changed, 8 insertions(+), 24 deletions(-) diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 3a0a1ab..bd14629 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -15,8 +15,6 @@ Variants { required property ShellScreen modelData readonly property real scaling: Scaling.scale(screen) - property var settingsPanel: null - screen: modelData implicitHeight: Style.barHeight * scaling color: "transparent" diff --git a/Modules/Bar/Volume.qml b/Modules/Bar/Volume.qml index 5e02908..7d17e8f 100644 --- a/Modules/Bar/Volume.qml +++ b/Modules/Bar/Volume.qml @@ -10,9 +10,6 @@ Item { width: pill.width height: pill.height - // Reference to settings panel - property var settingsPanel: null - Component.onCompleted: { console.log("[Volume] settingsPanel received:", !!settingsPanel) } @@ -87,15 +84,8 @@ Item { } } onClicked: { - // Open settings panel and navigate to Audio tab - console.log("[Volume] Attempting to open settings panel...") - try { - settingsPanel.isLoaded = true - settingsPanel.content.currentTabIndex = 5 // Audio tab index - console.log("[Volume] Settings panel opened successfully") - } catch (error) { - console.log("[Volume] Error opening settings panel:", error) - } + settingsPanel.currentTabIndex = 5 // Audio tab index + settingsPanel.isLoaded = true } } } diff --git a/Modules/Settings/SettingsPanel.qml b/Modules/Settings/SettingsPanel.qml index 189e969..a073432 100644 --- a/Modules/Settings/SettingsPanel.qml +++ b/Modules/Settings/SettingsPanel.qml @@ -10,6 +10,8 @@ import qs.Widgets NLoader { id: root + property int currentTabIndex: 0 + content: Component { NPanel { id: panel @@ -18,7 +20,6 @@ NLoader { WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand - property int currentTabIndex: 0 property var tabsModel: [{ "label": "General", "icon": "tune", @@ -65,11 +66,6 @@ NLoader { "source": "Tabs/About.qml" }] - onVisibleChanged: { - if (visible) - currentTabIndex = 0 - } - Component.onCompleted: show() Rectangle { @@ -113,7 +109,7 @@ NLoader { delegate: Rectangle { id: tabItem - readonly property bool selected: index === panel.currentTabIndex + readonly property bool selected: index === currentTabIndex width: parent.width height: 32 * scaling // Back to original height radius: Style.radiusSmall * scaling @@ -154,7 +150,7 @@ NLoader { onEntered: tabItem.hovering = true onExited: tabItem.hovering = false onCanceled: tabItem.hovering = false - onClicked: panel.currentTabIndex = index + onClicked: currentTabIndex = index } } } @@ -185,7 +181,7 @@ NLoader { // Tab label on the main right NText { - text: panel.tabsModel[panel.currentTabIndex].label + text: panel.tabsModel[currentTabIndex].label font.pointSize: Style.fontSizeLarge * scaling font.weight: Style.fontWeightBold color: Colors.accentPrimary @@ -209,7 +205,7 @@ NLoader { id: stack Layout.fillWidth: true Layout.fillHeight: true - currentIndex: panel.currentTabIndex + currentIndex: currentTabIndex Tabs.General {} Tabs.Bar {} From c4264f2f097596dcc9e9c46468c6807441530abe Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 13 Aug 2025 08:37:45 -0400 Subject: [PATCH 230/394] Settings: hiding misc tabs for now, as its empty --- Modules/Settings/SettingsPanel.qml | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Modules/Settings/SettingsPanel.qml b/Modules/Settings/SettingsPanel.qml index a073432..7625af4 100644 --- a/Modules/Settings/SettingsPanel.qml +++ b/Modules/Settings/SettingsPanel.qml @@ -20,7 +20,8 @@ NLoader { WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand - property var tabsModel: [{ + property var tabsModel: [ + { "label": "General", "icon": "tune", "source": "Tabs/General.qml" @@ -56,11 +57,13 @@ NLoader { "label": "Wallpaper Selector", "icon": "wallpaper_slideshow", "source": "Tabs/WallpaperSelector.qml" - }, { - "label": "Misc", - "icon": "more_horiz", - "source": "Tabs/Misc.qml" - }, { + }, + // { + // "label": "Misc", + // "icon": "more_horiz", + // "source": "Tabs/Misc.qml" + // }, + { "label": "About", "icon": "info", "source": "Tabs/About.qml" @@ -109,7 +112,7 @@ NLoader { delegate: Rectangle { id: tabItem - readonly property bool selected: index === currentTabIndex + width: parent.width height: 32 * scaling // Back to original height radius: Style.radiusSmall * scaling @@ -117,6 +120,8 @@ NLoader { border.color: "transparent" border.width: 0 + readonly property bool selected: index === currentTabIndex + // Subtle hover effect: only icon/text color tint on hover property bool hovering: false From 94b4a096e794c2a5d9bf4419834c5346ee25e0de Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 13 Aug 2025 08:39:12 -0400 Subject: [PATCH 231/394] SidePanel - Utilities: rewired the wallpaper selector shortcut --- Modules/SidePanel/Cards/UtilitiesCard.qml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Modules/SidePanel/Cards/UtilitiesCard.qml b/Modules/SidePanel/Cards/UtilitiesCard.qml index 8040e06..4f6a17b 100644 --- a/Modules/SidePanel/Cards/UtilitiesCard.qml +++ b/Modules/SidePanel/Cards/UtilitiesCard.qml @@ -30,6 +30,10 @@ NBox { // Wallpaper NIconButton { icon: "image" + onClicked: { + settingsPanel.currentTabIndex = 8 // Audio tab index + settingsPanel.isLoaded = true + } } Item { From 8663b36ecbcb076895be60bf5a16c06aef180c40 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 13 Aug 2025 09:03:09 -0400 Subject: [PATCH 232/394] PowerMenu wip --- Modules/Settings/SettingsPanel.qml | 10 +- Modules/SidePanel/Cards/ProfileCard.qml | 381 ++++++++++++++++++++++++ Services/Settings.qml | 2 +- 3 files changed, 386 insertions(+), 7 deletions(-) diff --git a/Modules/Settings/SettingsPanel.qml b/Modules/Settings/SettingsPanel.qml index 7625af4..e237c6a 100644 --- a/Modules/Settings/SettingsPanel.qml +++ b/Modules/Settings/SettingsPanel.qml @@ -20,8 +20,7 @@ NLoader { WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand - property var tabsModel: [ - { + property var tabsModel: [{ "label": "General", "icon": "tune", "source": "Tabs/General.qml" @@ -57,8 +56,7 @@ NLoader { "label": "Wallpaper Selector", "icon": "wallpaper_slideshow", "source": "Tabs/WallpaperSelector.qml" - }, - // { + }, // { // "label": "Misc", // "icon": "more_horiz", // "source": "Tabs/Misc.qml" @@ -112,7 +110,7 @@ NLoader { delegate: Rectangle { id: tabItem - + width: parent.width height: 32 * scaling // Back to original height radius: Style.radiusSmall * scaling @@ -120,7 +118,7 @@ NLoader { border.color: "transparent" border.width: 0 - readonly property bool selected: index === currentTabIndex + readonly property bool selected: index === currentTabIndex // Subtle hover effect: only icon/text color tint on hover property bool hovering: false diff --git a/Modules/SidePanel/Cards/ProfileCard.qml b/Modules/SidePanel/Cards/ProfileCard.qml index 90a23db..3fa605d 100644 --- a/Modules/SidePanel/Cards/ProfileCard.qml +++ b/Modules/SidePanel/Cards/ProfileCard.qml @@ -62,11 +62,18 @@ NBox { } } NIconButton { + id: powerButton icon: "power_settings_new" + onClicked: { + //settingsPanel.isLoaded = !settingsPanel.isLoaded + powerMenu.show() + } } } } + // ---------------------------------- + // Uptime Timer { interval: 60000 repeat: true @@ -99,4 +106,378 @@ NBox { } } } + + // ---------------------------------- + // Logout menu + function logout() { + if (WorkspaceManager.isNiri) { + logoutProcessNiri.running = true + } else if (WorkspaceManager.isHyprland) { + logoutProcessHyprland.running = true + } else { + console.warn("No supported compositor detected for logout") + } + } + + function suspend() { + suspendProcess.running = true + } + + function shutdown() { + shutdownProcess.running = true + } + + function reboot() { + rebootProcess.running = true + } + + function updateSystemInfo() { + uptimeProcess.running = true + } + + + Process { + id: shutdownProcess + + command: ["shutdown", "-h", "now"] + running: false + } + + Process { + id: rebootProcess + + command: ["reboot"] + running: false + } + + Process { + id: suspendProcess + + command: ["systemctl", "suspend"] + running: false + } + + Process { + id: logoutProcessNiri + + command: ["niri", "msg", "action", "quit", "--skip-confirmation"] + running: false + } + + Process { + id: logoutProcessHyprland + + command: ["hyprctl", "dispatch", "exit"] + running: false + } + + Process { + id: logoutProcess + + command: ["loginctl", "terminate-user", Quickshell.env("USER")] + running: false + } + + NPanel { + id: powerMenu + + anchors.top: powerButton.bottom + anchors.right: powerButton.right + + Rectangle { + width: 160 * scaling + height: 220 * scaling + color: Colors.surface + radius: 8 * scaling + border.color: Colors.outline + border.width: 1 * scaling + visible: true + z: 9999 + anchors.top: parent.top + anchors.right: parent.right + anchors.rightMargin: 32 * scaling + anchors.topMargin: powerButton.y + powerButton.height + 48 * scaling + + // Prevent closing when clicking in the panel bg + MouseArea { + anchors.fill: parent + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 8 * scaling + spacing: 4 * scaling + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 36 * scaling + radius: 6 * scaling + color: lockButtonArea.containsMouse ? Colors.accentPrimary : "transparent" + + Item { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 * scaling + anchors.rightMargin: 12 * scaling + + Row { + id: lockRow + spacing: 8 * scaling + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + Text { + text: "lock_outline" + font.family: "Material Symbols Outlined" + font.pixelSize: 16 * scaling + color: lockButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: 1 * scaling + } + + Text { + text: "Lock Screen" + font.pixelSize: 14 * scaling + color: lockButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: 1 * scaling + } + } + } + + MouseArea { + id: lockButtonArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + lockScreen.locked = true + systemMenu.visible = false + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 36 * scaling + radius: 6 * scaling + color: suspendButtonArea.containsMouse ? Colors.accentPrimary : "transparent" + + Item { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 * scaling + anchors.rightMargin: 12 * scaling + + Row { + id: suspendRow + spacing: 8 * scaling + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + Text { + text: "bedtime" + font.family: "Material Symbols Outlined" + font.pixelSize: 16 * scaling + color: suspendButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: 1 * scaling + } + + Text { + text: "Suspend" + font.pixelSize: 14 * scaling + color: suspendButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: 1 * scaling + } + } + } + + MouseArea { + id: suspendButtonArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + suspend() + systemMenu.visible = false + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 36 * scaling + radius: 6 * scaling + color: rebootButtonArea.containsMouse ? Colors.accentPrimary : "transparent" + + Item { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 * scaling + anchors.rightMargin: 12 * scaling + + Row { + id: rebootRow + spacing: 8 * scaling + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + Text { + text: "refresh" + font.family: "Material Symbols Outlined" + font.pixelSize: 16 * scaling + color: rebootButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: 1 * scaling + } + + Text { + text: "Reboot" + font.pixelSize: 14 * scaling + color: rebootButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: 1 * scaling + } + } + } + + MouseArea { + id: rebootButtonArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + reboot() + systemMenu.visible = false + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 36 * scaling + radius: 6 * scaling + color: logoutButtonArea.containsMouse ? Colors.accentPrimary : "transparent" + + Item { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 * scaling + anchors.rightMargin: 12 * scaling + + Row { + id: logoutRow + spacing: 8 * scaling + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + Text { + text: "exit_to_app" + font.family: "Material Symbols Outlined" + font.pixelSize: 16 * scaling + color: logoutButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: 1 * scaling + } + + Text { + text: "Logout" + font.pixelSize: 14 * scaling + color: logoutButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: 1 * scaling + } + } + } + + MouseArea { + id: logoutButtonArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + logout() + systemMenu.visible = false + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 36 * scaling + radius: 6 * scaling + color: shutdownButtonArea.containsMouse ? Colors.accentPrimary : "transparent" + + Item { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 * scaling + anchors.rightMargin: 12 * scaling + + Row { + id: shutdownRow + spacing: 8 * scaling + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + Text { + text: "power_settings_new" + font.family: "Material Symbols Outlined" + font.pixelSize: 16 * scaling + color: shutdownButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: 1 * scaling + } + + Text { + text: "Shutdown" + font.pixelSize: 14 * scaling + color: shutdownButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: 1 * scaling + } + } + } + + MouseArea { + id: shutdownButtonArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + shutdown() + systemMenu.visible = false + } + } + } + } + } + } } diff --git a/Services/Settings.qml b/Services/Settings.qml index 392f42e..301714a 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -53,7 +53,7 @@ Singleton { onLoaded: function () { Qt.callLater(function () { if (adapter.wallpaper.current !== "") { - console.log("Settings: Initializing wallpaper to:", adapter.wallpaper.current) + console.log("[Settings] Set current wallpaper") Wallpapers.setCurrentWallpaper(adapter.wallpaper.current, true) } }) From bf94ebe46d829740b52c30c0937ab2e17b5fd0fd Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Wed, 13 Aug 2025 15:08:54 +0200 Subject: [PATCH 233/394] Add animations everywhere --- Modules/Bar/Bar.qml | 14 ++-- Modules/Bar/NotificationHistory.qml | 12 +++- Modules/Bar/NotificationHistoryPanel.qml | 71 ++++++++++++++++++++- Modules/Bar/Tray.qml | 81 +++++++++++++++++++++++- Modules/Bar/WiFi.qml | 13 +++- Modules/Bar/WiFiMenu.qml | 68 +++++++++++++++++++- Modules/Calendar/Calendar.qml | 77 ++++++++++++++++++++++ Modules/Demo/DemoPanel.qml | 77 +++++++++++++++++++++- Modules/Settings/SettingsPanel.qml | 77 +++++++++++++++++++++- Modules/SidePanel/SidePanel.qml | 78 +++++++++++++++++++++++ Widgets/NTooltip.qml | 78 +++++++++++++++++++---- 11 files changed, 618 insertions(+), 28 deletions(-) diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 4948ddb..bbb3a96 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -133,13 +133,19 @@ Variants { const localCenterX = width / 2 const localCenterY = height / 2 const globalPoint = mapToItem(null, localCenterX, localCenterY) - if (sidePanel.isLoaded) - sidePanel.isLoaded = false - else if (sidePanel.openAt) + if (sidePanel.isLoaded) { + // Call hide() instead of directly setting isLoaded to false + if (sidePanel.item && sidePanel.item.hide) { + sidePanel.item.hide() + } else { + sidePanel.isLoaded = false + } + } else if (sidePanel.openAt) { sidePanel.openAt(globalPoint.x, screen) - else + } else { // Fallback: toggle if API unavailable sidePanel.isLoaded = true + } } } } diff --git a/Modules/Bar/NotificationHistory.qml b/Modules/Bar/NotificationHistory.qml index 2d0b638..ba3fda5 100644 --- a/Modules/Bar/NotificationHistory.qml +++ b/Modules/Bar/NotificationHistory.qml @@ -19,7 +19,17 @@ NIconButton { notificationHistoryPanelLoader.isLoaded = true } if (notificationHistoryPanelLoader.item) { - notificationHistoryPanelLoader.item.visible = !notificationHistoryPanelLoader.item.visible + if (notificationHistoryPanelLoader.item.visible) { + // Panel is visible, hide it with animation + if (notificationHistoryPanelLoader.item.hide) { + notificationHistoryPanelLoader.item.hide() + } else { + notificationHistoryPanelLoader.item.visible = false + } + } else { + // Panel is hidden, show it + notificationHistoryPanelLoader.item.visible = true + } } } diff --git a/Modules/Bar/NotificationHistoryPanel.qml b/Modules/Bar/NotificationHistoryPanel.qml index db1fd42..530cc98 100644 --- a/Modules/Bar/NotificationHistoryPanel.qml +++ b/Modules/Bar/NotificationHistoryPanel.qml @@ -15,17 +15,56 @@ NLoader { NPanel { id: notificationPanel + // Override hide function to animate first + function hide() { + // Start hide animation + notificationRect.scaleValue = 0.8 + notificationRect.opacityValue = 0.0 + + // Hide after animation completes + hideTimer.start() + } + Connections { target: notificationPanel ignoreUnknownSignals: true function onDismissed() { + // Start hide animation + notificationRect.scaleValue = 0.8 + notificationRect.opacityValue = 0.0 + + // Hide after animation completes + hideTimer.start() + } + } + + // Also handle visibility changes from external sources + onVisibleChanged: { + if (!visible && notificationRect.opacityValue > 0) { + // Start hide animation + notificationRect.scaleValue = 0.8 + notificationRect.opacityValue = 0.0 + + // Hide after animation completes + hideTimer.start() + } + } + + // Timer to hide panel after animation + Timer { + id: hideTimer + interval: Style.animationSlow + repeat: false + onTriggered: { notificationPanel.visible = false + notificationPanel.dismissed() } } WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand Rectangle { + id: notificationRect color: Colors.backgroundSecondary radius: Style.radiusMedium * scaling border.color: Colors.backgroundTertiary @@ -37,6 +76,36 @@ NLoader { anchors.topMargin: Style.marginTiny * scaling anchors.rightMargin: Style.marginTiny * scaling + // Animation properties + property real scaleValue: 0.8 + property real opacityValue: 0.0 + + scale: scaleValue + opacity: opacityValue + + // Animate in when component is completed + Component.onCompleted: { + scaleValue = 1.0 + opacityValue = 1.0 + } + + // Animation behaviors + Behavior on scale { + NumberAnimation { + duration: Style.animationSlow + easing.type: Easing.OutExpo + + } + } + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutQuad + + } + } + ColumnLayout { anchors.fill: parent anchors.margins: Style.marginLarge * scaling @@ -72,7 +141,7 @@ NLoader { icon: "close" sizeMultiplier: 0.8 onClicked: { - notificationPanel.visible = false + notificationPanel.hide() } } } diff --git a/Modules/Bar/Tray.qml b/Modules/Bar/Tray.qml index f4cc90b..f74c0bd 100644 --- a/Modules/Bar/Tray.qml +++ b/Modules/Bar/Tray.qml @@ -122,16 +122,91 @@ Item { NPanel { id: trayPanel showOverlay: false // no colors overlay even if activated in settings + + // Override hide function to animate first + function hide() { + // Start hide animation + trayMenuRect.scaleValue = 0.8 + trayMenuRect.opacityValue = 0.0 + + // Hide after animation completes + hideTimer.start() + } + Connections { target: trayPanel ignoreUnknownSignals: true - function onDismissed() { + function onDismissed() { + // Start hide animation + trayMenuRect.scaleValue = 0.8 + trayMenuRect.opacityValue = 0.0 + + // Hide after animation completes + hideTimer.start() + } + } + + // Also handle visibility changes from external sources + onVisibleChanged: { + if (!visible && trayMenuRect.opacityValue > 0) { + // Start hide animation + trayMenuRect.scaleValue = 0.8 + trayMenuRect.opacityValue = 0.0 + + // Hide after animation completes + hideTimer.start() + } + } + + // Timer to hide panel after animation + Timer { + id: hideTimer + interval: Style.animationSlow + repeat: false + onTriggered: { trayPanel.visible = false trayMenu.hideMenu() } } - TrayMenu { - id: trayMenu + + Rectangle { + id: trayMenuRect + color: "transparent" + anchors.fill: parent + + // Animation properties + property real scaleValue: 0.8 + property real opacityValue: 0.0 + + scale: scaleValue + opacity: opacityValue + + // Animate in when component is completed + Component.onCompleted: { + scaleValue = 1.0 + opacityValue = 1.0 + } + + // Animation behaviors + Behavior on scale { + NumberAnimation { + duration: Style.animationSlow + easing.type: Easing.OutExpo + + } + } + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutQuad + + } + } + + TrayMenu { + id: trayMenu + } } } } diff --git a/Modules/Bar/WiFi.qml b/Modules/Bar/WiFi.qml index f494a76..4148a4a 100644 --- a/Modules/Bar/WiFi.qml +++ b/Modules/Bar/WiFi.qml @@ -29,11 +29,18 @@ NIconButton { wifiMenuLoader.isLoaded = true } if (wifiMenuLoader.item) { - wifiMenuLoader.item.visible = !wifiMenuLoader.item.visible if (wifiMenuLoader.item.visible) { - network.onMenuOpened() + // Panel is visible, hide it with animation + if (wifiMenuLoader.item.hide) { + wifiMenuLoader.item.hide() + } else { + wifiMenuLoader.item.visible = false + network.onMenuClosed() + } } else { - network.onMenuClosed() + // Panel is hidden, show it + wifiMenuLoader.item.visible = true + network.onMenuOpened() } } } diff --git a/Modules/Bar/WiFiMenu.qml b/Modules/Bar/WiFiMenu.qml index 6431007..21d2af5 100644 --- a/Modules/Bar/WiFiMenu.qml +++ b/Modules/Bar/WiFiMenu.qml @@ -18,11 +18,47 @@ NLoader { property string passwordInput: "" property bool showPasswordPrompt: false + function hide() { + wifiMenuRect.scaleValue = 0.8 + wifiMenuRect.opacityValue = 0.0 + + hideTimer.start() + } + + // Connect to NPanel's dismissed signal to handle external close events Connections { target: wifiPanel ignoreUnknownSignals: true function onDismissed() { + // Start hide animation + wifiMenuRect.scaleValue = 0.8 + wifiMenuRect.opacityValue = 0.0 + + // Hide after animation completes + hideTimer.start() + } + } + + // Also handle visibility changes from external sources + onVisibleChanged: { + if (!visible && wifiMenuRect.opacityValue > 0) { + // Start hide animation + wifiMenuRect.scaleValue = 0.8 + wifiMenuRect.opacityValue = 0.0 + + // Hide after animation completes + hideTimer.start() + } + } + + // Timer to hide panel after animation + Timer { + id: hideTimer + interval: Style.animationSlow + repeat: false + onTriggered: { wifiPanel.visible = false + wifiPanel.dismissed() network.onMenuClosed() } } @@ -30,6 +66,7 @@ NLoader { WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand Rectangle { + id: wifiMenuRect color: Colors.backgroundSecondary radius: Style.radiusMedium * scaling border.color: Colors.backgroundTertiary @@ -41,6 +78,34 @@ NLoader { anchors.topMargin: Style.marginTiny * scaling anchors.rightMargin: Style.marginTiny * scaling + // Animation properties + property real scaleValue: 0.8 + property real opacityValue: 0.0 + + scale: scaleValue + opacity: opacityValue + + // Animate in when component is completed + Component.onCompleted: { + scaleValue = 1.0 + opacityValue = 1.0 + } + + // Animation behaviors + Behavior on scale { + NumberAnimation { + duration: Style.animationSlow + easing.type: Easing.OutExpo + } + } + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutQuad + } + } + ColumnLayout { anchors.fill: parent anchors.margins: Style.marginLarge * scaling @@ -87,8 +152,7 @@ NLoader { icon: "close" sizeMultiplier: 0.8 onClicked: { - wifiPanel.visible = false - network.onMenuClosed() + wifiPanel.hide() } } } diff --git a/Modules/Calendar/Calendar.qml b/Modules/Calendar/Calendar.qml index fba498f..532ea02 100644 --- a/Modules/Calendar/Calendar.qml +++ b/Modules/Calendar/Calendar.qml @@ -15,7 +15,54 @@ NLoader { readonly property real scaling: Scaling.scale(screen) + // Override hide function to animate first + function hide() { + // Start hide animation + calendarRect.scaleValue = 0.8 + calendarRect.opacityValue = 0.0 + + // Hide after animation completes + hideTimer.start() + } + + // Connect to NPanel's dismissed signal to handle external close events + Connections { + target: calendarPanel + function onDismissed() { + // Start hide animation + calendarRect.scaleValue = 0.8 + calendarRect.opacityValue = 0.0 + + // Hide after animation completes + hideTimer.start() + } + } + + // Also handle visibility changes from external sources + onVisibleChanged: { + if (!visible && calendarRect.opacityValue > 0) { + // Start hide animation + calendarRect.scaleValue = 0.8 + calendarRect.opacityValue = 0.0 + + // Hide after animation completes + hideTimer.start() + } + } + + // Timer to hide panel after animation + Timer { + id: hideTimer + interval: Style.animationSlow + repeat: false + onTriggered: { + calendarPanel.visible = false + calendarPanel.dismissed() + } + } + Rectangle { + id: calendarRect color: Colors.backgroundSecondary radius: Style.radiusMedium * scaling border.color: Colors.backgroundTertiary @@ -27,11 +74,41 @@ NLoader { anchors.topMargin: Style.marginTiny * scaling anchors.rightMargin: Style.marginTiny * scaling + // Animation properties + property real scaleValue: 0.8 + property real opacityValue: 0.0 + + scale: scaleValue + opacity: opacityValue + + // Animate in when component is completed + Component.onCompleted: { + scaleValue = 1.0 + opacityValue = 1.0 + } + // Prevent closing when clicking in the panel bg MouseArea { anchors.fill: parent } + // Animation behaviors + Behavior on scale { + NumberAnimation { + duration: Style.animationSlow + easing.type: Easing.OutExpo + + } + } + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutQuad + + } + } + // Main Column ColumnLayout { anchors.fill: parent diff --git a/Modules/Demo/DemoPanel.qml b/Modules/Demo/DemoPanel.qml index 98ba412..744178f 100644 --- a/Modules/Demo/DemoPanel.qml +++ b/Modules/Demo/DemoPanel.qml @@ -15,9 +15,54 @@ NLoader { readonly property real scaling: Scaling.scale(screen) + // Override hide function to animate first + function hide() { + // Start hide animation + bgRect.scaleValue = 0.8 + bgRect.opacityValue = 0.0 + + // Hide after animation completes + hideTimer.start() + } + + // Connect to NPanel's dismissed signal to handle external close events + Connections { + target: demoPanel + function onDismissed() { + // Start hide animation + bgRect.scaleValue = 0.8 + bgRect.opacityValue = 0.0 + + // Hide after animation completes + hideTimer.start() + } + } + + // Also handle visibility changes from external sources + onVisibleChanged: { + if (!visible && bgRect.opacityValue > 0) { + // Start hide animation + bgRect.scaleValue = 0.8 + bgRect.opacityValue = 0.0 + + // Hide after animation completes + hideTimer.start() + } + } + + // Timer to hide panel after animation + Timer { + id: hideTimer + interval: Style.animationSlow + repeat: false + onTriggered: { + demoPanel.visible = false + demoPanel.dismissed() + } + } + // Ensure panel shows itself once created Component.onCompleted: { - console.log("[DemoPanel] Component completed, showing panel") show() } @@ -31,11 +76,41 @@ NLoader { height: 700 * scaling anchors.centerIn: parent + // Animation properties + property real scaleValue: 0.8 + property real opacityValue: 0.0 + + scale: scaleValue + opacity: opacityValue + + // Animate in when component is completed + Component.onCompleted: { + scaleValue = 1.0 + opacityValue = 1.0 + } + // Prevent closing when clicking in the panel bg MouseArea { anchors.fill: parent } + // Animation behaviors + Behavior on scale { + NumberAnimation { + duration: Style.animationSlow + easing.type: Easing.OutExpo + + } + } + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutQuad + + } + } + ColumnLayout { anchors.fill: parent anchors.margins: Style.marginXL * scaling diff --git a/Modules/Settings/SettingsPanel.qml b/Modules/Settings/SettingsPanel.qml index 189e969..831d802 100644 --- a/Modules/Settings/SettingsPanel.qml +++ b/Modules/Settings/SettingsPanel.qml @@ -16,6 +16,40 @@ NLoader { readonly property real scaling: Scaling.scale(screen) + // Override hide function to animate first + function hide() { + // Start hide animation + bgRect.scaleValue = 0.8 + bgRect.opacityValue = 0.0 + + // Hide after animation completes + hideTimer.start() + } + + // Connect to NPanel's dismissed signal to handle external close events + Connections { + target: panel + function onDismissed() { + // Start hide animation + bgRect.scaleValue = 0.8 + bgRect.opacityValue = 0.0 + + // Hide after animation completes + hideTimer.start() + } + } + + // Timer to hide panel after animation + Timer { + id: hideTimer + interval: Style.animationSlow + repeat: false + onTriggered: { + panel.visible = false + panel.dismissed() + } + } + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand property int currentTabIndex: 0 @@ -65,9 +99,18 @@ NLoader { "source": "Tabs/About.qml" }] + // Combined visibility change handler onVisibleChanged: { - if (visible) + if (visible) { currentTabIndex = 0 + } else if (bgRect.opacityValue > 0) { + // Start hide animation + bgRect.scaleValue = 0.8 + bgRect.opacityValue = 0.0 + + // Hide after animation completes + hideTimer.start() + } } Component.onCompleted: show() @@ -83,10 +126,40 @@ NLoader { height: (screen.height * 0.5) * scaling anchors.centerIn: parent + // Animation properties + property real scaleValue: 0.8 + property real opacityValue: 0.0 + + scale: scaleValue + opacity: opacityValue + + // Animate in when component is completed + Component.onCompleted: { + scaleValue = 1.0 + opacityValue = 1.0 + } + MouseArea { anchors.fill: parent } + // Animation behaviors + Behavior on scale { + NumberAnimation { + duration: Style.animationSlow + easing.type: Easing.OutExpo + + } + } + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutQuad + + } + } + RowLayout { anchors.fill: parent anchors.margins: Style.marginLarge * scaling @@ -196,7 +269,7 @@ NLoader { tooltipText: "Close" Layout.alignment: Qt.AlignVCenter onClicked: { - settingsPanel.isLoaded = !settingsPanel.isLoaded + panel.hide() } } } diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml index 5b0a5c6..c389c23 100644 --- a/Modules/SidePanel/SidePanel.qml +++ b/Modules/SidePanel/SidePanel.qml @@ -41,6 +41,41 @@ NLoader { // Ensure this panel attaches to the intended screen screen: root.targetScreen + // Override hide function to animate first + function hide() { + // Start hide animation + panelBackground.scaleValue = 0.8 + panelBackground.opacityValue = 0.0 + + // Hide after animation completes + hideTimer.start() + } + + // Connect to NPanel's dismissed signal to handle external close events + Connections { + target: sidePanel + function onDismissed() { + // Start hide animation + panelBackground.scaleValue = 0.8 + panelBackground.opacityValue = 0.0 + + // Hide after animation completes + hideTimer.start() + } + } + + // Also handle visibility changes from external sources + onVisibleChanged: { + if (!visible && panelBackground.opacityValue > 0) { + // Start hide animation + panelBackground.scaleValue = 0.8 + panelBackground.opacityValue = 0.0 + + // Hide after animation completes + hideTimer.start() + } + } + // Ensure panel shows itself once created Component.onCompleted: show() @@ -62,11 +97,54 @@ NLoader { x: Math.max(Style.marginSmall * scaling, Math.min(parent.width - width - Style.marginSmall * scaling, Math.round(anchorX - width / 2))) + // Animation properties + property real scaleValue: 0.8 + property real opacityValue: 0.0 + + scale: scaleValue + opacity: opacityValue + + // Animate in when component is completed + Component.onCompleted: { + scaleValue = 1.0 + opacityValue = 1.0 + } + + + + // Timer to hide panel after animation + Timer { + id: hideTimer + interval: Style.animationSlow + repeat: false + onTriggered: { + sidePanel.visible = false + sidePanel.dismissed() + } + } + // Prevent closing when clicking in the panel bg MouseArea { anchors.fill: parent } + // Animation behaviors + Behavior on scale { + NumberAnimation { + duration: Style.animationSlow + easing.type: Easing.OutExpo + + } + } + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutQuad + + } + } + // Content wrapper to ensure childrenRect drives implicit height Item { id: content diff --git a/Widgets/NTooltip.qml b/Widgets/NTooltip.qml index 361b2ce..401dfde 100644 --- a/Widgets/NTooltip.qml +++ b/Widgets/NTooltip.qml @@ -55,11 +55,23 @@ Window { x = pos.x - width / 2 + target.width / 2 y = pos.y + 12 // 12 px margin below } + + // Start with animation values + tooltipRect.scaleValue = 0.8 + tooltipRect.opacityValue = 0.0 visible = true + + // Use a timer to trigger the animation after the component is visible + showTimer.start() } function _hideNow() { - visible = false + // Start hide animation + tooltipRect.scaleValue = 0.8 + tooltipRect.opacityValue = 0.0 + + // Hide after animation completes + hideTimer.start() } Connections { @@ -97,7 +109,30 @@ Window { } } + // Timer to hide tooltip after animation + Timer { + id: hideTimer + interval: Style.animationNormal + repeat: false + onTriggered: { + visible = false + } + } + + // Timer to trigger show animation + Timer { + id: showTimer + interval: 10 // Very short delay to ensure component is visible + repeat: false + onTriggered: { + // Animate to final values + tooltipRect.scaleValue = 1.0 + tooltipRect.opacityValue = 1.0 + } + } + Rectangle { + id: tooltipRect anchors.fill: parent radius: Style.radiusMedium * scaling gradient: Gradient { @@ -113,16 +148,37 @@ Window { border.color: Colors.outline border.width: Math.max(1, Style.borderThin * scaling) z: 1 - } - NText { - id: tooltipText - anchors.centerIn: parent - text: root.text - font.pointSize: Style.fontSizeMedium * scaling - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - wrapMode: Text.Wrap - z: 1 + // Animation properties + property real scaleValue: 1.0 + property real opacityValue: 1.0 + + scale: scaleValue + opacity: opacityValue + + // Animation behaviors + Behavior on scale { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutExpo + } + } + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutQuad + } + } + + NText { + id: tooltipText + anchors.centerIn: parent + text: root.text + font.pointSize: Style.fontSizeMedium * scaling + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + wrapMode: Text.Wrap + } } } From 5fd0e57b91084e600dfc997edf8008f7591fd3ee Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 13 Aug 2025 09:33:45 -0400 Subject: [PATCH 234/394] Formatting --- Modules/Bar/NotificationHistoryPanel.qml | 8 +++----- Modules/Bar/Tray.qml | 22 ++++++++++------------ Modules/Bar/WiFiMenu.qml | 6 +++--- Modules/Calendar/Calendar.qml | 8 +++----- Modules/Demo/DemoPanel.qml | 8 +++----- Modules/SidePanel/SidePanel.qml | 10 +++------- Widgets/NTooltip.qml | 6 +++--- 7 files changed, 28 insertions(+), 40 deletions(-) diff --git a/Modules/Bar/NotificationHistoryPanel.qml b/Modules/Bar/NotificationHistoryPanel.qml index 5b6fd98..ffd16fa 100644 --- a/Modules/Bar/NotificationHistoryPanel.qml +++ b/Modules/Bar/NotificationHistoryPanel.qml @@ -20,7 +20,7 @@ NLoader { // Start hide animation notificationRect.scaleValue = 0.8 notificationRect.opacityValue = 0.0 - + // Hide after animation completes hideTimer.start() } @@ -32,7 +32,7 @@ NLoader { // Start hide animation notificationRect.scaleValue = 0.8 notificationRect.opacityValue = 0.0 - + // Hide after animation completes hideTimer.start() } @@ -44,7 +44,7 @@ NLoader { // Start hide animation notificationRect.scaleValue = 0.8 notificationRect.opacityValue = 0.0 - + // Hide after animation completes hideTimer.start() } @@ -94,7 +94,6 @@ NLoader { NumberAnimation { duration: Style.animationSlow easing.type: Easing.OutExpo - } } @@ -102,7 +101,6 @@ NLoader { NumberAnimation { duration: Style.animationNormal easing.type: Easing.OutQuad - } } diff --git a/Modules/Bar/Tray.qml b/Modules/Bar/Tray.qml index f74c0bd..4ed0114 100644 --- a/Modules/Bar/Tray.qml +++ b/Modules/Bar/Tray.qml @@ -122,13 +122,13 @@ Item { NPanel { id: trayPanel showOverlay: false // no colors overlay even if activated in settings - + // Override hide function to animate first - function hide() { - // Start hide animation + function hide() { + // Start hide animation trayMenuRect.scaleValue = 0.8 trayMenuRect.opacityValue = 0.0 - + // Hide after animation completes hideTimer.start() } @@ -136,11 +136,11 @@ Item { Connections { target: trayPanel ignoreUnknownSignals: true - function onDismissed() { - // Start hide animation + function onDismissed() { + // Start hide animation trayMenuRect.scaleValue = 0.8 trayMenuRect.opacityValue = 0.0 - + // Hide after animation completes hideTimer.start() } @@ -148,11 +148,11 @@ Item { // Also handle visibility changes from external sources onVisibleChanged: { - if (!visible && trayMenuRect.opacityValue > 0) { - // Start hide animation + if (!visible && trayMenuRect.opacityValue > 0) { + // Start hide animation trayMenuRect.scaleValue = 0.8 trayMenuRect.opacityValue = 0.0 - + // Hide after animation completes hideTimer.start() } @@ -192,7 +192,6 @@ Item { NumberAnimation { duration: Style.animationSlow easing.type: Easing.OutExpo - } } @@ -200,7 +199,6 @@ Item { NumberAnimation { duration: Style.animationNormal easing.type: Easing.OutQuad - } } diff --git a/Modules/Bar/WiFiMenu.qml b/Modules/Bar/WiFiMenu.qml index 21d2af5..7d13663 100644 --- a/Modules/Bar/WiFiMenu.qml +++ b/Modules/Bar/WiFiMenu.qml @@ -21,7 +21,7 @@ NLoader { function hide() { wifiMenuRect.scaleValue = 0.8 wifiMenuRect.opacityValue = 0.0 - + hideTimer.start() } @@ -33,7 +33,7 @@ NLoader { // Start hide animation wifiMenuRect.scaleValue = 0.8 wifiMenuRect.opacityValue = 0.0 - + // Hide after animation completes hideTimer.start() } @@ -45,7 +45,7 @@ NLoader { // Start hide animation wifiMenuRect.scaleValue = 0.8 wifiMenuRect.opacityValue = 0.0 - + // Hide after animation completes hideTimer.start() } diff --git a/Modules/Calendar/Calendar.qml b/Modules/Calendar/Calendar.qml index 532ea02..3feaee8 100644 --- a/Modules/Calendar/Calendar.qml +++ b/Modules/Calendar/Calendar.qml @@ -20,7 +20,7 @@ NLoader { // Start hide animation calendarRect.scaleValue = 0.8 calendarRect.opacityValue = 0.0 - + // Hide after animation completes hideTimer.start() } @@ -32,7 +32,7 @@ NLoader { // Start hide animation calendarRect.scaleValue = 0.8 calendarRect.opacityValue = 0.0 - + // Hide after animation completes hideTimer.start() } @@ -44,7 +44,7 @@ NLoader { // Start hide animation calendarRect.scaleValue = 0.8 calendarRect.opacityValue = 0.0 - + // Hide after animation completes hideTimer.start() } @@ -97,7 +97,6 @@ NLoader { NumberAnimation { duration: Style.animationSlow easing.type: Easing.OutExpo - } } @@ -105,7 +104,6 @@ NLoader { NumberAnimation { duration: Style.animationNormal easing.type: Easing.OutQuad - } } diff --git a/Modules/Demo/DemoPanel.qml b/Modules/Demo/DemoPanel.qml index 744178f..d16bd09 100644 --- a/Modules/Demo/DemoPanel.qml +++ b/Modules/Demo/DemoPanel.qml @@ -20,7 +20,7 @@ NLoader { // Start hide animation bgRect.scaleValue = 0.8 bgRect.opacityValue = 0.0 - + // Hide after animation completes hideTimer.start() } @@ -32,7 +32,7 @@ NLoader { // Start hide animation bgRect.scaleValue = 0.8 bgRect.opacityValue = 0.0 - + // Hide after animation completes hideTimer.start() } @@ -44,7 +44,7 @@ NLoader { // Start hide animation bgRect.scaleValue = 0.8 bgRect.opacityValue = 0.0 - + // Hide after animation completes hideTimer.start() } @@ -99,7 +99,6 @@ NLoader { NumberAnimation { duration: Style.animationSlow easing.type: Easing.OutExpo - } } @@ -107,7 +106,6 @@ NLoader { NumberAnimation { duration: Style.animationNormal easing.type: Easing.OutQuad - } } diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml index c389c23..695d8d8 100644 --- a/Modules/SidePanel/SidePanel.qml +++ b/Modules/SidePanel/SidePanel.qml @@ -46,7 +46,7 @@ NLoader { // Start hide animation panelBackground.scaleValue = 0.8 panelBackground.opacityValue = 0.0 - + // Hide after animation completes hideTimer.start() } @@ -58,7 +58,7 @@ NLoader { // Start hide animation panelBackground.scaleValue = 0.8 panelBackground.opacityValue = 0.0 - + // Hide after animation completes hideTimer.start() } @@ -70,7 +70,7 @@ NLoader { // Start hide animation panelBackground.scaleValue = 0.8 panelBackground.opacityValue = 0.0 - + // Hide after animation completes hideTimer.start() } @@ -110,8 +110,6 @@ NLoader { opacityValue = 1.0 } - - // Timer to hide panel after animation Timer { id: hideTimer @@ -133,7 +131,6 @@ NLoader { NumberAnimation { duration: Style.animationSlow easing.type: Easing.OutExpo - } } @@ -141,7 +138,6 @@ NLoader { NumberAnimation { duration: Style.animationNormal easing.type: Easing.OutQuad - } } diff --git a/Widgets/NTooltip.qml b/Widgets/NTooltip.qml index 401dfde..8f4b8da 100644 --- a/Widgets/NTooltip.qml +++ b/Widgets/NTooltip.qml @@ -55,12 +55,12 @@ Window { x = pos.x - width / 2 + target.width / 2 y = pos.y + 12 // 12 px margin below } - + // Start with animation values tooltipRect.scaleValue = 0.8 tooltipRect.opacityValue = 0.0 visible = true - + // Use a timer to trigger the animation after the component is visible showTimer.start() } @@ -69,7 +69,7 @@ Window { // Start hide animation tooltipRect.scaleValue = 0.8 tooltipRect.opacityValue = 0.0 - + // Hide after animation completes hideTimer.start() } From 207310cd24a7f6e69c6e45afa6cf09aba1c9127a Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 13 Aug 2025 09:34:05 -0400 Subject: [PATCH 235/394] SettingsPanel: no longer using hardcoded ID to focus a tab --- Modules/Bar/Volume.qml | 2 +- Modules/Settings/SettingsPanel.qml | 51 ++++++++++++++++++++--- Modules/SidePanel/Cards/ProfileCard.qml | 2 +- Modules/SidePanel/Cards/UtilitiesCard.qml | 2 +- 4 files changed, 48 insertions(+), 9 deletions(-) diff --git a/Modules/Bar/Volume.qml b/Modules/Bar/Volume.qml index 7d17e8f..456d493 100644 --- a/Modules/Bar/Volume.qml +++ b/Modules/Bar/Volume.qml @@ -84,7 +84,7 @@ Item { } } onClicked: { - settingsPanel.currentTabIndex = 5 // Audio tab index + settingsPanel.requestedTab = settingsPanel.tabsIds.AUDIO settingsPanel.isLoaded = true } } diff --git a/Modules/Settings/SettingsPanel.qml b/Modules/Settings/SettingsPanel.qml index b6f1615..e0d9d30 100644 --- a/Modules/Settings/SettingsPanel.qml +++ b/Modules/Settings/SettingsPanel.qml @@ -10,20 +10,39 @@ import qs.Widgets NLoader { id: root - property int currentTabIndex: 0 + property var tabsIds: null + property var requestedTab: null + + Component.onCompleted: { + // Fill up our ideads + tabsIds = Object.freeze({ + "GENERAL": 0, + "BAR": 1, + "TIME_WEATHER": 2, + "SCREEN_RECORDER": 3, + "NETWORK": 4, + "AUDIO": 5, + "DISPLAY": 6, + "WALLPAPER": 7, + "WALLPAPER_SELECTOR": 8, + "MISC": 9, + "ABOUT": 10 + }) + } content: Component { NPanel { id: panel readonly property real scaling: Scaling.scale(screen) + property int currentTabIndex: 0 // Override hide function to animate first function hide() { // Start hide animation bgRect.scaleValue = 0.8 bgRect.opacityValue = 0.0 - + // Hide after animation completes hideTimer.start() } @@ -35,7 +54,7 @@ NLoader { // Start hide animation bgRect.scaleValue = 0.8 bgRect.opacityValue = 0.0 - + // Hide after animation completes hideTimer.start() } @@ -55,47 +74,58 @@ NLoader { WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand property var tabsModel: [{ + "id": root.tabsIds.GENERAL, "label": "General", "icon": "tune", "source": "Tabs/General.qml" }, { + "id": root.tabsIds.BAR, "label": "Bar", "icon": "web_asset", "source": "Tabs/Bar.qml" }, { + "id": root.tabsIds.TIME_WEATHER, "label": "Time & Weather", "icon": "schedule", "source": "Tabs/TimeWeather.qml" }, { + "id": root.tabsIds.SCREEN_RECORDER, "label": "Screen Recorder", "icon": "videocam", "source": "Tabs/ScreenRecorder.qml" }, { + "id": root.tabsIds.NETWORK, "label": "Network", "icon": "wifi", "source": "Tabs/Network.qml" }, { + "id": root.tabsIds.AUDIO, "label": "Audio", "icon": "volume_up", "source": "Tabs/Audio.qml" }, { + "id": root.tabsIds.DISPLAY, "label": "Display", "icon": "monitor", "source": "Tabs/Display.qml" }, { + "id": root.tabsIds.WALLPAPER, "label": "Wallpaper", "icon": "image", "source": "Tabs/Wallpaper.qml" }, { + "id": root.tabsIds.WALLPAPER_SELECTOR, "label": "Wallpaper Selector", "icon": "wallpaper_slideshow", "source": "Tabs/WallpaperSelector.qml" }, // { + // "id": root.tabsIds.MISC, // "label": "Misc", // "icon": "more_horiz", // "source": "Tabs/Misc.qml" // }, { + "id": root.tabsIds.ABOUT, "label": "About", "icon": "info", "source": "Tabs/About.qml" @@ -104,12 +134,23 @@ NLoader { // Combined visibility change handler onVisibleChanged: { if (visible) { + // Default to first tab currentTabIndex = 0 + + // Find the request tab if necessary + if (requestedTab != null) { + for (var i = 0; i < tabsModel.length; i++) { + if (tabsModel[i].id == requestedTab) { + currentTabIndex = i + break + } + } + } } else if (bgRect.opacityValue > 0) { // Start hide animation bgRect.scaleValue = 0.8 bgRect.opacityValue = 0.0 - + // Hide after animation completes hideTimer.start() } @@ -150,7 +191,6 @@ NLoader { NumberAnimation { duration: Style.animationSlow easing.type: Easing.OutExpo - } } @@ -158,7 +198,6 @@ NLoader { NumberAnimation { duration: Style.animationNormal easing.type: Easing.OutQuad - } } diff --git a/Modules/SidePanel/Cards/ProfileCard.qml b/Modules/SidePanel/Cards/ProfileCard.qml index 3fa605d..81c1f67 100644 --- a/Modules/SidePanel/Cards/ProfileCard.qml +++ b/Modules/SidePanel/Cards/ProfileCard.qml @@ -58,6 +58,7 @@ NBox { icon: "settings" tooltipText: "Open settings" onClicked: { + settingsPanel.requestedTab = settingsPanel.tabsIds.GENERAL settingsPanel.isLoaded = !settingsPanel.isLoaded } } @@ -135,7 +136,6 @@ NBox { uptimeProcess.running = true } - Process { id: shutdownProcess diff --git a/Modules/SidePanel/Cards/UtilitiesCard.qml b/Modules/SidePanel/Cards/UtilitiesCard.qml index 4f6a17b..e40d9ba 100644 --- a/Modules/SidePanel/Cards/UtilitiesCard.qml +++ b/Modules/SidePanel/Cards/UtilitiesCard.qml @@ -31,7 +31,7 @@ NBox { NIconButton { icon: "image" onClicked: { - settingsPanel.currentTabIndex = 8 // Audio tab index + settingsPanel.requestedTab = settingsPanel.tabsIds.WALLPAPER_SELECTOR settingsPanel.isLoaded = true } } From 01dd64dbccad87300bdb8eb3551600bbbc208f6f Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 13 Aug 2025 09:40:44 -0400 Subject: [PATCH 236/394] Moved NotificationHistoryPanel in shell.qml --- Modules/Bar/NotificationHistory.qml | 20 ++++++++------------ shell.qml | 4 ++++ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Modules/Bar/NotificationHistory.qml b/Modules/Bar/NotificationHistory.qml index 89aa863..d4c3b47 100644 --- a/Modules/Bar/NotificationHistory.qml +++ b/Modules/Bar/NotificationHistory.qml @@ -15,25 +15,21 @@ NIconButton { icon: "notifications" tooltipText: "Notification History" onClicked: { - if (!notificationHistoryPanelLoader.active) { - notificationHistoryPanelLoader.isLoaded = true + if (!notificationHistoryPanel.active) { + notificationHistoryPanel.isLoaded = true } - if (notificationHistoryPanelLoader.item) { - if (notificationHistoryPanelLoader.item.visible) { + if (notificationHistoryPanel.item) { + if (notificationHistoryPanel.item.visible) { // Panel is visible, hide it with animation - if (notificationHistoryPanelLoader.item.hide) { - notificationHistoryPanelLoader.item.hide() + if (notificationHistoryPanel.item.hide) { + notificationHistoryPanel.item.hide() } else { - notificationHistoryPanelLoader.item.visible = false + notificationHistoryPanel.item.visible = false } } else { // Panel is hidden, show it - notificationHistoryPanelLoader.item.visible = true + notificationHistoryPanel.item.visible = true } } } - - NotificationHistoryPanel { - id: notificationHistoryPanelLoader - } } diff --git a/shell.qml b/shell.qml index f1e589b..f5f4e3b 100644 --- a/shell.qml +++ b/shell.qml @@ -43,6 +43,10 @@ ShellRoot { id: notification } + NotificationHistoryPanel { + id: notificationHistoryPanel + } + Component.onCompleted: { // Ensure our singleton is created as soon as possible so we start fetching weather asap Location.init() From b4149b2a5dc68e053eb2a4d5166a75effd337181 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Wed, 13 Aug 2025 15:49:32 +0200 Subject: [PATCH 237/394] Add PowerMenu --- Modules/SidePanel/Cards/ProfileCard.qml | 377 +--------------------- Modules/SidePanel/PowerMenu.qml | 400 ++++++++++++++++++++++++ 2 files changed, 407 insertions(+), 370 deletions(-) create mode 100644 Modules/SidePanel/PowerMenu.qml diff --git a/Modules/SidePanel/Cards/ProfileCard.qml b/Modules/SidePanel/Cards/ProfileCard.qml index 81c1f67..23f19df 100644 --- a/Modules/SidePanel/Cards/ProfileCard.qml +++ b/Modules/SidePanel/Cards/ProfileCard.qml @@ -6,6 +6,7 @@ import Quickshell.Io import Quickshell.Widgets import qs.Services import qs.Widgets +import qs.Modules.SidePanel // Header card with avatar, user and quick actions NBox { @@ -66,13 +67,18 @@ NBox { id: powerButton icon: "power_settings_new" onClicked: { - //settingsPanel.isLoaded = !settingsPanel.isLoaded powerMenu.show() } } } } + PowerMenu { + id: powerMenu + anchors.top: powerButton.bottom + anchors.right: powerButton.right + } + // ---------------------------------- // Uptime Timer { @@ -108,376 +114,7 @@ NBox { } } - // ---------------------------------- - // Logout menu - function logout() { - if (WorkspaceManager.isNiri) { - logoutProcessNiri.running = true - } else if (WorkspaceManager.isHyprland) { - logoutProcessHyprland.running = true - } else { - console.warn("No supported compositor detected for logout") - } - } - - function suspend() { - suspendProcess.running = true - } - - function shutdown() { - shutdownProcess.running = true - } - - function reboot() { - rebootProcess.running = true - } - function updateSystemInfo() { uptimeProcess.running = true } - - Process { - id: shutdownProcess - - command: ["shutdown", "-h", "now"] - running: false - } - - Process { - id: rebootProcess - - command: ["reboot"] - running: false - } - - Process { - id: suspendProcess - - command: ["systemctl", "suspend"] - running: false - } - - Process { - id: logoutProcessNiri - - command: ["niri", "msg", "action", "quit", "--skip-confirmation"] - running: false - } - - Process { - id: logoutProcessHyprland - - command: ["hyprctl", "dispatch", "exit"] - running: false - } - - Process { - id: logoutProcess - - command: ["loginctl", "terminate-user", Quickshell.env("USER")] - running: false - } - - NPanel { - id: powerMenu - - anchors.top: powerButton.bottom - anchors.right: powerButton.right - - Rectangle { - width: 160 * scaling - height: 220 * scaling - color: Colors.surface - radius: 8 * scaling - border.color: Colors.outline - border.width: 1 * scaling - visible: true - z: 9999 - anchors.top: parent.top - anchors.right: parent.right - anchors.rightMargin: 32 * scaling - anchors.topMargin: powerButton.y + powerButton.height + 48 * scaling - - // Prevent closing when clicking in the panel bg - MouseArea { - anchors.fill: parent - } - - ColumnLayout { - anchors.fill: parent - anchors.margins: 8 * scaling - spacing: 4 * scaling - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 36 * scaling - radius: 6 * scaling - color: lockButtonArea.containsMouse ? Colors.accentPrimary : "transparent" - - Item { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 12 * scaling - anchors.rightMargin: 12 * scaling - - Row { - id: lockRow - spacing: 8 * scaling - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - - Text { - text: "lock_outline" - font.family: "Material Symbols Outlined" - font.pixelSize: 16 * scaling - color: lockButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary - verticalAlignment: Text.AlignVCenter - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: 1 * scaling - } - - Text { - text: "Lock Screen" - font.pixelSize: 14 * scaling - color: lockButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary - verticalAlignment: Text.AlignVCenter - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: 1 * scaling - } - } - } - - MouseArea { - id: lockButtonArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - lockScreen.locked = true - systemMenu.visible = false - } - } - } - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 36 * scaling - radius: 6 * scaling - color: suspendButtonArea.containsMouse ? Colors.accentPrimary : "transparent" - - Item { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 12 * scaling - anchors.rightMargin: 12 * scaling - - Row { - id: suspendRow - spacing: 8 * scaling - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - - Text { - text: "bedtime" - font.family: "Material Symbols Outlined" - font.pixelSize: 16 * scaling - color: suspendButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary - verticalAlignment: Text.AlignVCenter - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: 1 * scaling - } - - Text { - text: "Suspend" - font.pixelSize: 14 * scaling - color: suspendButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary - verticalAlignment: Text.AlignVCenter - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: 1 * scaling - } - } - } - - MouseArea { - id: suspendButtonArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - suspend() - systemMenu.visible = false - } - } - } - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 36 * scaling - radius: 6 * scaling - color: rebootButtonArea.containsMouse ? Colors.accentPrimary : "transparent" - - Item { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 12 * scaling - anchors.rightMargin: 12 * scaling - - Row { - id: rebootRow - spacing: 8 * scaling - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - - Text { - text: "refresh" - font.family: "Material Symbols Outlined" - font.pixelSize: 16 * scaling - color: rebootButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary - verticalAlignment: Text.AlignVCenter - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: 1 * scaling - } - - Text { - text: "Reboot" - font.pixelSize: 14 * scaling - color: rebootButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary - verticalAlignment: Text.AlignVCenter - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: 1 * scaling - } - } - } - - MouseArea { - id: rebootButtonArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - reboot() - systemMenu.visible = false - } - } - } - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 36 * scaling - radius: 6 * scaling - color: logoutButtonArea.containsMouse ? Colors.accentPrimary : "transparent" - - Item { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 12 * scaling - anchors.rightMargin: 12 * scaling - - Row { - id: logoutRow - spacing: 8 * scaling - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - - Text { - text: "exit_to_app" - font.family: "Material Symbols Outlined" - font.pixelSize: 16 * scaling - color: logoutButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary - verticalAlignment: Text.AlignVCenter - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: 1 * scaling - } - - Text { - text: "Logout" - font.pixelSize: 14 * scaling - color: logoutButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary - verticalAlignment: Text.AlignVCenter - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: 1 * scaling - } - } - } - - MouseArea { - id: logoutButtonArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - logout() - systemMenu.visible = false - } - } - } - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 36 * scaling - radius: 6 * scaling - color: shutdownButtonArea.containsMouse ? Colors.accentPrimary : "transparent" - - Item { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 12 * scaling - anchors.rightMargin: 12 * scaling - - Row { - id: shutdownRow - spacing: 8 * scaling - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - - Text { - text: "power_settings_new" - font.family: "Material Symbols Outlined" - font.pixelSize: 16 * scaling - color: shutdownButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary - verticalAlignment: Text.AlignVCenter - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: 1 * scaling - } - - Text { - text: "Shutdown" - font.pixelSize: 14 * scaling - color: shutdownButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary - verticalAlignment: Text.AlignVCenter - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: 1 * scaling - } - } - } - - MouseArea { - id: shutdownButtonArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - shutdown() - systemMenu.visible = false - } - } - } - } - } - } } diff --git a/Modules/SidePanel/PowerMenu.qml b/Modules/SidePanel/PowerMenu.qml new file mode 100644 index 0000000..9fcdb6d --- /dev/null +++ b/Modules/SidePanel/PowerMenu.qml @@ -0,0 +1,400 @@ +import QtQuick +import QtQuick.Effects +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Widgets +import qs.Services +import qs.Widgets + +NPanel { + id: powerMenu + visible: false + + // Anchors will be set by the parent component + + function show() { + visible = true + } + + function hide() { + visible = false + } + + // Close menu when clicking outside + Connections { + target: Quickshell + function onMousePressed() { + if (powerMenu.visible && !powerMenu.contains(Quickshell.mousePosition)) { + powerMenu.hide() + } + } + } + + Rectangle { + width: 160 * scaling + height: 220 * scaling + color: Colors.surface + radius: 8 * scaling + border.color: Colors.outline + border.width: 1 * scaling + visible: true + z: 9999 + anchors.top: parent.top + anchors.right: parent.right + anchors.rightMargin: 32 * scaling + anchors.topMargin: 86 * scaling + + // Prevent closing when clicking in the panel bg + MouseArea { + anchors.fill: parent + onClicked: { + + // Prevent event bubbling + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 8 * scaling + spacing: 4 * scaling + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 36 * scaling + radius: 6 * scaling + color: lockButtonArea.containsMouse ? Colors.accentPrimary : "transparent" + + Item { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 * scaling + anchors.rightMargin: 12 * scaling + + Row { + id: lockRow + spacing: 8 * scaling + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + Text { + text: "lock_outline" + font.family: "Material Symbols Outlined" + font.pixelSize: 16 * scaling + color: lockButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: 1 * scaling + } + + Text { + text: "Lock Screen" + font.pixelSize: 14 * scaling + color: lockButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: 1 * scaling + } + } + } + + MouseArea { + id: lockButtonArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + // TODO: Implement lock screen functionality + console.log("Lock screen requested") + powerMenu.visible = false + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 36 * scaling + radius: 6 * scaling + color: suspendButtonArea.containsMouse ? Colors.accentPrimary : "transparent" + + Item { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 * scaling + anchors.rightMargin: 12 * scaling + + Row { + id: suspendRow + spacing: 8 * scaling + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + Text { + text: "bedtime" + font.family: "Material Symbols Outlined" + font.pixelSize: 16 * scaling + color: suspendButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: 1 * scaling + } + + Text { + text: "Suspend" + font.pixelSize: 14 * scaling + color: suspendButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: 1 * scaling + } + } + } + + MouseArea { + id: suspendButtonArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + suspend() + powerMenu.visible = false + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 36 * scaling + radius: 6 * scaling + color: rebootButtonArea.containsMouse ? Colors.accentPrimary : "transparent" + + Item { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 * scaling + anchors.rightMargin: 12 * scaling + + Row { + id: rebootRow + spacing: 8 * scaling + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + Text { + text: "refresh" + font.family: "Material Symbols Outlined" + font.pixelSize: 16 * scaling + color: rebootButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: 1 * scaling + } + + Text { + text: "Reboot" + font.pixelSize: 14 * scaling + color: rebootButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: 1 * scaling + } + } + } + + MouseArea { + id: rebootButtonArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + reboot() + powerMenu.visible = false + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 36 * scaling + radius: 6 * scaling + color: logoutButtonArea.containsMouse ? Colors.accentPrimary : "transparent" + + Item { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 * scaling + anchors.rightMargin: 12 * scaling + + Row { + id: logoutRow + spacing: 8 * scaling + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + Text { + text: "exit_to_app" + font.family: "Material Symbols Outlined" + font.pixelSize: 16 * scaling + color: logoutButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: 1 * scaling + } + + Text { + text: "Logout" + font.pixelSize: 14 * scaling + color: logoutButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: 1 * scaling + } + } + } + + MouseArea { + id: logoutButtonArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + logout() + powerMenu.visible = false + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 36 * scaling + radius: 6 * scaling + color: shutdownButtonArea.containsMouse ? Colors.accentPrimary : "transparent" + + Item { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 * scaling + anchors.rightMargin: 12 * scaling + + Row { + id: shutdownRow + spacing: 8 * scaling + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + Text { + text: "power_settings_new" + font.family: "Material Symbols Outlined" + font.pixelSize: 16 * scaling + color: shutdownButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: 1 * scaling + } + + Text { + text: "Shutdown" + font.pixelSize: 14 * scaling + color: shutdownButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: 1 * scaling + } + } + } + + MouseArea { + id: shutdownButtonArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + shutdown() + powerMenu.visible = false + } + } + } + } + } + + // ---------------------------------- + // System functions + function logout() { + if (Workspaces.isNiri) { + logoutProcessNiri.running = true + } else if (Workspaces.isHyprland) { + logoutProcessHyprland.running = true + } else { + console.warn("No supported compositor detected for logout") + } + } + + function suspend() { + suspendProcess.running = true + } + + function shutdown() { + shutdownProcess.running = true + } + + function reboot() { + rebootProcess.running = true + } + + Process { + id: shutdownProcess + + command: ["shutdown", "-h", "now"] + running: false + } + + Process { + id: rebootProcess + + command: ["reboot"] + running: false + } + + Process { + id: suspendProcess + + command: ["systemctl", "suspend"] + running: false + } + + Process { + id: logoutProcessNiri + + command: ["niri", "msg", "action", "quit", "--skip-confirmation"] + running: false + } + + Process { + id: logoutProcessHyprland + + command: ["hyprctl", "dispatch", "exit"] + running: false + } + + Process { + id: logoutProcess + + command: ["loginctl", "terminate-user", Quickshell.env("USER")] + running: false + } +} \ No newline at end of file From d345e3fbc0ec3d918089ca66e2790e6cbe06d0db Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Wed, 13 Aug 2025 15:54:49 +0200 Subject: [PATCH 238/394] Fix WallpaperSelector scrolling --- Modules/Settings/Tabs/WallpaperSelector.qml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Modules/Settings/Tabs/WallpaperSelector.qml b/Modules/Settings/Tabs/WallpaperSelector.qml index 2cc6d94..a31d626 100644 --- a/Modules/Settings/Tabs/WallpaperSelector.qml +++ b/Modules/Settings/Tabs/WallpaperSelector.qml @@ -122,8 +122,11 @@ Item { anchors.fill: parent clip: true model: folderModel + + boundsBehavior: Flickable.StopAtBounds + flickableDirection: Flickable.AutoFlickDirection + interactive: false - // Fixed 5 items per row - more aggressive sizing property int columns: 5 property int itemSize: Math.floor( (width - leftMargin - rightMargin - (4 * Style.marginSmall * scaling)) / columns) From fc493db9062e5df7d84b0aec8f289e4a7e7ad99c Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Wed, 13 Aug 2025 16:15:05 +0200 Subject: [PATCH 239/394] Add Recording indicator to bar --- Modules/Bar/Bar.qml | 16 ++++++++--- Modules/SidePanel/Cards/PowerProfilesCard.qml | 27 ++++++++++++++----- Modules/SidePanel/PowerMenu.qml | 1 - Modules/SidePanel/SidePanel.qml | 1 - 4 files changed, 34 insertions(+), 11 deletions(-) diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 1e588dc..4d7ae42 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -81,12 +81,22 @@ Variants { anchors.verticalCenter: bar.verticalCenter spacing: Style.marginSmall * scaling - Tray { + // Screen Recording Indicator + NIconButton { + id: screenRecordingIndicator + icon: "videocam" + tooltipText: "Screen Recording Active" + sizeMultiplier: 0.8 + showBorder: false + showFilled: ScreenRecorder.isRecording + visible: ScreenRecorder.isRecording anchors.verticalCenter: parent.verticalCenter + onClicked: { + ScreenRecorder.toggleRecording() + } } - // TODO: Notification Icon - NotificationHistory { + Tray { anchors.verticalCenter: parent.verticalCenter } diff --git a/Modules/SidePanel/Cards/PowerProfilesCard.qml b/Modules/SidePanel/Cards/PowerProfilesCard.qml index 6b74b24..4f3c141 100644 --- a/Modules/SidePanel/Cards/PowerProfilesCard.qml +++ b/Modules/SidePanel/Cards/PowerProfilesCard.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell +import Quickshell.Services.UPower import qs.Services import qs.Widgets @@ -10,6 +11,10 @@ NBox { Layout.fillWidth: true Layout.preferredWidth: 1 implicitHeight: powerRow.implicitHeight + Style.marginMedium * 2 * scaling + + // PowerProfiles service + property var powerProfiles: PowerProfiles + RowLayout { id: powerRow anchors.fill: parent @@ -21,23 +26,33 @@ NBox { // Performance NIconButton { icon: "speed" + enabled: powerProfiles.hasPerformanceProfile + opacity: enabled ? 1.0 : 0.3 + showFilled: powerProfiles.profile === PowerProfile.Performance + showBorder: powerProfiles.profile !== PowerProfile.Performance onClicked: { - - /* TODO: hook to power profile */ } + if (powerProfiles.hasPerformanceProfile) { + powerProfiles.profile = PowerProfile.Performance + } + } } // Balanced NIconButton { icon: "balance" + showFilled: powerProfiles.profile === PowerProfile.Balanced + showBorder: powerProfiles.profile !== PowerProfile.Balanced onClicked: { - - /* TODO: hook to power profile */ } + powerProfiles.profile = PowerProfile.Balanced + } } // Eco NIconButton { icon: "eco" + showFilled: powerProfiles.profile === PowerProfile.PowerSaver + showBorder: powerProfiles.profile !== PowerProfile.PowerSaver onClicked: { - - /* TODO: hook to power profile */ } + powerProfiles.profile = PowerProfile.PowerSaver + } } Item { Layout.fillWidth: true diff --git a/Modules/SidePanel/PowerMenu.qml b/Modules/SidePanel/PowerMenu.qml index 9fcdb6d..fc8f96c 100644 --- a/Modules/SidePanel/PowerMenu.qml +++ b/Modules/SidePanel/PowerMenu.qml @@ -50,7 +50,6 @@ NPanel { anchors.fill: parent onClicked: { - // Prevent event bubbling } } diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml index 695d8d8..96de4ce 100644 --- a/Modules/SidePanel/SidePanel.qml +++ b/Modules/SidePanel/SidePanel.qml @@ -15,7 +15,6 @@ NLoader { // Target screen to open on property var targetScreen: null - // Public API to open the panel aligned under a given x coordinate. function openAt(x, screen) { anchorX = x targetScreen = screen From eedea2e65bd3cf3820d08bda48c29ab31efc22ec Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 13 Aug 2025 10:22:17 -0400 Subject: [PATCH 240/394] Harmonized PowerMenu with proper Style.xxx --- Modules/Settings/Tabs/WallpaperSelector.qml | 2 +- Modules/SidePanel/PowerMenu.qml | 116 ++++++++++++-------- 2 files changed, 69 insertions(+), 49 deletions(-) diff --git a/Modules/Settings/Tabs/WallpaperSelector.qml b/Modules/Settings/Tabs/WallpaperSelector.qml index a31d626..3be5c0f 100644 --- a/Modules/Settings/Tabs/WallpaperSelector.qml +++ b/Modules/Settings/Tabs/WallpaperSelector.qml @@ -122,7 +122,7 @@ Item { anchors.fill: parent clip: true model: folderModel - + boundsBehavior: Flickable.StopAtBounds flickableDirection: Flickable.AutoFlickDirection interactive: false diff --git a/Modules/SidePanel/PowerMenu.qml b/Modules/SidePanel/PowerMenu.qml index 9fcdb6d..a82bf14 100644 --- a/Modules/SidePanel/PowerMenu.qml +++ b/Modules/SidePanel/PowerMenu.qml @@ -12,7 +12,6 @@ NPanel { visible: false // Anchors will be set by the parent component - function show() { visible = true } @@ -21,28 +20,29 @@ NPanel { visible = false } - // Close menu when clicking outside - Connections { - target: Quickshell - function onMousePressed() { - if (powerMenu.visible && !powerMenu.contains(Quickshell.mousePosition)) { - powerMenu.hide() - } - } - } - Rectangle { width: 160 * scaling height: 220 * scaling - color: Colors.surface - radius: 8 * scaling + radius: Style.radiusMedium * scaling border.color: Colors.outline - border.width: 1 * scaling + border.width: Math.max(1, Style.borderThin * scaling) + gradient: Gradient { + GradientStop { + position: 0.0 + color: Colors.backgroundTertiary + } + GradientStop { + position: 1.0 + color: Colors.backgroundSecondary + } + } + visible: true z: 9999 + anchors.top: parent.top anchors.right: parent.right - anchors.rightMargin: 32 * scaling + anchors.rightMargin: Style.marginLarge * scaling anchors.topMargin: 86 * scaling // Prevent closing when clicking in the panel bg @@ -56,25 +56,27 @@ NPanel { ColumnLayout { anchors.fill: parent - anchors.margins: 8 * scaling - spacing: 4 * scaling + anchors.margins: Style.marginSmall * scaling + spacing: Style.marginTiny * scaling + // -------------- + // Lock Rectangle { Layout.fillWidth: true Layout.preferredHeight: 36 * scaling - radius: 6 * scaling + radius: Style.radiusSmall * scaling color: lockButtonArea.containsMouse ? Colors.accentPrimary : "transparent" Item { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 12 * scaling - anchors.rightMargin: 12 * scaling + anchors.leftMargin: Style.marginMedium * scaling + anchors.rightMargin: Style.marginMedium * scaling Row { id: lockRow - spacing: 8 * scaling + spacing: Style.marginSmall * scaling anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter @@ -82,7 +84,10 @@ NPanel { Text { text: "lock_outline" font.family: "Material Symbols Outlined" - font.pixelSize: 16 * scaling + font.pointSize: Style.fontSizeLarge * scaling + font.variableAxes: { + "wght": (Font.Normal + Font.Bold) / 2.0 + } color: lockButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter @@ -91,7 +96,6 @@ NPanel { Text { text: "Lock Screen" - font.pixelSize: 14 * scaling color: lockButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter @@ -114,22 +118,24 @@ NPanel { } } + // -------------- + // Suspend Rectangle { Layout.fillWidth: true Layout.preferredHeight: 36 * scaling - radius: 6 * scaling + radius: Style.radiusSmall * scaling color: suspendButtonArea.containsMouse ? Colors.accentPrimary : "transparent" Item { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 12 * scaling - anchors.rightMargin: 12 * scaling + anchors.leftMargin: Style.marginMedium * scaling + anchors.rightMargin: Style.marginMedium * scaling Row { id: suspendRow - spacing: 8 * scaling + spacing: Style.marginSmall * scaling anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter @@ -137,7 +143,10 @@ NPanel { Text { text: "bedtime" font.family: "Material Symbols Outlined" - font.pixelSize: 16 * scaling + font.pointSize: Style.fontSizeLarge * scaling + font.variableAxes: { + "wght": (Font.Normal + Font.Bold) / 2.0 + } color: suspendButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter @@ -146,7 +155,6 @@ NPanel { Text { text: "Suspend" - font.pixelSize: 14 * scaling color: suspendButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter @@ -168,22 +176,24 @@ NPanel { } } + // -------------- + // Reboot Rectangle { Layout.fillWidth: true Layout.preferredHeight: 36 * scaling - radius: 6 * scaling + radius: Style.radiusSmall * scaling color: rebootButtonArea.containsMouse ? Colors.accentPrimary : "transparent" Item { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 12 * scaling - anchors.rightMargin: 12 * scaling + anchors.leftMargin: Style.marginMedium * scaling + anchors.rightMargin: Style.marginMedium * scaling Row { id: rebootRow - spacing: 8 * scaling + spacing: Style.marginSmall * scaling anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter @@ -191,7 +201,10 @@ NPanel { Text { text: "refresh" font.family: "Material Symbols Outlined" - font.pixelSize: 16 * scaling + font.pointSize: Style.fontSizeLarge * scaling + font.variableAxes: { + "wght": (Font.Normal + Font.Bold) / 2.0 + } color: rebootButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter @@ -200,7 +213,6 @@ NPanel { Text { text: "Reboot" - font.pixelSize: 14 * scaling color: rebootButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter @@ -222,22 +234,24 @@ NPanel { } } + // -------------- + // Logout Rectangle { Layout.fillWidth: true Layout.preferredHeight: 36 * scaling - radius: 6 * scaling + radius: Style.radiusSmall * scaling color: logoutButtonArea.containsMouse ? Colors.accentPrimary : "transparent" Item { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 12 * scaling - anchors.rightMargin: 12 * scaling + anchors.leftMargin: Style.marginMedium * scaling + anchors.rightMargin: Style.marginMedium * scaling Row { id: logoutRow - spacing: 8 * scaling + spacing: Style.marginSmall * scaling anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter @@ -245,7 +259,10 @@ NPanel { Text { text: "exit_to_app" font.family: "Material Symbols Outlined" - font.pixelSize: 16 * scaling + font.pointSize: Style.fontSizeLarge * scaling + font.variableAxes: { + "wght": (Font.Normal + Font.Bold) / 2.0 + } color: logoutButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter @@ -254,7 +271,6 @@ NPanel { Text { text: "Logout" - font.pixelSize: 14 * scaling color: logoutButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter @@ -276,22 +292,24 @@ NPanel { } } + // -------------- + // Shutdown Rectangle { Layout.fillWidth: true Layout.preferredHeight: 36 * scaling - radius: 6 * scaling + radius: Style.radiusSmall * scaling color: shutdownButtonArea.containsMouse ? Colors.accentPrimary : "transparent" Item { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 12 * scaling - anchors.rightMargin: 12 * scaling + anchors.leftMargin: Style.marginMedium * scaling + anchors.rightMargin: Style.marginMedium * scaling Row { id: shutdownRow - spacing: 8 * scaling + spacing: Style.marginSmall * scaling anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter @@ -299,7 +317,10 @@ NPanel { Text { text: "power_settings_new" font.family: "Material Symbols Outlined" - font.pixelSize: 16 * scaling + font.pointSize: Style.fontSizeLarge * scaling + font.variableAxes: { + "wght": (Font.Normal + Font.Bold) / 2.0 + } color: shutdownButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter @@ -308,7 +329,6 @@ NPanel { Text { text: "Shutdown" - font.pixelSize: 14 * scaling color: shutdownButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter @@ -397,4 +417,4 @@ NPanel { command: ["loginctl", "terminate-user", Quickshell.env("USER")] running: false } -} \ No newline at end of file +} From c2ff8b175528f428ff700f3e645959a0b7a47dd7 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Wed, 13 Aug 2025 16:23:17 +0200 Subject: [PATCH 241/394] Show NotificationHistory symbol again --- Modules/Bar/Bar.qml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 4d7ae42..afba6fa 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -100,6 +100,10 @@ Variants { anchors.verticalCenter: parent.verticalCenter } + NotificationHistory { + anchors.verticalCenter: parent.verticalCenter + } + WiFi { anchors.verticalCenter: parent.verticalCenter } From b09043e6eed8e8f63d804328551c2508d23b885b Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Wed, 13 Aug 2025 16:38:21 +0200 Subject: [PATCH 242/394] Stop NotificationHistory from clipping, move to Modules/Notification/ --- Modules/{Bar => Notification}/NotificationHistoryPanel.qml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) rename Modules/{Bar => Notification}/NotificationHistoryPanel.qml (98%) diff --git a/Modules/Bar/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml similarity index 98% rename from Modules/Bar/NotificationHistoryPanel.qml rename to Modules/Notification/NotificationHistoryPanel.qml index ffd16fa..0fc0aac 100644 --- a/Modules/Bar/NotificationHistoryPanel.qml +++ b/Modules/Notification/NotificationHistoryPanel.qml @@ -152,12 +152,12 @@ NLoader { Layout.fillHeight: true model: NotificationService.historyModel spacing: Style.marginMedium * scaling - clip: true + clip: false boundsBehavior: Flickable.StopAtBounds delegate: Rectangle { width: notificationList ? (notificationList.width - 20) : 380 * scaling - height: 80 + height: Math.max(80, notificationContent.height + 30) radius: Style.radiusMedium * scaling color: notificationMouseArea.containsMouse ? Colors.accentPrimary : "transparent" @@ -170,6 +170,7 @@ NLoader { // Notification content Column { + id: notificationContent Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter spacing: 5 From 70211db9ff18d932fc6596346306c2ceddac2dac Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 13 Aug 2025 11:54:12 -0400 Subject: [PATCH 243/394] Brightness script --- Bin/brigthness.sh | 223 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100755 Bin/brigthness.sh diff --git a/Bin/brigthness.sh b/Bin/brigthness.sh new file mode 100755 index 0000000..d133302 --- /dev/null +++ b/Bin/brigthness.sh @@ -0,0 +1,223 @@ +#!/bin/bash +# +# Bash script to get/set monitor brightness. +# This script acts as a smart wrapper for 'ddcutil' and the Linux sysfs backlight interface. +# +# It automatically determines whether to use ddcutil (for external monitors) +# or the /sys/class/backlight interface (for internal displays like eDP/LVDS). +# + +# --- Configuration --- +readonly CACHE_PATH="/tmp/ddcutil_detect_cache.txt" +readonly CACHE_TTL=3600 # Cache duration in seconds (1 hour) + +# --- Helper Functions --- + +# Prints an error message to stderr and exits with code 1. +# Usage: fail "Error message" +fail() { + echo "Error: $1" >&2 + exit 1 +} + +# Checks if a monitor name corresponds to an internal display. +# Usage: is_internal "monitor_name" +# Returns 0 (true) if internal, 1 (false) otherwise. +is_internal() { + local monitor_name="$1" + # eDP (embedded DisplayPort) and LVDS are common for laptop panels. + if [[ "$monitor_name" == "eDP"* || "$monitor_name" == "LVDS"* ]]; then + return 0 # Success (is internal) + else + return 1 # Failure (is not internal) + fi +} + +# Finds the corresponding backlight device in /sys/class/backlight for a given +# connector name (e.g., "DP-1"). +# It echoes the device name (e.g., "intel_backlight") on success. +# Usage: get_sysfs_backlight_device "connector_name" +get_sysfs_backlight_device() { + local target_connector="$1" + + for entry in /sys/class/backlight/*; do + # Ensure it's a valid directory entry + [ -d "$entry" ] || continue + + local device_name + device_name=$(basename "$entry") + + # Prioritize nvidia devices if found + if [[ "$device_name" == "nvidia_"* ]]; then + echo "$device_name" + return 0 + fi + + # Follow the symlink to find the graphics card connector + local real_path + if [ -L "$entry/device" ]; then + real_path=$(readlink -f "$entry/device") + # Extract the connector name from a path like .../card0-DP-1/... + # This regex finds "card" + digits + "-", and captures what follows. + if [[ "$real_path" =~ card[0-9]+-([^/]+) ]]; then + local path_connector="${BASH_REMATCH[1]}" + if [[ "$path_connector" == "$target_connector" ]]; then + echo "$device_name" + return 0 + fi + fi + fi + done + + return 1 # Not found +} + +# Retrieves the output of `ddcutil detect`, using a cache to avoid +# repeated slow calls. Echoes the command output. +# Usage: get_ddcutil_detect_output +get_ddcutil_detect_output() { + local use_cache=true + if [ ! -f "$CACHE_PATH" ]; then + use_cache=false + else + local now + now=$(date +%s) + local mtime + mtime=$(stat -c %Y "$CACHE_PATH") + local age=$((now - mtime)) + if (( age > CACHE_TTL )); then + use_cache=false + fi + fi + + if [[ "$use_cache" == true ]]; then + cat "$CACHE_PATH" + else + # Run the command, tee the output to the cache file, and also return it + ddcutil detect 2>/dev/null | tee "$CACHE_PATH" + fi +} + +# Parses the output of `ddcutil detect` to find the display index (e.g., 1) +# for a given monitor connector name (e.g., "DP-1"). +# Echoes the display index on success. +# Usage: get_ddc_index_for_monitor "monitor_name" +get_ddc_index_for_monitor() { + local target_monitor="$1" + local detect_output + detect_output=$(get_ddcutil_detect_output) + + # Check if ddcutil command failed or produced no output + if [ -z "$detect_output" ]; then + return 1 + fi + + local current_display="" + # Use process substitution and a while loop to read line-by-line + # This avoids issues with subshells and variable scope. + while IFS= read -r line; do + # Find lines like "Display 1" and store the number + if [[ "$line" =~ ^Display[[:space:]]+([0-9]+) ]]; then + current_display="${BASH_REMATCH[1]}" + continue + fi + + # Once we have a display number, look for its connector info + if [ -n "$current_display" ]; then + # Look for lines like "DRM connector: card0-DP-1" + if [[ "$line" =~ DRM_?connector:.*card[0-9]+-(${target_monitor}) ]]; then + echo "$current_display" + return 0 + fi + fi + done <<< "$detect_output" + + return 1 # Not found +} + +# --- Main Logic --- + +main() { + # Check for correct number of arguments + if [[ "$#" -lt 2 ]]; then + echo "-1" + exit 1 + fi + + local cmd="$1" + local mon="$2" + local val="${3:-}" # Default to empty if not provided + + if is_internal "$mon"; then + # --- Handle Internal Display (via /sys/class/backlight) --- + local backlight_device + backlight_device=$(get_sysfs_backlight_device "$mon") + if [ -z "$backlight_device" ]; then + echo "-1" # Error: Could not find backlight device + exit 1 + fi + + local brightness_file="/sys/class/backlight/$backlight_device/brightness" + local max_brightness_file="/sys/class/backlight/$backlight_device/max_brightness" + + if [ "$cmd" == "get" ]; then + local current_b + current_b=$(cat "$brightness_file") + local max_b + max_b=$(cat "$max_brightness_file") + # Perform integer arithmetic to scale to 0-100 + echo $((current_b * 100 / max_b)) + + elif [ "$cmd" == "set" ]; then + [[ "$#" -lt 3 ]] && echo "-1" && exit 1 + local max_b + max_b=$(cat "$max_brightness_file") + # Scale the 0-100 value to the device's raw value + local raw_brightness=$((val * max_b / 100)) + # NOTE: Writing here may require root privileges or specific udev rules. + if ! echo "$raw_brightness" > "$brightness_file" 2>/dev/null; then + echo "-1" # Error: Permission denied or other write error + exit 1 + fi + echo "$val" # Echo back the set value on success + else + echo "-1" # Invalid command + exit 1 + fi + + else + # --- Handle External Display (via ddcutil) --- + local display_index + display_index=$(get_ddc_index_for_monitor "$mon") + if [ -z "$display_index" ]; then + echo "-1" # Error: Could not find DDC display index + exit 1 + fi + + if [ "$cmd" == "get" ]; then + # Call ddcutil, parse for "current value = X," + local output + output=$(ddcutil --display "$display_index" getvcp 10 2>/dev/null) + if [[ "$output" =~ current[[:space:]]+value[[:space:]]*=[[:space:]]*([0-9]+) ]]; then + echo "${BASH_REMATCH[1]}" + else + echo "-1" # Error: Could not parse brightness from ddcutil + exit 1 + fi + elif [ "$cmd" == "set" ]; then + [[ "$#" -lt 3 ]] && echo "-1" && exit 1 + if ddcutil --display "$display_index" setvcp 10 "$val" >/dev/null 2>&1; then + echo "$val" # Echo back the set value on success + else + echo "-1" # Error: ddcutil command failed + exit 1 + fi + else + echo "-1" # Invalid command + exit 1 + fi + fi +} + +# Run the main function with all script arguments +main "$@" \ No newline at end of file From 82b9a69f4280298eadd955627d2f70fd43fcd822 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 13 Aug 2025 13:26:53 -0400 Subject: [PATCH 244/394] Settings: really hide the misc tab --- Modules/Settings/SettingsPanel.qml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/Settings/SettingsPanel.qml b/Modules/Settings/SettingsPanel.qml index e0d9d30..c5c8f2b 100644 --- a/Modules/Settings/SettingsPanel.qml +++ b/Modules/Settings/SettingsPanel.qml @@ -96,7 +96,7 @@ NLoader { }, { "id": root.tabsIds.NETWORK, "label": "Network", - "icon": "wifi", + "icon": "lan", "source": "Tabs/Network.qml" }, { "id": root.tabsIds.AUDIO, @@ -258,7 +258,7 @@ NLoader { NText { text: modelData.label color: selected ? Colors.onAccent : (tabItem.hovering ? Colors.onAccent : Colors.textPrimary) - font.pointSize: Style.fontSizeLarge * scaling + font.pointSize: Style.fontSizeMedium * scaling font.weight: Style.fontWeightBold Layout.fillWidth: true } @@ -336,7 +336,7 @@ NLoader { Tabs.Display {} Tabs.Wallpaper {} Tabs.WallpaperSelector {} - Tabs.Misc {} + //Tabs.Misc {} Tabs.About {} } } From ddde4b30c458516313bea9df48c592ef5df29701 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 13 Aug 2025 15:56:27 -0400 Subject: [PATCH 245/394] NRadioButton: New widget --- Widgets/NRadioButton.qml | 44 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 Widgets/NRadioButton.qml diff --git a/Widgets/NRadioButton.qml b/Widgets/NRadioButton.qml new file mode 100644 index 0000000..06e9098 --- /dev/null +++ b/Widgets/NRadioButton.qml @@ -0,0 +1,44 @@ +import QtQuick +import QtQuick.Controls +import qs.Services +import qs.Widgets + +RadioButton { + id: root + + indicator: Rectangle { + id: outerCircle + + implicitWidth: 20 * scaling + implicitHeight: 20 * scaling + radius: width * 0.5 + color: "transparent" + border.color: root.checked ? Colors.accentPrimary : Colors.textPrimary + border.width: 2 + anchors.verticalCenter: parent.verticalCenter + + Rectangle { + anchors.centerIn: parent + implicitWidth: Style.marginSmall * scaling + implicitHeight: Style.marginSmall * scaling + + radius: width * 0.5 + color: Qt.alpha(Colors.accentPrimary, root.checked ? 1 : 0) + } + + Behavior on border.color { + ColorAnimation { + duration: Style.animationNormal + easing.type: Easing.InQuad + } + } + } + + contentItem: NText { + text: root.text + font.pointSize: Style.fontSizeMedium * scaling + anchors.verticalCenter: parent.verticalCenter + anchors.left: outerCircle.right + anchors.leftMargin: Style.marginSmall * scaling + } +} From b9eb31c6d415ac078041f6f39eaf9d101e82f9d9 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 13 Aug 2025 15:58:08 -0400 Subject: [PATCH 246/394] Audio - Moved all input/output logic out of UI into the Audio service - Removed volume overdrive which does not work and is out of scope - Reworked the audio input/output selector with radio buttons instead of NComboBox - Hacked a bit the main volume slider so it does not crash PW --- Modules/Bar/Volume.qml | 38 +-- Modules/Settings/Tabs/Audio.qml | 299 ++++++++---------- Modules/SidePanel/Cards/PowerProfilesCard.qml | 4 +- Services/Audio.qml | 33 +- Services/Settings.qml | 1 - Services/Time.qml | 2 +- 6 files changed, 172 insertions(+), 205 deletions(-) diff --git a/Modules/Bar/Volume.qml b/Modules/Bar/Volume.qml index 456d493..0bdc9c8 100644 --- a/Modules/Bar/Volume.qml +++ b/Modules/Bar/Volume.qml @@ -21,35 +21,7 @@ Item { if (Audio.muted) { return "volume_off" } - return Audio.volume === 0 ? "volume_off" : (Audio.volume < 0.33 ? "volume_down" : "volume_up") - } - - function getIconColor() { - return (getDisplayVolume() <= 1.0) ? Colors.textPrimary : getVolumeColor() - } - - function getVolumeColor() { - if (getDisplayVolume() <= 1.0) { - return Colors.accentPrimary - } - - // Indicate that the volume is over 100% - // Calculate interpolation factor (0 at 100%, 1.0 at 200%) - let factor = (getDisplayVolume() - 1.0) - - // Blend between accent and warning colors - return Qt.rgba(Colors.accentPrimary.r + (Colors.error.r - Colors.accentPrimary.r) * factor, - Colors.accentPrimary.g + (Colors.error.g - Colors.accentPrimary.g) * factor, - Colors.accentPrimary.b + (Colors.error.b - Colors.accentPrimary.b) * factor, 1) - } - - function getDisplayVolume() { - // If volumeOverdrive is false, clamp to 100% - if (!Settings.data.audio || !Settings.data.audio.volumeOverdrive) { - return Math.min(Audio.volume, 1.0) - } - // If volumeOverdrive is true, allow up to 200% - return Math.min(Audio.volume, 2.0) + return Audio.volume <= Number.EPSILON ? "volume_off" : (Audio.volume < 0.33 ? "volume_down" : "volume_up") } // Connection used to open the pill when volume changes @@ -69,12 +41,12 @@ Item { NPill { id: pill icon: getIcon() - iconCircleColor: getVolumeColor() - collapsedIconColor: getIconColor() + iconCircleColor: Colors.accentPrimary + collapsedIconColor: Colors.textPrimary autoHide: true - text: Math.round(getDisplayVolume() * 100) + "%" + text: Math.floor(Audio.volume * 100) + "%" tooltipText: "Volume: " + Math.round( - getDisplayVolume() * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume." + Audio.volume * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume." onWheel: function (angle) { if (angle > 0) { diff --git a/Modules/Settings/Tabs/Audio.qml b/Modules/Settings/Tabs/Audio.qml index 3f783bb..49c4d03 100644 --- a/Modules/Settings/Tabs/Audio.qml +++ b/Modules/Settings/Tabs/Audio.qml @@ -10,6 +10,16 @@ import qs.Services ColumnLayout { id: root + property real localVolume: Audio.volume + + // Connection used to open the pill when volume changes + Connections { + target: Audio.sink?.audio ? Audio.sink?.audio : null + function onVolumeChanged() { + localVolume = Audio.volume + } + } + spacing: 0 ScrollView { @@ -54,54 +64,55 @@ ColumnLayout { spacing: Style.marginSmall * scaling Layout.fillWidth: true - NText { - text: "Master Volume" - font.weight: Style.fontWeightBold - color: Colors.textPrimary - } + ColumnLayout { + spacing: Style.marginTiniest * scaling - NText { - text: "System-wide volume level" - font.pointSize: Style.fontSizeSmall * scaling - color: Colors.textSecondary - wrapMode: Text.WordWrap - Layout.fillWidth: true - } + NText { + text: "Master Volume" + font.weight: Style.fontWeightBold + color: Colors.textPrimary + } + NText { + text: "System-wide volume level" + font.pointSize: Style.fontSizeSmall * scaling + color: Colors.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } RowLayout { + // Pipewire seems a bit finicky, if we spam too many volume changes it breaks easily + // Probably because they have some quick fades in and out to avoid clipping + // We use a timer to space out the updates, to avoid lock up + Timer { + interval: 100 + running: true + repeat: true + onTriggered: { + if (Math.abs(localVolume - Audio.volume) >= 0.01) { + Audio.volumeSet(localVolume) + } + } + } + NSlider { - id: masterVolumeSlider Layout.fillWidth: true from: 0 - to: allowOverdrive.value ? 200 : 100 - value: (Audio.volume || 0) * 100 - stepSize: 5 - onValueChanged: { - Audio.volumeSet(value / 100) + to: Settings.data.audio.volumeOverdrive ? 2.0 : 1.0 + value: localVolume + stepSize: 0.01 + onMoved: { + localVolume = value } } NText { - text: Math.round(masterVolumeSlider.value) + "%" + text: Math.floor(Audio.volume * 100) + "%" Layout.alignment: Qt.AlignVCenter color: Colors.textSecondary } } - - NToggle { - id: allowOverdrive - label: "Allow Volume Overdrive" - description: "Enable volume levels above 100% (up to 200%)" - value: Settings.data.audio ? Settings.data.audio.volumeOverdrive : false - onToggled: function (checked) { - Settings.data.audio.volumeOverdrive = checked - - // If overdrive is disabled and current volume is above 100%, cap it - if (!checked && Audio.volume > 1.0) { - Audio.volumeSet(1.0) - } - } - } } // Mute Toggle @@ -142,165 +153,123 @@ ColumnLayout { Layout.bottomMargin: Style.marginSmall * scaling } - // Output Device - NComboBox { - id: outputDeviceCombo - label: "Output Device" - description: "Default audio output device" - optionsKeys: outputDeviceKeys - optionsLabels: outputDeviceLabels - currentKey: Audio.sink ? Audio.sink.id.toString() : "" - onSelected: function (key) { - // Find the node by ID and set it as preferred - for (var i = 0; i < Pipewire.nodes.count; i++) { - let node = Pipewire.nodes.get(i) - if (node.id.toString() === key && node.isSink) { - Pipewire.preferredDefaultAudioSink = node - break - } - } - } + // ------------------------------- + // Output Devices + ButtonGroup { + id: sinks } - // Input Device - NComboBox { - id: inputDeviceCombo - label: "Input Device" - description: "Default audio input device" - optionsKeys: inputDeviceKeys - optionsLabels: inputDeviceLabels - currentKey: Audio.source ? Audio.source.id.toString() : "" - onSelected: function (key) { - // Find the node by ID and set it as preferred - for (var i = 0; i < Pipewire.nodes.count; i++) { - let node = Pipewire.nodes.get(i) - if (node.id.toString() === key && !node.isSink) { - Pipewire.preferredDefaultAudioSource = node - break - } + ColumnLayout { + spacing: Style.marginTiniest * scaling + Layout.fillWidth: true + Layout.bottomMargin: Style.marginLarge * scaling + + NText { + text: "Output Device" + font.pointSize: Style.fontSizeMedium * scaling + font.weight: Style.fontWeightBold + color: Colors.textPrimary + } + + NText { + text: "Select the desired audio output device" + font.pointSize: Style.fontSizeSmall * scaling + color: Colors.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + Repeater { + model: Audio.sinks + NRadioButton { + required property PwNode modelData + ButtonGroup.group: sinks + checked: Audio.sink?.id === modelData.id + onClicked: Audio.setAudioSink(modelData) + text: modelData.description } } } } - // Divider - NDivider { - Layout.fillWidth: true - Layout.topMargin: Style.marginLarge * scaling - Layout.bottomMargin: Style.marginMedium * scaling + // ------------------------------- + // Input Devices + ButtonGroup { + id: sources } - // Audio Visualizer Category ColumnLayout { - spacing: Style.marginSmall * scaling + spacing: Style.marginTiniest * scaling Layout.fillWidth: true + Layout.bottomMargin: Style.marginLarge * scaling NText { - text: "Audio Visualizer" - font.pointSize: Style.fontSizeXL * scaling + text: "Input Device" + font.pointSize: Style.fontSizeMedium * scaling font.weight: Style.fontWeightBold color: Colors.textPrimary - Layout.bottomMargin: Style.marginSmall * scaling } - // Audio Visualizer section - NComboBox { - id: audioVisualizerCombo - label: "Visualization Type" - description: "Choose a visualization type for media playback" - optionsKeys: ["radial", "bars", "wave"] - optionsLabels: ["Radial", "Bars", "Wave"] - currentKey: Settings.data.audio ? Settings.data.audio.audioVisualizer.type : "radial" - onSelected: function (key) { - if (!Settings.data.audio) { - Settings.data.audio = {} - } - if (!Settings.data.audio.audioVisualizer) { - Settings.data.audio.audioVisualizer = {} - } - Settings.data.audio.audioVisualizer.type = key + NText { + text: "Select desired audio input device" + font.pointSize: Style.fontSizeSmall * scaling + color: Colors.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + Repeater { + model: Audio.sources + NRadioButton { + required property PwNode modelData + ButtonGroup.group: sources + checked: Audio.source?.id === modelData.id + onClicked: Audio.setAudioSource(modelData) + text: modelData.description } } } } - } - } - // Device list properties - property var outputDeviceKeys: ["default"] - property var outputDeviceLabels: ["Default Output"] - property var inputDeviceKeys: ["default"] - property var inputDeviceLabels: ["Default Input"] - - // Bind Pipewire nodes - PwObjectTracker { - id: nodeTracker - objects: [Pipewire.nodes] - } - - // Update device lists when component is completed - Component.onCompleted: { - updateDeviceLists() - } - - // Timer to check if pipewire is ready and update device lists - Timer { - id: deviceUpdateTimer - interval: 100 - repeat: true - running: !(Pipewire && Pipewire.ready) - onTriggered: { - if (Pipewire && Pipewire.ready) { - updateDeviceLists() - running = false + // Divider + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginLarge * scaling + Layout.bottomMargin: Style.marginMedium * scaling } - } - } - // Update device lists when nodes change - Connections { - target: nodeTracker - function onObjectsChanged() { - updateDeviceLists() - } - } + // Audio Visualizer Category + ColumnLayout { + spacing: Style.marginSmall * scaling + Layout.fillWidth: true - Repeater { - id: nodesRepeater - model: Pipewire.nodes - delegate: Item { - Component.onCompleted: { - if (modelData && modelData.isSink && modelData.audio) { - // Add to output devices - let key = modelData.id.toString() - if (!outputDeviceKeys.includes(key)) { - outputDeviceKeys.push(key) - outputDeviceLabels.push(modelData.description || modelData.name || "Unknown Device") - } - } else if (modelData && !modelData.isSink && modelData.audio) { - // Add to input devices - let key = modelData.id.toString() - if (!inputDeviceKeys.includes(key)) { - inputDeviceKeys.push(key) - inputDeviceLabels.push(modelData.description || modelData.name || "Unknown Device") + NText { + text: "Audio Visualizer" + font.pointSize: Style.fontSizeXL * scaling + font.weight: Style.fontWeightBold + color: Colors.textPrimary + Layout.bottomMargin: Style.marginSmall * scaling + } + + // Audio Visualizer section + NComboBox { + id: audioVisualizerCombo + label: "Visualization Type" + description: "Choose a visualization type for media playback" + optionsKeys: ["radial", "bars", "wave"] + optionsLabels: ["Radial", "Bars", "Wave"] + currentKey: Settings.data.audio ? Settings.data.audio.audioVisualizer.type : "radial" + onSelected: function (key) { + if (!Settings.data.audio) { + Settings.data.audio = {} + } + if (!Settings.data.audio.audioVisualizer) { + Settings.data.audio.audioVisualizer = {} + } + Settings.data.audio.audioVisualizer.type = key } } } } } - - function updateDeviceLists() { - if (Pipewire && Pipewire.ready) { - // Update comboboxes - if (outputDeviceCombo) { - outputDeviceCombo.optionsKeys = outputDeviceKeys - outputDeviceCombo.optionsLabels = outputDeviceLabels - } - - if (inputDeviceCombo) { - inputDeviceCombo.optionsKeys = inputDeviceKeys - inputDeviceCombo.optionsLabels = inputDeviceLabels - } - } - } } diff --git a/Modules/SidePanel/Cards/PowerProfilesCard.qml b/Modules/SidePanel/Cards/PowerProfilesCard.qml index 4f3c141..e5cc02a 100644 --- a/Modules/SidePanel/Cards/PowerProfilesCard.qml +++ b/Modules/SidePanel/Cards/PowerProfilesCard.qml @@ -11,10 +11,10 @@ NBox { Layout.fillWidth: true Layout.preferredWidth: 1 implicitHeight: powerRow.implicitHeight + Style.marginMedium * 2 * scaling - + // PowerProfiles service property var powerProfiles: PowerProfiles - + RowLayout { id: powerRow anchors.fill: parent diff --git a/Services/Audio.qml b/Services/Audio.qml index ea88d63..b006ae2 100644 --- a/Services/Audio.qml +++ b/Services/Audio.qml @@ -8,8 +8,24 @@ import Quickshell.Services.Pipewire Singleton { id: root + readonly property var nodes: Pipewire.nodes.values.reduce((acc, node) => { + if (!node.isStream) { + if (node.isSink) { + acc.sinks.push(node) + } else if (node.audio) { + acc.sources.push(node) + } + } + return acc + }, { + "sources": [], + "sinks": [] + }) + readonly property PwNode sink: Pipewire.defaultAudioSink readonly property PwNode source: Pipewire.defaultAudioSource + readonly property list sinks: nodes.sinks + readonly property list sources: nodes.sources // Volume [0..1] is readonly from outside readonly property alias volume: root._volume @@ -29,15 +45,26 @@ Singleton { } function volumeSet(newVolume) { - // Clamp volume to 200% if (sink?.ready && sink?.audio) { + // Clamp it accordingly sink.audio.muted = false - sink.audio.volume = Math.max(0, Math.min(2, newVolume)) + sink.audio.volume = Math.max(0, Math.min(1, newVolume)) + //console.log("[Audio] volumeSet", sink.audio.volume); + } else { + console.warn("[Audio] No sink available") } } + function setAudioSink(newSink: PwNode): void { + Pipewire.preferredDefaultAudioSink = newSink + } + + function setAudioSource(newSource: PwNode): void { + Pipewire.preferredDefaultAudioSource = newSource + } + PwObjectTracker { - objects: [Pipewire.defaultAudioSink, Pipewire.nodes] + objects: [...root.sinks, ...root.sources] } Connections { diff --git a/Services/Settings.qml b/Services/Settings.qml index 301714a..3b0cbbb 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -173,7 +173,6 @@ Singleton { property JsonObject audio audio: JsonObject { - property bool volumeOverdrive: false property JsonObject audioVisualizer audioVisualizer: JsonObject { diff --git a/Services/Time.qml b/Services/Time.qml index 7a2463c..4748192 100644 --- a/Services/Time.qml +++ b/Services/Time.qml @@ -40,7 +40,7 @@ Singleton { // Returns a Unix Timestamp (in seconds) readonly property int timestamp: { - return Math.floor(Date.now() / 1000) + return Math.floor(date / 1000) } From 73580bcc0471664de84ef1cbb139b70128310c89 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 13 Aug 2025 16:05:00 -0400 Subject: [PATCH 247/394] Moved Cava to Services/ --- {Modules/Audio => Services}/Cava.qml | 6 ++++-- Services/MediaPlayer.qml | 1 - shell.qml | 3 +-- 3 files changed, 5 insertions(+), 5 deletions(-) rename {Modules/Audio => Services}/Cava.qml (99%) diff --git a/Modules/Audio/Cava.qml b/Services/Cava.qml similarity index 99% rename from Modules/Audio/Cava.qml rename to Services/Cava.qml index d0c53b4..fb7f221 100644 --- a/Modules/Audio/Cava.qml +++ b/Services/Cava.qml @@ -3,8 +3,10 @@ import Quickshell import Quickshell.Io import qs.Services -Scope { +Singleton { id: root + + property var values: Array(count).fill(0) property int count: 44 property int noiseReduction: 60 property string channels: "mono" @@ -29,7 +31,7 @@ Scope { } }) - property var values: Array(count).fill(0) + Process { id: process diff --git a/Services/MediaPlayer.qml b/Services/MediaPlayer.qml index cae8464..c193d6a 100644 --- a/Services/MediaPlayer.qml +++ b/Services/MediaPlayer.qml @@ -4,7 +4,6 @@ import QtQuick import Quickshell import Quickshell.Services.Mpris import qs.Services -import qs.Modules.Audio Singleton { id: root diff --git a/shell.qml b/shell.qml index f5f4e3b..631fb13 100644 --- a/shell.qml +++ b/shell.qml @@ -4,8 +4,6 @@ import Quickshell import Quickshell.Io import Quickshell.Widgets import Quickshell.Services.Pipewire -import qs.Widgets -import qs.Modules.Audio import qs.Modules.Bar import qs.Modules.Calendar import qs.Modules.Demo @@ -14,6 +12,7 @@ import qs.Modules.SidePanel import qs.Modules.Notification import qs.Modules.Settings import qs.Services +import qs.Widgets ShellRoot { id: shellRoot From ee326a72ae65ec34059456eeb323882be329d11c Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 13 Aug 2025 16:31:11 -0400 Subject: [PATCH 248/394] Basic IPC Manager - Not backward compatible --- Services/Cava.qml | 4 +--- Services/IPCManager.qml | 46 +++++++++++++++++++++++++++++++++++++++++ shell.qml | 2 ++ 3 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 Services/IPCManager.qml diff --git a/Services/Cava.qml b/Services/Cava.qml index fb7f221..2e996fa 100644 --- a/Services/Cava.qml +++ b/Services/Cava.qml @@ -5,7 +5,7 @@ import qs.Services Singleton { id: root - + property var values: Array(count).fill(0) property int count: 44 property int noiseReduction: 60 @@ -31,8 +31,6 @@ Singleton { } }) - - Process { id: process property int index: 0 diff --git a/Services/IPCManager.qml b/Services/IPCManager.qml new file mode 100644 index 0000000..f9f8d8f --- /dev/null +++ b/Services/IPCManager.qml @@ -0,0 +1,46 @@ +import QtQuick +import Quickshell.Io + +Item { + id: root + + IpcHandler { + target: "settings" + + function toggle() { + settingsPanel.isLoaded = !settingsPanel.isLoaded + } + } + + IpcHandler { + target: "notifications" + + function toggleHistory() { + notificationHistoryPanel.isLoaded = !notificationHistoryPanel.isLoaded + } + + function toggleDoNotDisturb() {// TODO + } + } + + IpcHandler { + target: "idleInhibitor" + + function toggle() {// TODO + } + } + + IpcHandler { + target: "appLauncher" + + function toggle() {// TODO + } + } + + IpcHandler { + target: "lockScreen" + + function toggle() {// TODO + } + } +} diff --git a/shell.qml b/shell.qml index 631fb13..84d7daf 100644 --- a/shell.qml +++ b/shell.qml @@ -46,6 +46,8 @@ ShellRoot { id: notificationHistoryPanel } + IPCManager {} + Component.onCompleted: { // Ensure our singleton is created as soon as possible so we start fetching weather asap Location.init() From ade52f50138271851ad756f5bc23e02de4bae313 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 13 Aug 2025 16:38:38 -0400 Subject: [PATCH 249/394] Audio Service: more abstraction --- Modules/Bar/Volume.qml | 8 ++--- Modules/Settings/Tabs/Audio.qml | 2 +- Services/Audio.qml | 64 ++++++++++++++++++--------------- 3 files changed, 39 insertions(+), 35 deletions(-) diff --git a/Modules/Bar/Volume.qml b/Modules/Bar/Volume.qml index 0bdc9c8..412a196 100644 --- a/Modules/Bar/Volume.qml +++ b/Modules/Bar/Volume.qml @@ -10,10 +10,6 @@ Item { width: pill.width height: pill.height - Component.onCompleted: { - console.log("[Volume] settingsPanel received:", !!settingsPanel) - } - // Used to avoid opening the pill on Quickshell startup property bool firstVolumeReceived: false @@ -50,9 +46,9 @@ Item { onWheel: function (angle) { if (angle > 0) { - Audio.volumeIncrement() + Audio.increaseVolume() } else if (angle < 0) { - Audio.volumeDecrement() + Audio.decreaseVolume() } } onClicked: { diff --git a/Modules/Settings/Tabs/Audio.qml b/Modules/Settings/Tabs/Audio.qml index 49c4d03..5554aa5 100644 --- a/Modules/Settings/Tabs/Audio.qml +++ b/Modules/Settings/Tabs/Audio.qml @@ -91,7 +91,7 @@ ColumnLayout { repeat: true onTriggered: { if (Math.abs(localVolume - Audio.volume) >= 0.01) { - Audio.volumeSet(localVolume) + Audio.setVolume(localVolume) } } } diff --git a/Services/Audio.qml b/Services/Audio.qml index b006ae2..137dff6 100644 --- a/Services/Audio.qml +++ b/Services/Audio.qml @@ -34,34 +34,7 @@ Singleton { readonly property alias muted: root._muted property bool _muted: !!sink?.audio?.muted - readonly property real step: 0.05 - - function volumeIncrement() { - volumeSet(volume + step) - } - - function volumeDecrement() { - volumeSet(volume - step) - } - - function volumeSet(newVolume) { - if (sink?.ready && sink?.audio) { - // Clamp it accordingly - sink.audio.muted = false - sink.audio.volume = Math.max(0, Math.min(1, newVolume)) - //console.log("[Audio] volumeSet", sink.audio.volume); - } else { - console.warn("[Audio] No sink available") - } - } - - function setAudioSink(newSink: PwNode): void { - Pipewire.preferredDefaultAudioSink = newSink - } - - function setAudioSource(newSource: PwNode): void { - Pipewire.preferredDefaultAudioSource = newSource - } + readonly property real stepVolume: 0.05 PwObjectTracker { objects: [...root.sinks, ...root.sources] @@ -83,4 +56,39 @@ Singleton { console.log("[Audio] onMuteChanged:", root._muted) } } + + function increaseVolume() { + setVolume(volume + stepVolume) + } + + function decreaseVolume() { + setVolume(volume - stepVolume) + } + + function setVolume(newVolume: real) { + if (sink?.ready && sink?.audio) { + // Clamp it accordingly + sink.audio.muted = false + sink.audio.volume = Math.max(0, Math.min(1, newVolume)) + //console.log("[Audio] setVolume", sink.audio.volume); + } else { + console.warn("[Audio] No sink available") + } + } + + function setMuted(muted: bool) { + if (sink?.ready && sink?.audio) { + sink.audio.muted = muted + } else { + console.warn("[Audio] No sink available") + } + } + + function setAudioSink(newSink: PwNode): void { + Pipewire.preferredDefaultAudioSink = newSink + } + + function setAudioSource(newSource: PwNode): void { + Pipewire.preferredDefaultAudioSource = newSource + } } From a1b6c0845dee9ff0b7e71883b7328a39456858a0 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 13 Aug 2025 17:22:42 -0400 Subject: [PATCH 250/394] Replace some hardcoded spacing by their respective shorthands --- Modules/Bar/Tray.qml | 4 ++-- Modules/Bar/TrayMenu.qml | 8 ++++---- Modules/Demo/DemoPanel.qml | 4 ++-- Modules/Settings/Tabs/About.qml | 10 +++++----- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Modules/Bar/Tray.qml b/Modules/Bar/Tray.qml index 4ed0114..e35a7ac 100644 --- a/Modules/Bar/Tray.qml +++ b/Modules/Bar/Tray.qml @@ -32,8 +32,8 @@ Item { IconImage { id: trayIcon anchors.centerIn: parent - width: 16 * scaling - height: 16 * scaling + width: Style.marginLarge * scaling + height: Style.marginLarge * scaling smooth: false asynchronous: true backer.fillMode: Image.PreserveAspectFit diff --git a/Modules/Bar/TrayMenu.qml b/Modules/Bar/TrayMenu.qml index 75073d6..b7dc336 100644 --- a/Modules/Bar/TrayMenu.qml +++ b/Modules/Bar/TrayMenu.qml @@ -134,8 +134,8 @@ PopupWindow { } Image { - Layout.preferredWidth: 16 * scaling - Layout.preferredHeight: 16 * scaling + Layout.preferredWidth: Style.marginLarge * scaling + Layout.preferredHeight: Style.marginLarge * scaling source: modelData?.icon ?? "" visible: (modelData?.icon ?? "") !== "" fillMode: Image.PreserveAspectFit @@ -369,8 +369,8 @@ PopupWindow { } Image { - Layout.preferredWidth: 16 * scaling - Layout.preferredHeight: 16 * scaling + Layout.preferredWidth: Style.marginLarge * scaling + Layout.preferredHeight: Style.marginLarge * scaling source: modelData?.icon ?? "" visible: (modelData?.icon ?? "") !== "" fillMode: Image.PreserveAspectFit diff --git a/Modules/Demo/DemoPanel.qml b/Modules/Demo/DemoPanel.qml index d16bd09..799fbc6 100644 --- a/Modules/Demo/DemoPanel.qml +++ b/Modules/Demo/DemoPanel.qml @@ -127,7 +127,7 @@ NLoader { // NSlider ColumnLayout { - spacing: 16 * scaling + spacing: Style.marginLarge * scaling NText { text: "Scaling" color: Colors.accentSecondary @@ -170,7 +170,7 @@ NLoader { // NIconButton ColumnLayout { - spacing: 16 * scaling + spacing: Style.marginLarge * scaling NText { text: "NIconButton" color: Colors.accentSecondary diff --git a/Modules/Settings/Tabs/About.qml b/Modules/Settings/Tabs/About.qml index 574f219..88f143c 100644 --- a/Modules/Settings/Tabs/About.qml +++ b/Modules/Settings/Tabs/About.qml @@ -69,7 +69,7 @@ ColumnLayout { font.pointSize: 14 * Scaling.scale(screen) color: Colors.textSecondary Layout.alignment: Qt.AlignCenter - Layout.bottomMargin: 16 * Scaling.scale(screen) + Layout.bottomMargin: Style.marginLarge * scaling.scale(screen) } GridLayout { @@ -80,28 +80,28 @@ ColumnLayout { NText { text: "Latest Version:" - font.pointSize: 16 * Scaling.scale(screen) + font.pointSize: Style.marginLarge * scaling.scale(screen) color: Colors.textSecondary Layout.alignment: Qt.AlignRight } NText { text: root.latestVersion - font.pointSize: 16 * Scaling.scale(screen) + font.pointSize: Style.marginLarge * scaling.scale(screen) color: Colors.textPrimary font.weight: Style.fontWeightBold } NText { text: "Installed Version:" - font.pointSize: 16 * Scaling.scale(screen) + font.pointSize: Style.marginLarge * scaling.scale(screen) color: Colors.textSecondary Layout.alignment: Qt.AlignRight } NText { text: root.currentVersion - font.pointSize: 16 * Scaling.scale(screen) + font.pointSize: Style.marginLarge * scaling.scale(screen) color: Colors.textPrimary font.weight: Style.fontWeightBold } From d4eff975663c11f0dbf7ec18fc583d4d5ace17f7 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 13 Aug 2025 18:04:32 -0400 Subject: [PATCH 251/394] MediaPlayer/Card WIP --- Modules/SidePanel/Cards/MediaCard.qml | 619 ++++++++++++++++++++++++-- Services/Cava.qml | 29 +- Services/MediaPlayer.qml | 19 +- 3 files changed, 611 insertions(+), 56 deletions(-) diff --git a/Modules/SidePanel/Cards/MediaCard.qml b/Modules/SidePanel/Cards/MediaCard.qml index 6142b4c..0902abc 100644 --- a/Modules/SidePanel/Cards/MediaCard.qml +++ b/Modules/SidePanel/Cards/MediaCard.qml @@ -7,41 +7,604 @@ import qs.Widgets NBox { id: root - readonly property real scaling: Scaling.scale(screen) - Layout.fillWidth: true + Layout.fillHeight: true + // Let content dictate the height (no hardcoded height here) // Height can be overridden by parent layout (SidePanel binds it to stats card) - implicitHeight: content.implicitHeight + Style.marginLarge * 2 * scaling + //implicitHeight: content.implicitHeight + Style.marginLarge * 2 * scaling + // Component.onCompleted: { + // console.log(MediaPlayer.trackArtUrl) + // } + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginLarge * scaling - Column { - id: content - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: Style.marginMedium * scaling - spacing: Style.marginMedium * scaling + ColumnLayout { + id: fallback + visible: !main.visible + spacing: Style.marginMedium * scaling - Item { - height: Style.marginLarge * scaling + Item { + Layout.fillWidth: true + } + NText { + text: "music_note" + font.family: "Material Symbols Outlined" + font.pointSize: 28 * scaling + color: Colors.textSecondary + Layout.alignment: Qt.AlignHCenter + } + NText { + text: "No media player detected" + color: Colors.textDisabled + Layout.alignment: Qt.AlignHCenter + } + Item { + Layout.fillWidth: true + } } - Text { - text: "music_note" - font.family: "Material Symbols Outlined" - font.pointSize: 28 * scaling - color: Colors.textSecondary - anchors.horizontalCenter: parent.horizontalCenter - } - NText { - text: "No music player detected" - color: Colors.textSecondary - horizontalAlignment: Text.AlignHCenter - anchors.horizontalCenter: parent.horizontalCenter - } + ColumnLayout { + id: main - Item { - height: Style.marginLarge * scaling + visible: MediaPlayer.currentPlayer + spacing: Style.marginMedium * scaling + + RowLayout { + spacing: Style.marginMedium * scaling + + // Rounded thumbnail image + Rectangle { + + width: 90 * scaling + height: 90 * scaling + radius: width * 0.5 + color: trackArt.visible ? Colors.accentPrimary : "transparent" + border.color: trackArt.visible ? Colors.outline : "transparent" + border.width: Math.max(1, Style.borderThin * scaling) + clip: true + + NImageRounded { + id: trackArt + visible: MediaPlayer.trackArtUrl.toString() !== "" + + anchors.fill: parent + anchors.margins: Style.marginTiny * scaling + imagePath: MediaPlayer.trackArtUrl + fallbackIcon: "image" + borderColor: Colors.outline + borderWidth: Math.max(1, Style.borderThin * scaling) + imageRadius: width * 0.5 + } + + // Fallback icon when no album art available + NText { + anchors.centerIn: parent + text: "album" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeLarge * 12 * scaling + visible: !trackArt.visible + } + } + + // ------------------------- + // Track metadata + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginTiny * scaling + + NText { + visible: MediaPlayer.trackTitle !== "" + text: MediaPlayer.trackTitle + font.pointSize: Style.fontSizeMedium * scaling + font.weight: Style.fontWeightBold + elide: Text.ElideRight + wrapMode: Text.Wrap + maximumLineCount: 2 + Layout.fillWidth: true + } + + NText { + visible: MediaPlayer.trackArtist !== "" + text: MediaPlayer.trackArtist + color: Colors.textSecondary + font.pointSize: Style.fontSizeSmall * scaling + elide: Text.ElideRight + Layout.fillWidth: true + } + + NText { + visible: MediaPlayer.trackAlbum !== "" + text: MediaPlayer.trackAlbum + color: Colors.textSecondary + font.pointSize: Style.fontSizeSmall * scaling + elide: Text.ElideRight + Layout.fillWidth: true + } + } + } + + // ------------------------- + // Progress bar + Rectangle { + id: progressBarBackground + width: parent.width + height: 4 * scaling + radius: Style.radiusSmall * scaling + color: Colors.backgroundTertiary + Layout.fillWidth: true + + property real progressRatio: { + if (!MediaPlayer.currentPlayer || !MediaPlayer.isPlaying || MediaPlayer.trackLength <= 0) { + return 0 + } + return Math.min(1, MediaPlayer.currentPosition / MediaPlayer.trackLength) + } + + Rectangle { + id: progressFill + width: progressBarBackground.progressRatio * parent.width + height: parent.height + radius: parent.radius + color: Colors.accentPrimary + + Behavior on width { + NumberAnimation { + duration: 200 + } + } + } + + // Interactive progress handle + Rectangle { + id: progressHandle + width: 16 * scaling + height: 16 * scaling + radius: width * 0.5 + color: Colors.accentPrimary + border.color: Colors.backgroundPrimary + border.width: Math.max(1 * Style.borderMedium * scaling) + + x: Math.max(0, Math.min(parent.width - width, progressFill.width - width / 2)) + anchors.verticalCenter: parent.verticalCenter + + visible: MediaPlayer.trackLength > 0 + scale: progressMouseArea.containsMouse || progressMouseArea.pressed ? 1.2 : 1.0 + + Behavior on scale { + NumberAnimation { + duration: 150 + } + } + } + + // Mouse area for seeking + MouseArea { + id: progressMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + enabled: MediaPlayer.trackLength > 0 && MediaPlayer.canSeek + + onClicked: function (mouse) { + let ratio = mouse.x / width + MediaPlayer.seekByRatio(ratio) + } + + onPositionChanged: function (mouse) { + if (pressed) { + let ratio = Math.max(0, Math.min(1, mouse.x / width)) + MediaPlayer.seekByRatio(ratio) + } + } + } + } + + // ------------------------- + // Media controls + RowLayout { + spacing: Style.marginMedium * scaling + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + + // Previous button + NIconButton { + icon: "skip_previous" + onClicked: MediaPlayer.canGoPrevious ? MediaPlayer.previous() : {} + } + + // Play/Pause button + NIconButton { + icon: MediaPlayer.isPlaying ? "pause" : "play_arrow" + onClicked: (MediaPlayer.canPlay || MediaPlayer.canPause) ? MediaPlayer.playPause() : {} + } + + // Next button + NIconButton { + icon: "skip_next" + onClicked: MediaPlayer.canGoNext ? MediaPlayer.next() : {} + } + } } } -} +}// import QtQuick// import QtQuick.Controls// import QtQuick.Layouts// import QtQuick.Effects// import qs.Settings// import qs.Components// import qs.Services + +// Rectangle { +// id: musicCard +// color: "transparent" + +// Rectangle { +// id: card +// anchors.fill: parent +// color: Theme.surface +// radius: 18 * scaling + +// // Show fallback UI if no player is available +// Item { +// width: parent.width +// height: parent.height +// visible: !MusicManager.currentPlayer + +// ColumnLayout { +// anchors.centerIn: parent +// spacing: 16 * scaling + +// Text { +// text: "music_note" +// font.family: "Material Symbols Outlined" +// font.pixelSize: Theme.fontSizeHeader * scaling +// color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.3) +// Layout.alignment: Qt.AlignHCenter +// } + +// Text { +// text: MusicManager.hasPlayer ? "No controllable player selected" : "No music player detected" +// color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.6) +// font.family: Theme.fontFamily +// font.pixelSize: Theme.fontSizeSmall * scaling +// Layout.alignment: Qt.AlignHCenter +// } +// } +// } + +// // Main player UI +// ColumnLayout { +// anchors.fill: parent +// anchors.margins: 18 * scaling +// spacing: 12 * scaling +// visible: !!MusicManager.currentPlayer + +// // Player selector +// ComboBox { +// id: playerSelector +// Layout.fillWidth: true +// Layout.preferredHeight: 40 * scaling +// visible: MusicManager.getAvailablePlayers().length > 1 +// model: MusicManager.getAvailablePlayers() +// textRole: "identity" +// currentIndex: MusicManager.selectedPlayerIndex + +// background: Rectangle { +// implicitWidth: 120 * scaling +// implicitHeight: 40 * scaling +// color: Theme.surfaceVariant +// border.color: playerSelector.activeFocus ? Theme.accentPrimary : Theme.outline +// border.width: 1 * scaling +// radius: 16 * scaling +// } + +// contentItem: Text { +// leftPadding: 12 * scaling +// rightPadding: playerSelector.indicator.width + playerSelector.spacing +// text: playerSelector.displayText +// font.pixelSize: 13 * scaling +// color: Theme.textPrimary +// verticalAlignment: Text.AlignVCenter +// elide: Text.ElideRight +// } + +// indicator: Text { +// x: playerSelector.width - width - 12 * scaling +// y: playerSelector.topPadding + (playerSelector.availableHeight - height) / 2 +// text: "arrow_drop_down" +// font.family: "Material Symbols Outlined" +// font.pixelSize: 24 * scaling +// color: Theme.textPrimary +// } + +// popup: Popup { +// y: playerSelector.height +// width: playerSelector.width +// implicitHeight: contentItem.implicitHeight +// padding: 1 * scaling + +// contentItem: ListView { +// clip: true +// implicitHeight: contentHeight +// model: playerSelector.popup.visible ? playerSelector.delegateModel : null +// currentIndex: playerSelector.highlightedIndex + +// ScrollIndicator.vertical: ScrollIndicator {} +// } + +// background: Rectangle { +// color: Theme.surfaceVariant +// border.color: Theme.outline +// border.width: 1 * scaling +// radius: 16 +// } +// } + +// delegate: ItemDelegate { +// width: playerSelector.width +// contentItem: Text { +// text: modelData.identity +// font.pixelSize: 13 * scaling +// color: Theme.textPrimary +// verticalAlignment: Text.AlignVCenter +// elide: Text.ElideRight +// } +// highlighted: playerSelector.highlightedIndex === index + +// background: Rectangle { +// color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent" +// } +// } + +// onActivated: { +// MusicManager.selectedPlayerIndex = index; +// MusicManager.updateCurrentPlayer(); +// } +// } + +// // Album art with spectrum visualizer +// RowLayout { +// spacing: 12 * scaling +// Layout.fillWidth: true + +// // Album art container with circular spectrum overlay +// Item { +// id: albumArtContainer +// width: 96 * scaling +// height: 96 * scaling // enough for spectrum and art (will adjust if needed) +// Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + +// // Circular spectrum visualizer around album art +// CircularSpectrum { +// id: spectrum +// values: MusicManager.cavaValues +// anchors.centerIn: parent +// innerRadius: 30 * scaling // Position just outside 60x60 album art +// outerRadius: 48 * scaling // Extend bars outward from album art +// fillColor: Theme.accentPrimary +// strokeColor: Theme.accentPrimary +// strokeWidth: 0 * scaling +// z: 0 +// } + +// // Album art image +// Rectangle { +// id: albumArtwork +// width: 60 * scaling +// height: 60 * scaling +// anchors.centerIn: parent +// radius: width * 0.5 +// color: Qt.darker(Theme.surface, 1.1) +// border.color: Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.3) +// border.width: 1 * scaling + +// Image { +// id: albumArt +// anchors.fill: parent +// anchors.margins: 2 * scaling +// fillMode: Image.PreserveAspectCrop +// smooth: true +// mipmap: true +// cache: false +// asynchronous: true +// sourceSize.width: 60 * scaling +// sourceSize.height: 60 * scaling +// source: MusicManager.trackArtUrl +// visible: source.toString() !== "" + +// // Apply circular mask for rounded corners +// layer.enabled: true +// layer.effect: MultiEffect { +// maskEnabled: true +// maskSource: mask +// } +// } + +// Item { +// id: mask + +// anchors.fill: albumArt +// layer.enabled: true +// visible: false + +// Rectangle { +// width: albumArt.width +// height: albumArt.height +// radius: albumArt.width / 2 // circle +// } +// } + +// // Fallback icon when no album art available +// Text { +// anchors.centerIn: parent +// text: "album" +// font.family: "Material Symbols Outlined" +// font.pixelSize: Theme.fontSizeBody * scaling +// color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.4) +// visible: !albumArt.visible +// } +// } +// } + +// // Progress bar +// Rectangle { +// id: progressBarBackground +// width: parent.width +// height: 6 * scaling +// radius: 3 +// color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.15) +// Layout.fillWidth: true + +// property real progressRatio: { +// if (!MusicManager.currentPlayer || !MusicManager.isPlaying || MusicManager.trackLength <= 0) { +// return 0; +// } +// return Math.min(1, MusicManager.currentPosition / MusicManager.trackLength); +// } + +// Rectangle { +// id: progressFill +// width: progressBarBackground.progressRatio * parent.width +// height: parent.height +// radius: parent.radius +// color: Theme.accentPrimary + +// Behavior on width { +// NumberAnimation { +// duration: 200 +// } +// } +// } + +// // Interactive progress handle +// Rectangle { +// id: progressHandle +// width: 12 * scaling +// height: 12 * scaling +// radius: width * 0.5 +// color: Theme.accentPrimary +// border.color: Qt.lighter(Theme.accentPrimary, 1.3) +// border.width: 1 * scaling + +// x: Math.max(0, Math.min(parent.width - width, progressFill.width - width / 2)) +// anchors.verticalCenter: parent.verticalCenter + +// visible: MusicManager.trackLength > 0 +// scale: progressMouseArea.containsMouse || progressMouseArea.pressed ? 1.2 : 1.0 + +// Behavior on scale { +// NumberAnimation { +// duration: 150 +// } +// } +// } + +// // Mouse area for seeking +// MouseArea { +// id: progressMouseArea +// anchors.fill: parent +// hoverEnabled: true +// cursorShape: Qt.PointingHandCursor +// enabled: MusicManager.trackLength > 0 && MusicManager.canSeek + +// onClicked: function (mouse) { +// let ratio = mouse.x / width; +// MusicManager.seekByRatio(ratio); +// } + +// onPositionChanged: function (mouse) { +// if (pressed) { +// let ratio = Math.max(0, Math.min(1, mouse.x / width)); +// MusicManager.seekByRatio(ratio); +// } +// } +// } +// } + +// // Media controls +// RowLayout { +// spacing: 4 * scaling +// Layout.fillWidth: true +// Layout.alignment: Qt.AlignHCenter + +// // Previous button +// Rectangle { +// width: 28 * scaling +// height: 28 * scaling +// radius: width * 0.5 +// color: previousButton.containsMouse ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.2) : Qt.darker(Theme.surface, 1.1) +// border.color: Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.3) +// border.width: 1 * scaling + +// MouseArea { +// id: previousButton +// anchors.fill: parent +// hoverEnabled: true +// cursorShape: Qt.PointingHandCursor +// enabled: MusicManager.canGoPrevious +// onClicked: MusicManager.previous() +// } + +// Text { +// anchors.centerIn: parent +// text: "skip_previous" +// font.family: "Material Symbols Outlined" +// font.pixelSize: Theme.fontSizeCaption * scaling +// color: previousButton.enabled ? Theme.accentPrimary : Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.3) +// } +// } + +// // Play/Pause button +// Rectangle { +// width: 36 * scaling +// height: 36 * scaling +// radius: width * 0.5 +// color: playButton.containsMouse ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.2) : Qt.darker(Theme.surface, 1.1) +// border.color: Theme.accentPrimary +// border.width: 2 * scaling + +// MouseArea { +// id: playButton +// anchors.fill: parent +// hoverEnabled: true +// cursorShape: Qt.PointingHandCursor +// enabled: MusicManager.canPlay || MusicManager.canPause +// onClicked: MusicManager.playPause() +// } + +// Text { +// anchors.centerIn: parent +// text: MusicManager.isPlaying ? "pause" : "play_arrow" +// font.family: "Material Symbols Outlined" +// font.pixelSize: Theme.fontSizeBody * scaling +// color: playButton.enabled ? Theme.accentPrimary : Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.3) +// } +// } + +// // Next button +// Rectangle { +// width: 28 * scaling +// height: 28 * scaling +// radius: width * 0.5 +// color: nextButton.containsMouse ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.2) : Qt.darker(Theme.surface, 1.1) +// border.color: Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.3) +// border.width: 1 * scaling + +// MouseArea { +// id: nextButton +// anchors.fill: parent +// hoverEnabled: true +// cursorShape: Qt.PointingHandCursor +// enabled: MusicManager.canGoNext +// onClicked: MusicManager.next() +// } + +// Text { +// anchors.centerIn: parent +// text: "skip_next" +// font.family: "Material Symbols Outlined" +// font.pixelSize: Theme.fontSizeCaption * scaling +// color: nextButton.enabled ? Theme.accentPrimary : Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.3) +// } +// } +// } +// } +// } +// } + diff --git a/Services/Cava.qml b/Services/Cava.qml index 2e996fa..78f7050 100644 --- a/Services/Cava.qml +++ b/Services/Cava.qml @@ -1,7 +1,8 @@ +pragma Singleton + import QtQuick import Quickshell import Quickshell.Io -import qs.Services Singleton { id: root @@ -59,19 +60,19 @@ Singleton { stdout: SplitParser { splitMarker: "" onRead: data => { - const newValues = Array(count).fill(0) - for (var i = 0; i < values.length; i++) { - newValues[i] = values[i] - } - if (process.index + data.length > count) { - process.index = 0 - } - for (var i = 0; i < data.length; i += 1) { - newValues[process.index] = Math.min(data.charCodeAt(i), 128) / 128 - process.index = (process.index + 1) % count - } - values = newValues - } + const newValues = Array(count).fill(0) + for (var i = 0; i < values.length; i++) { + newValues[i] = values[i] + } + if (process.index + data.length > count) { + process.index = 0 + } + for (var i = 0; i < data.length; i += 1) { + newValues[process.index] = Math.min(data.charCodeAt(i), 128) / 128 + process.index = (process.index + 1) % count + } + values = newValues + } } } } diff --git a/Services/MediaPlayer.qml b/Services/MediaPlayer.qml index c193d6a..f809c1c 100644 --- a/Services/MediaPlayer.qml +++ b/Services/MediaPlayer.qml @@ -12,9 +12,9 @@ Singleton { property real currentPosition: 0 property int selectedPlayerIndex: 0 property bool isPlaying: currentPlayer ? currentPlayer.isPlaying : false - property string trackTitle: currentPlayer ? (currentPlayer.trackTitle || "Unknown Track") : "" - property string trackArtist: currentPlayer ? (currentPlayer.trackArtist || "Unknown Artist") : "" - property string trackAlbum: currentPlayer ? (currentPlayer.trackAlbum || "Unknown Album") : "" + property string trackTitle: currentPlayer ? (currentPlayer.trackTitle || "") : "" + property string trackArtist: currentPlayer ? (currentPlayer.trackArtist || "") : "" + property string trackAlbum: currentPlayer ? (currentPlayer.trackAlbum || "") : "" property string trackArtUrl: currentPlayer ? (currentPlayer.trackArtUrl || "") : "" property real trackLength: currentPlayer ? currentPlayer.length : 0 property bool canPlay: currentPlayer ? currentPlayer.canPlay : false @@ -24,13 +24,8 @@ Singleton { property bool canSeek: currentPlayer ? currentPlayer.canSeek : false property bool hasPlayer: getAvailablePlayers().length > 0 - // Expose cava values - property alias cavaValues: cava.values - - Item { - Component.onCompleted: { - updateCurrentPlayer() - } + Component.onCompleted: { + updateCurrentPlayer() } function getAvailablePlayers() { @@ -154,8 +149,4 @@ Singleton { updateCurrentPlayer() } } - - Cava { - id: cava - } } From 484d38a096f976357c5359685faf6323adc235f5 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 13 Aug 2025 18:26:07 -0400 Subject: [PATCH 252/394] MediaCard: Player selector --- Modules/SidePanel/Cards/MediaCard.qml | 134 +++++++++++++++++++++----- 1 file changed, 109 insertions(+), 25 deletions(-) diff --git a/Modules/SidePanel/Cards/MediaCard.qml b/Modules/SidePanel/Cards/MediaCard.qml index 0902abc..63b5780 100644 --- a/Modules/SidePanel/Cards/MediaCard.qml +++ b/Modules/SidePanel/Cards/MediaCard.qml @@ -1,4 +1,5 @@ import QtQuick +import QtQuick.Controls import QtQuick.Layouts import qs.Services import qs.Widgets @@ -51,6 +52,90 @@ NBox { visible: MediaPlayer.currentPlayer spacing: Style.marginMedium * scaling + // Player selector + ComboBox { + id: playerSelector + Layout.fillWidth: true + Layout.preferredHeight: 30 * scaling + visible: MediaPlayer.getAvailablePlayers().length > 1 + model: MediaPlayer.getAvailablePlayers() + textRole: "identity" + currentIndex: MediaPlayer.selectedPlayerIndex + + background: Rectangle { + // implicitWidth: 120 * scaling + // implicitHeight: 30 * scaling + color: "transparent" + border.color: playerSelector.activeFocus ? Colors.hover : Colors.outline + border.width: Math.max(1, Style.borderThin * scaling) + radius: Style.radiusMedium * scaling + } + + contentItem: NText { + leftPadding: Style.marginMedium * scaling + rightPadding: playerSelector.indicator.width + playerSelector.spacing + text: playerSelector.displayText + font.pointSize: Style.fontSizeSmall * scaling + color: Colors.textPrimary + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + indicator: Text { + x: playerSelector.width - width - Style.marginMedium * scaling + y: playerSelector.topPadding + (playerSelector.availableHeight - height) / 2 + text: "arrow_drop_down" + font.family: "Material Symbols Outlined" + font.pointSize: Style.marginXL * scaling + color: Colors.textPrimary + } + + popup: Popup { + y: playerSelector.height + width: playerSelector.width + implicitHeight: Math.min(160 * scaling, contentItem.implicitHeight + Style.marginMedium * scaling * 2) + padding: Style.marginSmall * scaling + + contentItem: ListView { + clip: true + implicitHeight: contentHeight + model: playerSelector.popup.visible ? playerSelector.delegateModel : null + currentIndex: playerSelector.highlightedIndex + ScrollIndicator.vertical: ScrollIndicator {} + } + + background: Rectangle { + color: Colors.backgroundSecondary + border.color: Colors.outline + border.width: Math.max(1, Style.borderThin * scaling) + radius: Style.radiusMedium * scaling + } + } + + delegate: ItemDelegate { + width: playerSelector.width + contentItem: NText { + text: modelData.identity + font.pointSize: Style.fontSizeSmall * scaling + color: highlighted ? Colors.backgroundPrimary : Colors.textPrimary + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + highlighted: playerSelector.highlightedIndex === index + + background: Rectangle { + width: playerSelector.width - Style.marginSmall * scaling * 2 + color: highlighted ? Colors.hover : "transparent" + radius: Style.radiusSmall * scaling + } + } + + onActivated: { + MediaPlayer.selectedPlayerIndex = currentIndex + MediaPlayer.updateCurrentPlayer() + } + } + RowLayout { spacing: Style.marginMedium * scaling @@ -228,8 +313,7 @@ NBox { } } } -}// import QtQuick// import QtQuick.Controls// import QtQuick.Layouts// import QtQuick.Effects// import qs.Settings// import qs.Components// import qs.Services - +} // import QtQuick// import QtQuick.Controls// import QtQuick.Layouts// import QtQuick.Effects// import qs.Settings// import qs.Components// import qs.Services // Rectangle { // id: musicCard // color: "transparent" @@ -244,7 +328,7 @@ NBox { // Item { // width: parent.width // height: parent.height -// visible: !MusicManager.currentPlayer +// visible: !MediaPlayer.currentPlayer // ColumnLayout { // anchors.centerIn: parent @@ -259,7 +343,7 @@ NBox { // } // Text { -// text: MusicManager.hasPlayer ? "No controllable player selected" : "No music player detected" +// text: MediaPlayer.hasPlayer ? "No controllable player selected" : "No music player detected" // color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.6) // font.family: Theme.fontFamily // font.pixelSize: Theme.fontSizeSmall * scaling @@ -273,17 +357,17 @@ NBox { // anchors.fill: parent // anchors.margins: 18 * scaling // spacing: 12 * scaling -// visible: !!MusicManager.currentPlayer +// visible: !!MediaPlayer.currentPlayer // // Player selector // ComboBox { // id: playerSelector // Layout.fillWidth: true // Layout.preferredHeight: 40 * scaling -// visible: MusicManager.getAvailablePlayers().length > 1 -// model: MusicManager.getAvailablePlayers() +// visible: MediaPlayer.getAvailablePlayers().length > 1 +// model: MediaPlayer.getAvailablePlayers() // textRole: "identity" -// currentIndex: MusicManager.selectedPlayerIndex +// currentIndex: MediaPlayer.selectedPlayerIndex // background: Rectangle { // implicitWidth: 120 * scaling @@ -353,8 +437,8 @@ NBox { // } // onActivated: { -// MusicManager.selectedPlayerIndex = index; -// MusicManager.updateCurrentPlayer(); +// MediaPlayer.selectedPlayerIndex = index; +// MediaPlayer.updateCurrentPlayer(); // } // } @@ -373,7 +457,7 @@ NBox { // // Circular spectrum visualizer around album art // CircularSpectrum { // id: spectrum -// values: MusicManager.cavaValues +// values: MediaPlayer.cavaValues // anchors.centerIn: parent // innerRadius: 30 * scaling // Position just outside 60x60 album art // outerRadius: 48 * scaling // Extend bars outward from album art @@ -405,7 +489,7 @@ NBox { // asynchronous: true // sourceSize.width: 60 * scaling // sourceSize.height: 60 * scaling -// source: MusicManager.trackArtUrl +// source: MediaPlayer.trackArtUrl // visible: source.toString() !== "" // // Apply circular mask for rounded corners @@ -452,10 +536,10 @@ NBox { // Layout.fillWidth: true // property real progressRatio: { -// if (!MusicManager.currentPlayer || !MusicManager.isPlaying || MusicManager.trackLength <= 0) { +// if (!MediaPlayer.currentPlayer || !MediaPlayer.isPlaying || MediaPlayer.trackLength <= 0) { // return 0; // } -// return Math.min(1, MusicManager.currentPosition / MusicManager.trackLength); +// return Math.min(1, MediaPlayer.currentPosition / MediaPlayer.trackLength); // } // Rectangle { @@ -485,7 +569,7 @@ NBox { // x: Math.max(0, Math.min(parent.width - width, progressFill.width - width / 2)) // anchors.verticalCenter: parent.verticalCenter -// visible: MusicManager.trackLength > 0 +// visible: MediaPlayer.trackLength > 0 // scale: progressMouseArea.containsMouse || progressMouseArea.pressed ? 1.2 : 1.0 // Behavior on scale { @@ -501,17 +585,17 @@ NBox { // anchors.fill: parent // hoverEnabled: true // cursorShape: Qt.PointingHandCursor -// enabled: MusicManager.trackLength > 0 && MusicManager.canSeek +// enabled: MediaPlayer.trackLength > 0 && MediaPlayer.canSeek // onClicked: function (mouse) { // let ratio = mouse.x / width; -// MusicManager.seekByRatio(ratio); +// MediaPlayer.seekByRatio(ratio); // } // onPositionChanged: function (mouse) { // if (pressed) { // let ratio = Math.max(0, Math.min(1, mouse.x / width)); -// MusicManager.seekByRatio(ratio); +// MediaPlayer.seekByRatio(ratio); // } // } // } @@ -537,8 +621,8 @@ NBox { // anchors.fill: parent // hoverEnabled: true // cursorShape: Qt.PointingHandCursor -// enabled: MusicManager.canGoPrevious -// onClicked: MusicManager.previous() +// enabled: MediaPlayer.canGoPrevious +// onClicked: MediaPlayer.previous() // } // Text { @@ -564,13 +648,13 @@ NBox { // anchors.fill: parent // hoverEnabled: true // cursorShape: Qt.PointingHandCursor -// enabled: MusicManager.canPlay || MusicManager.canPause -// onClicked: MusicManager.playPause() +// enabled: MediaPlayer.canPlay || MediaPlayer.canPause +// onClicked: MediaPlayer.playPause() // } // Text { // anchors.centerIn: parent -// text: MusicManager.isPlaying ? "pause" : "play_arrow" +// text: MediaPlayer.isPlaying ? "pause" : "play_arrow" // font.family: "Material Symbols Outlined" // font.pixelSize: Theme.fontSizeBody * scaling // color: playButton.enabled ? Theme.accentPrimary : Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.3) @@ -591,8 +675,8 @@ NBox { // anchors.fill: parent // hoverEnabled: true // cursorShape: Qt.PointingHandCursor -// enabled: MusicManager.canGoNext -// onClicked: MusicManager.next() +// enabled: MediaPlayer.canGoNext +// onClicked: MediaPlayer.next() // } // Text { From 8a6ac222bb7a35da59aad3897dc101b7cf368ed7 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 13 Aug 2025 18:45:24 -0400 Subject: [PATCH 253/394] Settings: About polishing --- Modules/Settings/Tabs/About.qml | 142 ++++++++++---------------------- 1 file changed, 43 insertions(+), 99 deletions(-) diff --git a/Modules/Settings/Tabs/About.qml b/Modules/Settings/Tabs/About.qml index 88f143c..bd90b27 100644 --- a/Modules/Settings/Tabs/About.qml +++ b/Modules/Settings/Tabs/About.qml @@ -11,7 +11,7 @@ ColumnLayout { id: root property string latestVersion: GitHub.latestVersion - property string currentVersion: "v1.2.1" // Fallback version + property string currentVersion: "v2.0.0" // Fallback version property var contributors: GitHub.contributors spacing: 0 @@ -45,8 +45,8 @@ ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true - padding: 16 - rightPadding: 12 + padding: Style.marginLarge * scaling + rightPadding: Style.marginMedium * scaling clip: true ScrollBar.horizontal.policy: ScrollBar.AlwaysOff ScrollBar.vertical.policy: ScrollBar.AsNeeded @@ -57,51 +57,47 @@ ColumnLayout { NText { text: "Noctalia: quiet by design" - font.pointSize: 24 * Scaling.scale(screen) + font.pointSize: Style.fontSizeXXL * scaling font.weight: Style.fontWeightBold color: Colors.textPrimary Layout.alignment: Qt.AlignCenter - Layout.bottomMargin: 8 * Scaling.scale(screen) + Layout.bottomMargin: Style.marginSmall * scaling } NText { text: "It may just be another quickshell setup but it won't get in your way." - font.pointSize: 14 * Scaling.scale(screen) + font.pointSize: Style.fontSizeMedium * scaling color: Colors.textSecondary Layout.alignment: Qt.AlignCenter - Layout.bottomMargin: Style.marginLarge * scaling.scale(screen) + Layout.bottomMargin: Style.marginLarge * scaling } GridLayout { Layout.alignment: Qt.AlignCenter columns: 2 - rowSpacing: 4 - columnSpacing: 8 + rowSpacing: Style.marginTiny * scaling + columnSpacing: Style.marginSmall * scaling NText { text: "Latest Version:" - font.pointSize: Style.marginLarge * scaling.scale(screen) color: Colors.textSecondary Layout.alignment: Qt.AlignRight } NText { text: root.latestVersion - font.pointSize: Style.marginLarge * scaling.scale(screen) color: Colors.textPrimary font.weight: Style.fontWeightBold } NText { text: "Installed Version:" - font.pointSize: Style.marginLarge * scaling.scale(screen) color: Colors.textSecondary Layout.alignment: Qt.AlignRight } NText { text: root.currentVersion - font.pointSize: Style.marginLarge * scaling.scale(screen) color: Colors.textPrimary font.weight: Style.fontWeightBold } @@ -109,10 +105,10 @@ ColumnLayout { Rectangle { Layout.alignment: Qt.AlignCenter - Layout.topMargin: 8 - Layout.preferredWidth: updateText.implicitWidth + 46 - Layout.preferredHeight: 32 - radius: 20 + Layout.topMargin: Style.marginSmall * scaling + Layout.preferredWidth: updateText.implicitWidth + 46 * scaling + Layout.preferredHeight: 32 * scaling + radius: Style.radiusLarge * scaling color: updateArea.containsMouse ? Colors.accentPrimary : "transparent" border.color: Colors.accentPrimary border.width: 1 @@ -141,15 +137,14 @@ ColumnLayout { NText { text: "system_update" font.family: "Material Symbols Outlined" - font.pointSize: 18 * Scaling.scale(screen) + font.pointSize: 18 * scaling color: updateArea.containsMouse ? Colors.backgroundPrimary : Colors.accentPrimary } NText { id: updateText - text: "Download latest release" - font.pointSize: 14 * Scaling.scale(screen) + font.pointSize: 14 * scaling color: updateArea.containsMouse ? Colors.backgroundPrimary : Colors.accentPrimary } } @@ -166,38 +161,26 @@ ColumnLayout { } } - // Separator - Rectangle { + NDivider { Layout.fillWidth: true - Layout.topMargin: 26 - Layout.bottomMargin: 18 - height: 1 - color: Colors.outline - opacity: 0.3 + Layout.topMargin: Style.marginLarge * 2 * scaling + Layout.bottomMargin: Style.marginLarge * scaling } NText { - text: "Contributors" - font.pointSize: 18 * Scaling.scale(screen) + text: `Contributors: ${root.contributors.length}` + font.pointSize: Style.fontSizeLarge * scaling font.weight: Style.fontWeightBold color: Colors.textPrimary Layout.alignment: Qt.AlignCenter - Layout.topMargin: 32 - } - - NText { - text: "(" + root.contributors.length + ")" - font.pointSize: 14 * Scaling.scale(screen) - color: Colors.textSecondary - Layout.alignment: Qt.AlignCenter - Layout.topMargin: 4 + Layout.topMargin: Style.marginLarge * 2 } ScrollView { Layout.alignment: Qt.AlignCenter - Layout.preferredWidth: 200 * 4 + Layout.preferredWidth: 200 * Style.marginTiny * scaling Layout.fillHeight: true - Layout.topMargin: 16 + Layout.topMargin: Style.marginLarge * scaling clip: true ScrollBar.horizontal.policy: ScrollBar.AlwaysOff ScrollBar.vertical.policy: ScrollBar.AsNeeded @@ -206,86 +189,47 @@ ColumnLayout { id: contributorsGrid anchors.fill: parent - width: 200 * 4 + width: 200 * 4 * scaling height: Math.ceil(root.contributors.length / 4) * 100 - cellWidth: 200 - cellHeight: 100 + cellWidth: 200 * scaling + cellHeight: 100 * scaling model: root.contributors delegate: Rectangle { - width: contributorsGrid.cellWidth - 16 - height: contributorsGrid.cellHeight - 4 - radius: 20 + width: contributorsGrid.cellWidth - Style.marginLarge * scaling + height: contributorsGrid.cellHeight - Style.marginTiny * scaling + radius: Style.radiusLarge * scaling color: contributorArea.containsMouse ? Colors.hover : "transparent" RowLayout { anchors.fill: parent - anchors.margins: 8 - spacing: 12 + anchors.margins: Style.marginSmall * scaling + spacing: Style.marginMedium * scaling Item { Layout.alignment: Qt.AlignVCenter - Layout.preferredWidth: 40 - Layout.preferredHeight: 40 - - Image { - id: avatarImage + Layout.preferredWidth: 64 * scaling + Layout.preferredHeight: 64 * scaling + NImageRounded { + imagePath: modelData.avatar_url || "" anchors.fill: parent - source: modelData.avatar_url || "" - sourceSize: Qt.size(80, 80) - visible: false - mipmap: true - smooth: true - asynchronous: true - fillMode: Image.PreserveAspectCrop - cache: true - - onStatusChanged: { - if (status === Image.Error) { - console.log("[About] Failed to load avatar for", modelData.login, "URL:", modelData.avatar_url) - } - } - } - - MultiEffect { - anchors.fill: parent - source: avatarImage - maskEnabled: true - maskSource: mask - } - - Item { - id: mask - - anchors.fill: parent - layer.enabled: true - visible: false - - Rectangle { - anchors.fill: parent - radius: parent.width / 2 - } - } - - NText { - anchors.centerIn: parent - text: "person" - font.family: "Material Symbols Outlined" - font.pointSize: 24 * Scaling.scale(screen) - color: contributorArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary - visible: !avatarImage.source || avatarImage.status !== Image.Ready + anchors.margins: Style.marginTiny * scaling + fallbackIcon: "person" + borderColor: Colors.accentPrimary + borderWidth: Math.max(1, Style.borderMedium * scaling) + imageRadius: width * 0.5 } } ColumnLayout { - spacing: 4 + spacing: Style.marginTiny * scaling Layout.alignment: Qt.AlignVCenter Layout.fillWidth: true NText { text: modelData.login || "Unknown" - font.pointSize: 13 * Scaling.scale(screen) + font.weight: Style.fontWeightBold color: contributorArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary elide: Text.ElideRight Layout.fillWidth: true @@ -293,7 +237,7 @@ ColumnLayout { NText { text: (modelData.contributions || 0) + " commits" - font.pointSize: 11 * Scaling.scale(screen) + font.pointSize: Style.fontSizeSmall * scaling color: contributorArea.containsMouse ? Colors.backgroundPrimary : Colors.textSecondary } } From 31ae919a7aecc49c225c7bb28a7d1b7c1a57037b Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 13 Aug 2025 22:19:44 -0400 Subject: [PATCH 254/394] Decent cava linear visualizer --- Modules/Audio/CircularSpectrum.qml | 52 ++++ Modules/Audio/LinearSpectrum.qml | 61 ++++ Modules/Settings/Tabs/Audio.qml | 14 +- Modules/SidePanel/Cards/MediaCard.qml | 408 ++------------------------ Services/Cava.qml | 54 ++-- Services/GitHub.qml | 2 +- Services/Settings.qml | 6 +- 7 files changed, 178 insertions(+), 419 deletions(-) create mode 100644 Modules/Audio/CircularSpectrum.qml create mode 100644 Modules/Audio/LinearSpectrum.qml diff --git a/Modules/Audio/CircularSpectrum.qml b/Modules/Audio/CircularSpectrum.qml new file mode 100644 index 0000000..e4e0850 --- /dev/null +++ b/Modules/Audio/CircularSpectrum.qml @@ -0,0 +1,52 @@ +import QtQuick +import qs.Services + +Item { + id: root + property int innerRadius: 32 * scaling + property int outerRadius: 64 * scaling + property color fillColor: Colors.accentPrimary + property color strokeColor: Colors.textPrimary + property int strokeWidth: 0 * scaling + property var values: [] + property int usableOuter: 64 + + width: usableOuter * 2 + height: usableOuter * 2 + + Repeater { + model: root.values.length + Rectangle { + property real value: root.values[index] + property real angle: (index / root.values.length) * 360 + width: Math.max(2 * scaling, (root.innerRadius * 2 * Math.PI) / root.values.length - 4 * scaling) + height: value * (usableOuter - root.innerRadius) + radius: width / 2 + color: root.fillColor + border.color: root.strokeColor + border.width: root.strokeWidth + antialiasing: true + + x: root.width / 2 - width / 2 * Math.cos(Math.PI / 2 + 2 * Math.PI * index / root.values.length) - width / 2 + y: root.height / 2 - height + + transform: [ + Rotation { + origin.x: width / 2 + origin.y: height + //angle: (index / root.values.length) * 360 + }, + Translate { + x: root.innerRadius * Math.cos(2 * Math.PI * index / root.values.length) + y: root.innerRadius * Math.sin(2 * Math.PI * index / root.values.length) + } + ] + + Behavior on height { + SmoothedAnimation { + duration: 120 + } + } + } + } +} diff --git a/Modules/Audio/LinearSpectrum.qml b/Modules/Audio/LinearSpectrum.qml new file mode 100644 index 0000000..ae3e565 --- /dev/null +++ b/Modules/Audio/LinearSpectrum.qml @@ -0,0 +1,61 @@ +import QtQuick +import qs.Services + +Item { + id: root + property color fillColor: Colors.accentPrimary + property color strokeColor: Colors.textPrimary + property int strokeWidth: 0 + property var values: [] + + property real xScale: width / (values.length * 2) + + Repeater { + model: values.length + Rectangle { + property real amp: values[values.length - 1 - index] + + color: fillColor + border.color: strokeColor + border.width: strokeWidth + antialiasing: true + + x: index * xScale + y: root.height - height + + width: xScale * 0.5 + height: root.height * amp + + Behavior on height { + SmoothedAnimation { + duration: 5 + } + } + } + } + + Repeater { + model: values.length + Rectangle { + property real amp: values[index] + + color: fillColor + border.color: strokeColor + border.width: strokeWidth + antialiasing: true + + x: (values.length + index) * xScale + y: root.height - height + + width: xScale * 0.5 + height: root.height * amp + + Behavior on height { + SmoothedAnimation { + duration: 5 + } + } + } + } + +} diff --git a/Modules/Settings/Tabs/Audio.qml b/Modules/Settings/Tabs/Audio.qml index 5554aa5..dc6a052 100644 --- a/Modules/Settings/Tabs/Audio.qml +++ b/Modules/Settings/Tabs/Audio.qml @@ -256,17 +256,11 @@ ColumnLayout { id: audioVisualizerCombo label: "Visualization Type" description: "Choose a visualization type for media playback" - optionsKeys: ["radial", "bars", "wave"] - optionsLabels: ["Radial", "Bars", "Wave"] - currentKey: Settings.data.audio ? Settings.data.audio.audioVisualizer.type : "radial" + optionsKeys: ["none", "linear"] + optionsLabels: ["None", "Linear"] + currentKey: Settings.data.audio.visualizerType onSelected: function (key) { - if (!Settings.data.audio) { - Settings.data.audio = {} - } - if (!Settings.data.audio.audioVisualizer) { - Settings.data.audio.audioVisualizer = {} - } - Settings.data.audio.audioVisualizer.type = key + Settings.data.audio.visualizerType = key } } } diff --git a/Modules/SidePanel/Cards/MediaCard.qml b/Modules/SidePanel/Cards/MediaCard.qml index 63b5780..c3b4266 100644 --- a/Modules/SidePanel/Cards/MediaCard.qml +++ b/Modules/SidePanel/Cards/MediaCard.qml @@ -1,6 +1,8 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Quickshell +import qs.Modules.Audio import qs.Services import qs.Widgets @@ -21,6 +23,7 @@ NBox { anchors.fill: parent anchors.margins: Style.marginLarge * scaling + // Fallback ColumnLayout { id: fallback visible: !main.visible @@ -46,6 +49,7 @@ NBox { } } + // MediaPlayer Main Content ColumnLayout { id: main @@ -312,383 +316,29 @@ NBox { } } } + + Loader { + active: Settings.data.audio.visualizerType == "linear" + Layout.alignment: Qt.AlignHCenter + + sourceComponent: + LinearSpectrum { + width: 300 * scaling + height: 80 * scaling + values: Cava.values + fillColor: Colors.textPrimary + Layout.alignment: Qt.AlignHCenter + } + } + + // CircularSpectrum { + // visible: Settings.data.audio.visualizerType == "radial" + // values: Cava.values + // innerRadius: 30 * scaling // Position just outside 60x60 album art + // outerRadius: 48 * scaling // Extend bars outward from album art + // fillColor: Colors.accentPrimary + // strokeColor: Colors.accentPrimary + // strokeWidth: 0 * scaling + // } } -} // import QtQuick// import QtQuick.Controls// import QtQuick.Layouts// import QtQuick.Effects// import qs.Settings// import qs.Components// import qs.Services -// Rectangle { -// id: musicCard -// color: "transparent" - -// Rectangle { -// id: card -// anchors.fill: parent -// color: Theme.surface -// radius: 18 * scaling - -// // Show fallback UI if no player is available -// Item { -// width: parent.width -// height: parent.height -// visible: !MediaPlayer.currentPlayer - -// ColumnLayout { -// anchors.centerIn: parent -// spacing: 16 * scaling - -// Text { -// text: "music_note" -// font.family: "Material Symbols Outlined" -// font.pixelSize: Theme.fontSizeHeader * scaling -// color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.3) -// Layout.alignment: Qt.AlignHCenter -// } - -// Text { -// text: MediaPlayer.hasPlayer ? "No controllable player selected" : "No music player detected" -// color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.6) -// font.family: Theme.fontFamily -// font.pixelSize: Theme.fontSizeSmall * scaling -// Layout.alignment: Qt.AlignHCenter -// } -// } -// } - -// // Main player UI -// ColumnLayout { -// anchors.fill: parent -// anchors.margins: 18 * scaling -// spacing: 12 * scaling -// visible: !!MediaPlayer.currentPlayer - -// // Player selector -// ComboBox { -// id: playerSelector -// Layout.fillWidth: true -// Layout.preferredHeight: 40 * scaling -// visible: MediaPlayer.getAvailablePlayers().length > 1 -// model: MediaPlayer.getAvailablePlayers() -// textRole: "identity" -// currentIndex: MediaPlayer.selectedPlayerIndex - -// background: Rectangle { -// implicitWidth: 120 * scaling -// implicitHeight: 40 * scaling -// color: Theme.surfaceVariant -// border.color: playerSelector.activeFocus ? Theme.accentPrimary : Theme.outline -// border.width: 1 * scaling -// radius: 16 * scaling -// } - -// contentItem: Text { -// leftPadding: 12 * scaling -// rightPadding: playerSelector.indicator.width + playerSelector.spacing -// text: playerSelector.displayText -// font.pixelSize: 13 * scaling -// color: Theme.textPrimary -// verticalAlignment: Text.AlignVCenter -// elide: Text.ElideRight -// } - -// indicator: Text { -// x: playerSelector.width - width - 12 * scaling -// y: playerSelector.topPadding + (playerSelector.availableHeight - height) / 2 -// text: "arrow_drop_down" -// font.family: "Material Symbols Outlined" -// font.pixelSize: 24 * scaling -// color: Theme.textPrimary -// } - -// popup: Popup { -// y: playerSelector.height -// width: playerSelector.width -// implicitHeight: contentItem.implicitHeight -// padding: 1 * scaling - -// contentItem: ListView { -// clip: true -// implicitHeight: contentHeight -// model: playerSelector.popup.visible ? playerSelector.delegateModel : null -// currentIndex: playerSelector.highlightedIndex - -// ScrollIndicator.vertical: ScrollIndicator {} -// } - -// background: Rectangle { -// color: Theme.surfaceVariant -// border.color: Theme.outline -// border.width: 1 * scaling -// radius: 16 -// } -// } - -// delegate: ItemDelegate { -// width: playerSelector.width -// contentItem: Text { -// text: modelData.identity -// font.pixelSize: 13 * scaling -// color: Theme.textPrimary -// verticalAlignment: Text.AlignVCenter -// elide: Text.ElideRight -// } -// highlighted: playerSelector.highlightedIndex === index - -// background: Rectangle { -// color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent" -// } -// } - -// onActivated: { -// MediaPlayer.selectedPlayerIndex = index; -// MediaPlayer.updateCurrentPlayer(); -// } -// } - -// // Album art with spectrum visualizer -// RowLayout { -// spacing: 12 * scaling -// Layout.fillWidth: true - -// // Album art container with circular spectrum overlay -// Item { -// id: albumArtContainer -// width: 96 * scaling -// height: 96 * scaling // enough for spectrum and art (will adjust if needed) -// Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter - -// // Circular spectrum visualizer around album art -// CircularSpectrum { -// id: spectrum -// values: MediaPlayer.cavaValues -// anchors.centerIn: parent -// innerRadius: 30 * scaling // Position just outside 60x60 album art -// outerRadius: 48 * scaling // Extend bars outward from album art -// fillColor: Theme.accentPrimary -// strokeColor: Theme.accentPrimary -// strokeWidth: 0 * scaling -// z: 0 -// } - -// // Album art image -// Rectangle { -// id: albumArtwork -// width: 60 * scaling -// height: 60 * scaling -// anchors.centerIn: parent -// radius: width * 0.5 -// color: Qt.darker(Theme.surface, 1.1) -// border.color: Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.3) -// border.width: 1 * scaling - -// Image { -// id: albumArt -// anchors.fill: parent -// anchors.margins: 2 * scaling -// fillMode: Image.PreserveAspectCrop -// smooth: true -// mipmap: true -// cache: false -// asynchronous: true -// sourceSize.width: 60 * scaling -// sourceSize.height: 60 * scaling -// source: MediaPlayer.trackArtUrl -// visible: source.toString() !== "" - -// // Apply circular mask for rounded corners -// layer.enabled: true -// layer.effect: MultiEffect { -// maskEnabled: true -// maskSource: mask -// } -// } - -// Item { -// id: mask - -// anchors.fill: albumArt -// layer.enabled: true -// visible: false - -// Rectangle { -// width: albumArt.width -// height: albumArt.height -// radius: albumArt.width / 2 // circle -// } -// } - -// // Fallback icon when no album art available -// Text { -// anchors.centerIn: parent -// text: "album" -// font.family: "Material Symbols Outlined" -// font.pixelSize: Theme.fontSizeBody * scaling -// color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.4) -// visible: !albumArt.visible -// } -// } -// } - -// // Progress bar -// Rectangle { -// id: progressBarBackground -// width: parent.width -// height: 6 * scaling -// radius: 3 -// color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.15) -// Layout.fillWidth: true - -// property real progressRatio: { -// if (!MediaPlayer.currentPlayer || !MediaPlayer.isPlaying || MediaPlayer.trackLength <= 0) { -// return 0; -// } -// return Math.min(1, MediaPlayer.currentPosition / MediaPlayer.trackLength); -// } - -// Rectangle { -// id: progressFill -// width: progressBarBackground.progressRatio * parent.width -// height: parent.height -// radius: parent.radius -// color: Theme.accentPrimary - -// Behavior on width { -// NumberAnimation { -// duration: 200 -// } -// } -// } - -// // Interactive progress handle -// Rectangle { -// id: progressHandle -// width: 12 * scaling -// height: 12 * scaling -// radius: width * 0.5 -// color: Theme.accentPrimary -// border.color: Qt.lighter(Theme.accentPrimary, 1.3) -// border.width: 1 * scaling - -// x: Math.max(0, Math.min(parent.width - width, progressFill.width - width / 2)) -// anchors.verticalCenter: parent.verticalCenter - -// visible: MediaPlayer.trackLength > 0 -// scale: progressMouseArea.containsMouse || progressMouseArea.pressed ? 1.2 : 1.0 - -// Behavior on scale { -// NumberAnimation { -// duration: 150 -// } -// } -// } - -// // Mouse area for seeking -// MouseArea { -// id: progressMouseArea -// anchors.fill: parent -// hoverEnabled: true -// cursorShape: Qt.PointingHandCursor -// enabled: MediaPlayer.trackLength > 0 && MediaPlayer.canSeek - -// onClicked: function (mouse) { -// let ratio = mouse.x / width; -// MediaPlayer.seekByRatio(ratio); -// } - -// onPositionChanged: function (mouse) { -// if (pressed) { -// let ratio = Math.max(0, Math.min(1, mouse.x / width)); -// MediaPlayer.seekByRatio(ratio); -// } -// } -// } -// } - -// // Media controls -// RowLayout { -// spacing: 4 * scaling -// Layout.fillWidth: true -// Layout.alignment: Qt.AlignHCenter - -// // Previous button -// Rectangle { -// width: 28 * scaling -// height: 28 * scaling -// radius: width * 0.5 -// color: previousButton.containsMouse ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.2) : Qt.darker(Theme.surface, 1.1) -// border.color: Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.3) -// border.width: 1 * scaling - -// MouseArea { -// id: previousButton -// anchors.fill: parent -// hoverEnabled: true -// cursorShape: Qt.PointingHandCursor -// enabled: MediaPlayer.canGoPrevious -// onClicked: MediaPlayer.previous() -// } - -// Text { -// anchors.centerIn: parent -// text: "skip_previous" -// font.family: "Material Symbols Outlined" -// font.pixelSize: Theme.fontSizeCaption * scaling -// color: previousButton.enabled ? Theme.accentPrimary : Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.3) -// } -// } - -// // Play/Pause button -// Rectangle { -// width: 36 * scaling -// height: 36 * scaling -// radius: width * 0.5 -// color: playButton.containsMouse ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.2) : Qt.darker(Theme.surface, 1.1) -// border.color: Theme.accentPrimary -// border.width: 2 * scaling - -// MouseArea { -// id: playButton -// anchors.fill: parent -// hoverEnabled: true -// cursorShape: Qt.PointingHandCursor -// enabled: MediaPlayer.canPlay || MediaPlayer.canPause -// onClicked: MediaPlayer.playPause() -// } - -// Text { -// anchors.centerIn: parent -// text: MediaPlayer.isPlaying ? "pause" : "play_arrow" -// font.family: "Material Symbols Outlined" -// font.pixelSize: Theme.fontSizeBody * scaling -// color: playButton.enabled ? Theme.accentPrimary : Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.3) -// } -// } - -// // Next button -// Rectangle { -// width: 28 * scaling -// height: 28 * scaling -// radius: width * 0.5 -// color: nextButton.containsMouse ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.2) : Qt.darker(Theme.surface, 1.1) -// border.color: Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.3) -// border.width: 1 * scaling - -// MouseArea { -// id: nextButton -// anchors.fill: parent -// hoverEnabled: true -// cursorShape: Qt.PointingHandCursor -// enabled: MediaPlayer.canGoNext -// onClicked: MediaPlayer.next() -// } - -// Text { -// anchors.centerIn: parent -// text: "skip_next" -// font.family: "Material Symbols Outlined" -// font.pixelSize: Theme.fontSizeCaption * scaling -// color: nextButton.enabled ? Theme.accentPrimary : Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.3) -// } -// } -// } -// } -// } -// } - +} diff --git a/Services/Cava.qml b/Services/Cava.qml index 78f7050..2f3c069 100644 --- a/Services/Cava.qml +++ b/Services/Cava.qml @@ -7,43 +7,46 @@ import Quickshell.Io Singleton { id: root - property var values: Array(count).fill(0) - property int count: 44 - property int noiseReduction: 60 - property string channels: "mono" - property string monoOption: "average" + property var values: Array(barsCount).fill(0) + property int barsCount: 20 property var config: ({ "general": { - "bars": count, - "framerate": 30, - "autosens": 1 + "bars": barsCount, + "mode": "normal", + "framerate": 60, + "autosens": 0, + "overshoot": 0, + "sensitivity": 200, + "lower_cutoff_freq": 50, + "higher_cutoff_freq": 12000 }, "smoothing": { "monstercat": 1, - "gravity": 1000000, - "noise_reduction": noiseReduction + "gravity": 100, + "noise_reduction": 77 }, "output": { "method": "raw", "bit_format": 8, - "channels": channels, - "mono_option": monoOption + "channels": "mono", + "mono_option": "average" } }) Process { id: process - property int index: 0 + property int fillIndex: 0 stdinEnabled: true running: MediaPlayer.isPlaying command: ["cava", "-p", "/dev/stdin"] onExited: { stdinEnabled = true - index = 0 - values = Array(count).fill(0) + fillIndex = 0 + values = Array(barsCount).fill(0) } onStarted: { + for (const k in config) { if (typeof config[k] !== "object") { write(k + "=" + config[k] + "\n") @@ -56,20 +59,23 @@ Singleton { } } stdinEnabled = false + fillIndex = 0 + values = Array(barsCount).fill(0) } stdout: SplitParser { splitMarker: "" onRead: data => { - const newValues = Array(count).fill(0) - for (var i = 0; i < values.length; i++) { - newValues[i] = values[i] + if (process.fillIndex + data.length >= barsCount) { + process.fillIndex = 0 } - if (process.index + data.length > count) { - process.index = 0 - } - for (var i = 0; i < data.length; i += 1) { - newValues[process.index] = Math.min(data.charCodeAt(i), 128) / 128 - process.index = (process.index + 1) % count + + // copy array + var newValues = values.slice(0) + + for (var i = 0; i < data.length; i++) { + var amp = Math.min(data.charCodeAt(i), 128) / 128 + newValues[process.fillIndex] = amp * amp + process.fillIndex = (process.fillIndex + 1) % barsCount } values = newValues } diff --git a/Services/GitHub.qml b/Services/GitHub.qml index 620a616..1bfac4c 100644 --- a/Services/GitHub.qml +++ b/Services/GitHub.qml @@ -51,7 +51,7 @@ Singleton { function loadFromCache() { const now = Time.timestamp if (!data.timestamp || (now >= data.timestamp + githubUpdateFrequency)) { - console.log("[GitHub] Cache expired or missing, fetching new data from GitHub...") + console.log("[GitHub] Cache expired or missing, fetching new data") fetchFromGitHub() return } diff --git a/Services/Settings.qml b/Services/Settings.qml index 3b0cbbb..9f2ca39 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -173,11 +173,7 @@ Singleton { property JsonObject audio audio: JsonObject { - property JsonObject audioVisualizer - - audioVisualizer: JsonObject { - property string type: "radial" - } + property string visualizerType: "linear" } // ui From ca528695a154522f2718f68b8b8139088b83888c Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 13 Aug 2025 22:40:01 -0400 Subject: [PATCH 255/394] Cava tuning --- Modules/Audio/LinearSpectrum.qml | 17 +++++++---------- Modules/SidePanel/Cards/MediaCard.qml | 3 +-- Services/Cava.qml | 11 +++++------ 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/Modules/Audio/LinearSpectrum.qml b/Modules/Audio/LinearSpectrum.qml index ae3e565..11f2488 100644 --- a/Modules/Audio/LinearSpectrum.qml +++ b/Modules/Audio/LinearSpectrum.qml @@ -8,7 +8,7 @@ Item { property int strokeWidth: 0 property var values: [] - property real xScale: width / (values.length * 2) + readonly property real xScale: width / (values.length * 2) Repeater { model: values.length @@ -20,15 +20,14 @@ Item { border.width: strokeWidth antialiasing: true + width: xScale * 0.5 + height: root.height * amp x: index * xScale y: root.height - height - width: xScale * 0.5 - height: root.height * amp - Behavior on height { SmoothedAnimation { - duration: 5 + duration: 33 } } } @@ -44,18 +43,16 @@ Item { border.width: strokeWidth antialiasing: true + width: xScale * 0.5 + height: root.height * amp x: (values.length + index) * xScale y: root.height - height - width: xScale * 0.5 - height: root.height * amp - Behavior on height { SmoothedAnimation { - duration: 5 + duration: 33 } } } } - } diff --git a/Modules/SidePanel/Cards/MediaCard.qml b/Modules/SidePanel/Cards/MediaCard.qml index c3b4266..0dc0121 100644 --- a/Modules/SidePanel/Cards/MediaCard.qml +++ b/Modules/SidePanel/Cards/MediaCard.qml @@ -321,8 +321,7 @@ NBox { active: Settings.data.audio.visualizerType == "linear" Layout.alignment: Qt.AlignHCenter - sourceComponent: - LinearSpectrum { + sourceComponent: LinearSpectrum { width: 300 * scaling height: 80 * scaling values: Cava.values diff --git a/Services/Cava.qml b/Services/Cava.qml index 2f3c069..5b5728c 100644 --- a/Services/Cava.qml +++ b/Services/Cava.qml @@ -13,8 +13,7 @@ Singleton { property var config: ({ "general": { "bars": barsCount, - "mode": "normal", - "framerate": 60, + "framerate": 40, "autosens": 0, "overshoot": 0, "sensitivity": 200, @@ -22,13 +21,13 @@ Singleton { "higher_cutoff_freq": 12000 }, "smoothing": { - "monstercat": 1, - "gravity": 100, + "monstercat": 0, "noise_reduction": 77 }, "output": { "method": "raw", - "bit_format": 8, + "data_format": "binary", + "bit_format": "8bit", "channels": "mono", "mono_option": "average" } @@ -74,7 +73,7 @@ Singleton { for (var i = 0; i < data.length; i++) { var amp = Math.min(data.charCodeAt(i), 128) / 128 - newValues[process.fillIndex] = amp * amp + newValues[process.fillIndex] = amp process.fillIndex = (process.fillIndex + 1) % barsCount } values = newValues From 93b60e900293f1081074f6cec69912f299e2e2c5 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 13 Aug 2025 22:52:28 -0400 Subject: [PATCH 256/394] Cava: Switched to ascii mode - buttery smooth Fixes a nasty glitch where band would jump abruptly due to wrong data parsing. --- Modules/Audio/LinearSpectrum.qml | 12 ------------ Services/Cava.qml | 24 ++++-------------------- 2 files changed, 4 insertions(+), 32 deletions(-) diff --git a/Modules/Audio/LinearSpectrum.qml b/Modules/Audio/LinearSpectrum.qml index 11f2488..bb1600d 100644 --- a/Modules/Audio/LinearSpectrum.qml +++ b/Modules/Audio/LinearSpectrum.qml @@ -24,12 +24,6 @@ Item { height: root.height * amp x: index * xScale y: root.height - height - - Behavior on height { - SmoothedAnimation { - duration: 33 - } - } } } @@ -47,12 +41,6 @@ Item { height: root.height * amp x: (values.length + index) * xScale y: root.height - height - - Behavior on height { - SmoothedAnimation { - duration: 33 - } - } } } } diff --git a/Services/Cava.qml b/Services/Cava.qml index 5b5728c..3a5626f 100644 --- a/Services/Cava.qml +++ b/Services/Cava.qml @@ -13,7 +13,7 @@ Singleton { property var config: ({ "general": { "bars": barsCount, - "framerate": 40, + "framerate": 60, "autosens": 0, "overshoot": 0, "sensitivity": 200, @@ -26,7 +26,8 @@ Singleton { }, "output": { "method": "raw", - "data_format": "binary", + "data_format": "ascii", + "ascii_max_range": 100, "bit_format": "8bit", "channels": "mono", "mono_option": "average" @@ -35,17 +36,14 @@ Singleton { Process { id: process - property int fillIndex: 0 stdinEnabled: true running: MediaPlayer.isPlaying command: ["cava", "-p", "/dev/stdin"] onExited: { stdinEnabled = true - fillIndex = 0 values = Array(barsCount).fill(0) } onStarted: { - for (const k in config) { if (typeof config[k] !== "object") { write(k + "=" + config[k] + "\n") @@ -58,25 +56,11 @@ Singleton { } } stdinEnabled = false - fillIndex = 0 values = Array(barsCount).fill(0) } stdout: SplitParser { - splitMarker: "" onRead: data => { - if (process.fillIndex + data.length >= barsCount) { - process.fillIndex = 0 - } - - // copy array - var newValues = values.slice(0) - - for (var i = 0; i < data.length; i++) { - var amp = Math.min(data.charCodeAt(i), 128) / 128 - newValues[process.fillIndex] = amp - process.fillIndex = (process.fillIndex + 1) % barsCount - } - values = newValues + root.values = data.slice(0, -1).split(";").map(v => parseInt(v, 10) / 100) } } } From 850968c3c0ce322952cf7d5db5173599d3097deb Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 13 Aug 2025 23:05:49 -0400 Subject: [PATCH 257/394] MediaCard: less intrusive mediaplayer switcher --- Modules/SidePanel/Cards/MediaCard.qml | 33 +++++++++++++++++++-------- Modules/SidePanel/PowerMenu.qml | 10 ++++---- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/Modules/SidePanel/Cards/MediaCard.qml b/Modules/SidePanel/Cards/MediaCard.qml index 0dc0121..d0489ba 100644 --- a/Modules/SidePanel/Cards/MediaCard.qml +++ b/Modules/SidePanel/Cards/MediaCard.qml @@ -67,6 +67,7 @@ NBox { currentIndex: MediaPlayer.selectedPlayerIndex background: Rectangle { + visible: false // implicitWidth: 120 * scaling // implicitHeight: 30 * scaling color: "transparent" @@ -76,6 +77,7 @@ NBox { } contentItem: NText { + visible: false leftPadding: Style.marginMedium * scaling rightPadding: playerSelector.indicator.width + playerSelector.spacing text: playerSelector.displayText @@ -86,18 +88,21 @@ NBox { } indicator: Text { - x: playerSelector.width - width - Style.marginMedium * scaling + x: playerSelector.width - width y: playerSelector.topPadding + (playerSelector.availableHeight - height) / 2 text: "arrow_drop_down" font.family: "Material Symbols Outlined" - font.pointSize: Style.marginXL * scaling + font.pointSize: Style.fontSizeXL * scaling color: Colors.textPrimary + horizontalAlignment: Text.AlignRight } popup: Popup { - y: playerSelector.height - width: playerSelector.width - implicitHeight: Math.min(160 * scaling, contentItem.implicitHeight + Style.marginMedium * scaling * 2) + id: popup + x: playerSelector.width * 0.5 + y: playerSelector.height * 0.75 + width: playerSelector.width * 0.5 + implicitHeight: Math.min(160 * scaling, contentItem.implicitHeight + Style.marginMedium * scaling) padding: Style.marginSmall * scaling contentItem: ListView { @@ -109,10 +114,19 @@ NBox { } background: Rectangle { - color: Colors.backgroundSecondary + gradient: Gradient { + GradientStop { + position: 0.0 + color: Colors.backgroundTertiary + } + GradientStop { + position: 1.0 + color: Colors.backgroundSecondary + } + } border.color: Colors.outline border.width: Math.max(1, Style.borderThin * scaling) - radius: Style.radiusMedium * scaling + radius: Style.radiusTiny * scaling } } @@ -128,9 +142,9 @@ NBox { highlighted: playerSelector.highlightedIndex === index background: Rectangle { - width: playerSelector.width - Style.marginSmall * scaling * 2 + width: popup.width - Style.marginSmall * scaling * 2 color: highlighted ? Colors.hover : "transparent" - radius: Style.radiusSmall * scaling + radius: Style.radiusTiny * scaling } } @@ -143,6 +157,7 @@ NBox { RowLayout { spacing: Style.marginMedium * scaling + // ------------------------- // Rounded thumbnail image Rectangle { diff --git a/Modules/SidePanel/PowerMenu.qml b/Modules/SidePanel/PowerMenu.qml index 0da04b1..5a8a6c6 100644 --- a/Modules/SidePanel/PowerMenu.qml +++ b/Modules/SidePanel/PowerMenu.qml @@ -64,7 +64,7 @@ NPanel { Layout.fillWidth: true Layout.preferredHeight: 36 * scaling radius: Style.radiusSmall * scaling - color: lockButtonArea.containsMouse ? Colors.accentPrimary : "transparent" + color: lockButtonArea.containsMouse ? Colors.hover : "transparent" Item { anchors.left: parent.left @@ -123,7 +123,7 @@ NPanel { Layout.fillWidth: true Layout.preferredHeight: 36 * scaling radius: Style.radiusSmall * scaling - color: suspendButtonArea.containsMouse ? Colors.accentPrimary : "transparent" + color: suspendButtonArea.containsMouse ? Colors.hover : "transparent" Item { anchors.left: parent.left @@ -181,7 +181,7 @@ NPanel { Layout.fillWidth: true Layout.preferredHeight: 36 * scaling radius: Style.radiusSmall * scaling - color: rebootButtonArea.containsMouse ? Colors.accentPrimary : "transparent" + color: rebootButtonArea.containsMouse ? Colors.hover : "transparent" Item { anchors.left: parent.left @@ -239,7 +239,7 @@ NPanel { Layout.fillWidth: true Layout.preferredHeight: 36 * scaling radius: Style.radiusSmall * scaling - color: logoutButtonArea.containsMouse ? Colors.accentPrimary : "transparent" + color: logoutButtonArea.containsMouse ? Colors.hover : "transparent" Item { anchors.left: parent.left @@ -297,7 +297,7 @@ NPanel { Layout.fillWidth: true Layout.preferredHeight: 36 * scaling radius: Style.radiusSmall * scaling - color: shutdownButtonArea.containsMouse ? Colors.accentPrimary : "transparent" + color: shutdownButtonArea.containsMouse ? Colors.hover : "transparent" Item { anchors.left: parent.left From 241220745fbc36e41222742e6fc2d9ae6c2e1208 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 13 Aug 2025 23:20:14 -0400 Subject: [PATCH 258/394] All config files should be lowercase --- Assets/Wallust/wallust.toml | 2 +- Services/Colors.qml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Assets/Wallust/wallust.toml b/Assets/Wallust/wallust.toml index 6c1b8f2..a27a0b3 100644 --- a/Assets/Wallust/wallust.toml +++ b/Assets/Wallust/wallust.toml @@ -44,4 +44,4 @@ check_contrast = true # target: ABSOLUTE path in which to place a file with generated templated values. # ¡ If either one is a directory, then both SHOULD be one. ! # zathura = { template = 'zathura', target = '~/.config/zathura/zathurarc' } -Noctalia = { template = 'noctalia.json', target = '~/.config/noctalia/Theme.json' } \ No newline at end of file +Noctalia = { template = 'noctalia.json', target = '~/.config/noctalia/theme.json' } \ No newline at end of file diff --git a/Services/Colors.qml b/Services/Colors.qml index be5ce2c..4507667 100644 --- a/Services/Colors.qml +++ b/Services/Colors.qml @@ -81,7 +81,7 @@ Singleton { property color overlay: "#191724" } - // Wallust theme colors (loaded from Theme.json) + // Wallust theme colors (loaded from theme.json) QtObject { id: wallustTheme @@ -115,7 +115,7 @@ Singleton { // FileView to load Wallust theme data from Theme.json FileView { id: wallustFile - path: Settings.configDir + "Theme.json" + path: Settings.configDir + "theme.json" watchChanges: true onFileChanged: reload() onAdapterUpdated: writeAdapter() From eefab33f76fd235df5ffebac9db325cfb6cd5f7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Atoch?= Date: Wed, 13 Aug 2025 23:40:22 -0400 Subject: [PATCH 259/394] Improve PowerProfile card when you dont have power profile available --- Modules/SidePanel/Cards/PowerProfilesCard.qml | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/Modules/SidePanel/Cards/PowerProfilesCard.qml b/Modules/SidePanel/Cards/PowerProfilesCard.qml index e5cc02a..54a56dd 100644 --- a/Modules/SidePanel/Cards/PowerProfilesCard.qml +++ b/Modules/SidePanel/Cards/PowerProfilesCard.qml @@ -14,6 +14,7 @@ NBox { // PowerProfiles service property var powerProfiles: PowerProfiles + readonly property bool hasPP: powerProfiles.hasPerformanceProfile RowLayout { id: powerRow @@ -26,12 +27,12 @@ NBox { // Performance NIconButton { icon: "speed" - enabled: powerProfiles.hasPerformanceProfile - opacity: enabled ? 1.0 : 0.3 - showFilled: powerProfiles.profile === PowerProfile.Performance - showBorder: powerProfiles.profile !== PowerProfile.Performance + enabled: hasPP + opacity: enabled ? Style.opacityFull : Style.opacityMedium + showFilled: enabled && powerProfiles.profile === PowerProfile.Performance + showBorder: !enabled || powerProfiles.profile !== PowerProfile.Performance onClicked: { - if (powerProfiles.hasPerformanceProfile) { + if (enabled) { powerProfiles.profile = PowerProfile.Performance } } @@ -39,19 +40,27 @@ NBox { // Balanced NIconButton { icon: "balance" - showFilled: powerProfiles.profile === PowerProfile.Balanced - showBorder: powerProfiles.profile !== PowerProfile.Balanced + enabled: hasPP + opacity: enabled ? Style.opacityFull : Style.opacityMedium + showFilled: enabled && powerProfiles.profile === PowerProfile.Balanced + showBorder: !enabled || powerProfiles.profile !== PowerProfile.Balanced onClicked: { - powerProfiles.profile = PowerProfile.Balanced + if (enabled) { + powerProfiles.profile = PowerProfile.Balanced + } } } // Eco NIconButton { icon: "eco" - showFilled: powerProfiles.profile === PowerProfile.PowerSaver - showBorder: powerProfiles.profile !== PowerProfile.PowerSaver + enabled: hasPP + opacity: enabled ? Style.opacityFull : Style.opacityMedium + showFilled: enabled && powerProfiles.profile === PowerProfile.PowerSaver + showBorder: !enabled || powerProfiles.profile !== PowerProfile.PowerSaver onClicked: { - powerProfiles.profile = PowerProfile.PowerSaver + if (enabled) { + powerProfiles.profile = PowerProfile.PowerSaver + } } } Item { From 006946306985f6a78b32c40b72dc81c3fd25c10a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Atoch?= Date: Wed, 13 Aug 2025 23:57:54 -0400 Subject: [PATCH 260/394] system-stats fix for intel cpu --- Bin/system-stats.sh | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/Bin/system-stats.sh b/Bin/system-stats.sh index da966df..7f628ed 100755 --- a/Bin/system-stats.sh +++ b/Bin/system-stats.sh @@ -113,7 +113,7 @@ get_cpu_temp() { if [[ -f "$dir/name" ]]; then local name name=$(<"$dir/name") - # Check for supported sensor types (matches Zig code). + # Check for supported sensor types. if [[ "$name" == "coretemp" || "$name" == "k10temp" ]]; then TEMP_SENSOR_PATH=$dir TEMP_SENSOR_TYPE=$name @@ -131,15 +131,29 @@ get_cpu_temp() { # --- Get temp based on sensor type --- if [[ "$TEMP_SENSOR_TYPE" == "coretemp" ]]; then - # For Intel 'coretemp', average all core temperatures. - # find gets all temp inputs, cat reads them, and awk calculates the average. - # The value is in millidegrees Celsius, so we divide by 1000. - find "$TEMP_SENSOR_PATH" -type f -name 'temp*_input' -print0 | xargs -0 cat | awk ' - { total += $1; count++ } - END { - if (count > 0) print int(total / count / 1000); - else print 0; - }' + # For Intel 'coretemp', average all available temperature sensors. + local total_temp=0 + local sensor_count=0 + + # Use a for loop with a glob to iterate over all temp input files. + # This is more efficient than 'find' for this simple case. + for temp_file in "$TEMP_SENSOR_PATH"/temp*_input; do + # The glob returns the pattern itself if no files match, + # so we must check if the file actually exists. + if [[ -f "$temp_file" ]]; then + total_temp=$((total_temp + $(<"$temp_file"))) + sensor_count=$((sensor_count + 1)) + fi + done + + if (( sensor_count > 0 )); then + # Use awk for the final division to handle potential floating point numbers + # and convert from millidegrees to integer degrees Celsius. + awk -v total="$total_temp" -v count="$sensor_count" 'BEGIN { print int(total / count / 1000) }' + else + # If no sensor files were found, return 0. + echo 0 + fi elif [[ "$TEMP_SENSOR_TYPE" == "k10temp" ]]; then # For AMD 'k10temp', find the 'Tctl' sensor, which is the control temperature. @@ -151,7 +165,7 @@ get_cpu_temp() { break fi done - + if [[ -f "$tctl_input" ]]; then # Read the temperature and convert from millidegrees to degrees. echo "$(( $(<"$tctl_input") / 1000 ))" @@ -163,7 +177,6 @@ get_cpu_temp() { fi } - # --- Main Loop --- # This loop runs indefinitely, gathering and printing stats. while true; do From 71433c7807d46539743af4188969e93d7c60ab96 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Thu, 14 Aug 2025 13:17:44 +0200 Subject: [PATCH 261/394] Add: Lockscreen (wip) --- Modules/Lockscreen/Lockscreen.qml | 780 ++++++++++++++++++++++++ Modules/SidePanel/Cards/ProfileCard.qml | 2 + Modules/SidePanel/PowerMenu.qml | 11 +- Modules/SidePanel/SidePanel.qml | 1 + Services/IPCManager.qml | 12 +- 5 files changed, 804 insertions(+), 2 deletions(-) create mode 100644 Modules/Lockscreen/Lockscreen.qml diff --git a/Modules/Lockscreen/Lockscreen.qml b/Modules/Lockscreen/Lockscreen.qml new file mode 100644 index 0000000..57e6933 --- /dev/null +++ b/Modules/Lockscreen/Lockscreen.qml @@ -0,0 +1,780 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import QtQuick.Effects +import Quickshell +import Quickshell.Wayland +import Quickshell.Services.Pam +import Quickshell.Io +import Quickshell.Widgets +import qs.Services +import qs.Widgets + +WlSessionLock { + id: lock + + property string errorMessage: "" + property bool authenticating: false + property string password: "" + property bool pamAvailable: typeof PamContext !== "undefined" + locked: false + + function unlockAttempt() { + console.log("Unlock attempt started"); + if (!pamAvailable) { + lock.errorMessage = "PAM authentication not available."; + console.log("PAM not available"); + return; + } + if (!lock.password) { + lock.errorMessage = "Password required."; + console.log("No password entered"); + return; + } + console.log("Starting PAM authentication..."); + lock.authenticating = true; + lock.errorMessage = ""; + + console.log("[LockScreen] About to create PAM context with userName:", Quickshell.env("USER")); + var pam = Qt.createQmlObject('import Quickshell.Services.Pam; PamContext { config: "login"; user: "' + Quickshell.env("USER") + '" }', lock); + console.log("PamContext created", pam); + + pam.onCompleted.connect(function (result) { + console.log("PAM completed with result:", result); + lock.authenticating = false; + if (result === PamResult.Success) { + console.log("Authentication successful, unlocking..."); + lock.locked = false; + lock.password = ""; + lock.errorMessage = ""; + } else { + console.log("Authentication failed"); + lock.errorMessage = "Authentication failed."; + lock.password = ""; + } + pam.destroy(); + }); + + pam.onError.connect(function (error) { + console.log("PAM error:", error); + lock.authenticating = false; + lock.errorMessage = pam.message || "Authentication error."; + lock.password = ""; + pam.destroy(); + }); + + pam.onPamMessage.connect(function () { + console.log("PAM message:", pam.message, "isError:", pam.messageIsError); + if (pam.messageIsError) { + lock.errorMessage = pam.message; + } + }); + + pam.onResponseRequiredChanged.connect(function () { + console.log("PAM response required:", pam.responseRequired); + if (pam.responseRequired && lock.authenticating) { + console.log("Responding to PAM with password"); + pam.respond(lock.password); + } + }); + + var started = pam.start(); + console.log("PAM start result:", started); + } + + WlSessionLockSurface { + // Wallpaper image + Image { + id: lockBgImage + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + source: Wallpapers.currentWallpaper !== "" ? Wallpapers.currentWallpaper : "" + cache: true + smooth: true + mipmap: false + } + + // Blurred background + Rectangle { + anchors.fill: parent + color: "transparent" + + // Simple blur effect + layer.enabled: true + layer.smooth: true + layer.samples: 4 + } + + // Animated gradient overlay + Rectangle { + anchors.fill: parent + gradient: Gradient { + GradientStop { position: 0.0; color: Qt.rgba(0, 0, 0, 0.6) } + GradientStop { position: 0.3; color: Qt.rgba(0, 0, 0, 0.3) } + GradientStop { position: 0.7; color: Qt.rgba(0, 0, 0, 0.4) } + GradientStop { position: 1.0; color: Qt.rgba(0, 0, 0, 0.7) } + } + + // Subtle animated particles + Repeater { + model: 20 + Rectangle { + width: Math.random() * 4 + 2 + height: width + radius: width * 0.5 + color: Qt.rgba(Colors.accentPrimary.r, Colors.accentPrimary.g, Colors.accentPrimary.b, 0.3) + x: Math.random() * parent.width + y: Math.random() * parent.height + + SequentialAnimation on opacity { + loops: Animation.Infinite + NumberAnimation { to: 0.8; duration: 2000 + Math.random() * 3000 } + NumberAnimation { to: 0.1; duration: 2000 + Math.random() * 3000 } + } + } + } + } + + // Main content - Centered design + Item { + anchors.fill: parent + + // Top section - Time, date, and user info + ColumnLayout { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 80 * Scaling.scale(screen) + spacing: 40 * Scaling.scale(screen) + + // Time display - Large and prominent with pulse animation + Column { + spacing: 8 * Scaling.scale(screen) + Layout.alignment: Qt.AlignHCenter + + Text { + id: timeText + text: Qt.formatDateTime(new Date(), "HH:mm") + font.family: "Inter" + font.pixelSize: 140 * Scaling.scale(screen) + font.weight: Font.Bold + color: Colors.textPrimary + horizontalAlignment: Text.AlignHCenter + + SequentialAnimation on scale { + loops: Animation.Infinite + NumberAnimation { to: 1.02; duration: 2000; easing.type: Easing.InOutQuad } + NumberAnimation { to: 1.0; duration: 2000; easing.type: Easing.InOutQuad } + } + } + + Text { + id: dateText + text: Qt.formatDateTime(new Date(), "dddd, MMMM d") + font.family: "Inter" + font.pixelSize: 26 * Scaling.scale(screen) + font.weight: Font.Light + color: Colors.textSecondary + horizontalAlignment: Text.AlignHCenter + width: timeText.width + } + } + + // User section with animated avatar + Column { + spacing: 16 * Scaling.scale(screen) + Layout.alignment: Qt.AlignHCenter + + // Animated avatar with glow effect + Rectangle { + width: 120 * Scaling.scale(screen) + height: 120 * Scaling.scale(screen) + radius: width * 0.5 + color: "transparent" + border.color: Colors.accentPrimary + border.width: 3 * Scaling.scale(screen) + anchors.horizontalCenter: parent.horizontalCenter + + // Glow effect + Rectangle { + anchors.centerIn: parent + width: parent.width + 24 * Scaling.scale(screen) + height: parent.height + 24 * Scaling.scale(screen) + radius: width * 0.5 + color: "transparent" + border.color: Qt.rgba(Colors.accentPrimary.r, Colors.accentPrimary.g, Colors.accentPrimary.b, 0.3) + border.width: 2 * Scaling.scale(screen) + z: -1 + + SequentialAnimation on scale { + loops: Animation.Infinite + NumberAnimation { to: 1.1; duration: 1500; easing.type: Easing.InOutQuad } + NumberAnimation { to: 1.0; duration: 1500; easing.type: Easing.InOutQuad } + } + } + + NImageRounded { + anchors.centerIn: parent + width: 100 * Scaling.scale(screen) + height: 100 * Scaling.scale(screen) + imagePath: Quickshell.env("HOME") + "/.face" + fallbackIcon: "person" + imageRadius: width * 0.5 + } + + // Hover animation + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: parent.scale = 1.05 + onExited: parent.scale = 1.0 + } + + Behavior on scale { + NumberAnimation { duration: 200; easing.type: Easing.OutBack } + } + } + + + } + } + + // Centered terminal section + Item { + width: 520 * Scaling.scale(screen) + height: 200 * Scaling.scale(screen) + anchors.centerIn: parent + + ColumnLayout { + anchors.centerIn: parent + spacing: 20 * Scaling.scale(screen) + width: parent.width + + + + // Futuristic Terminal-Style Input + Item { + width: parent.width + height: 200 * Scaling.scale(screen) + Layout.fillWidth: true + + // Terminal background with scanlines + Rectangle { + id: terminalBackground + anchors.fill: parent + radius: 16 + color: Colors.applyOpacity(Colors.backgroundPrimary, "E6") + border.color: Colors.accentPrimary + border.width: 2 * Scaling.scale(screen) + + // Scanline effect + Repeater { + model: 20 + Rectangle { + width: parent.width + height: 1 + color: Colors.applyOpacity(Colors.accentPrimary, "1A") + y: index * 10 + opacity: 0.3 + + SequentialAnimation on opacity { + loops: Animation.Infinite + NumberAnimation { to: 0.6; duration: 2000 + Math.random() * 1000 } + NumberAnimation { to: 0.1; duration: 2000 + Math.random() * 1000 } + } + } + } + + // Terminal header + Rectangle { + width: parent.width + height: 40 * Scaling.scale(screen) + color: Colors.applyOpacity(Colors.accentPrimary, "33") + topLeftRadius: 14 + topRightRadius: 14 + + RowLayout { + anchors.fill: parent + anchors.margins: 12 * Scaling.scale(screen) + spacing: 12 * Scaling.scale(screen) + + Text { + text: "●" + color: Colors.error + font.pixelSize: 16 * Scaling.scale(screen) + } + + Text { + text: "●" + color: Colors.warning + font.pixelSize: 16 * Scaling.scale(screen) + } + + Text { + text: "●" + color: Colors.accentPrimary + font.pixelSize: 16 * Scaling.scale(screen) + } + + Text { + text: "SECURE TERMINAL" + color: Colors.textPrimary + font.family: "Monaco" + font.pixelSize: 14 * Scaling.scale(screen) + font.weight: Font.Bold + Layout.fillWidth: true + } + } + } + + // Terminal content area + ColumnLayout { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.topMargin: 50 * Scaling.scale(screen) + anchors.margins: 12 * Scaling.scale(screen) + spacing: 12 * Scaling.scale(screen) + + // Welcome back typing effect + RowLayout { + Layout.fillWidth: true + spacing: 12 * Scaling.scale(screen) + + Text { + text: "root@noctalia:~$" + color: Colors.accentPrimary + font.family: "Monaco" + font.pixelSize: 16 * Scaling.scale(screen) + font.weight: Font.Bold + } + + Text { + id: welcomeText + text: "" + color: Colors.textPrimary + font.family: "Monaco" + font.pixelSize: 16 * Scaling.scale(screen) + property int currentIndex: 0 + property string fullText: "echo 'Welcome back, " + Quickshell.env("USER") + "!'" + + Timer { + interval: 100 + running: true + repeat: true + onTriggered: { + if (parent.currentIndex < parent.fullText.length) { + parent.text = parent.fullText.substring(0, parent.currentIndex + 1) + parent.currentIndex++ + } else { + running = false + } + } + } + } + } + + // Command line with integrated password input + RowLayout { + Layout.fillWidth: true + spacing: 12 * Scaling.scale(screen) + + Text { + text: "root@noctalia:~$" + color: Colors.accentPrimary + font.family: "Monaco" + font.pixelSize: 16 * Scaling.scale(screen) + font.weight: Font.Bold + } + + Text { + text: "sudo unlock_session" + color: Colors.textPrimary + font.family: "Monaco" + font.pixelSize: 16 * Scaling.scale(screen) + } + + // Integrated password input (invisible, just for functionality) + TextInput { + id: passwordInput + width: 0 + height: 0 + visible: false + font.family: "Monaco" + font.pixelSize: 16 * Scaling.scale(screen) + color: Colors.textPrimary + echoMode: TextInput.Password + passwordCharacter: "*" + passwordMaskDelay: 0 + + text: lock.password + onTextChanged: { + lock.password = text + // Terminal typing sound effect (visual) + typingEffect.start() + } + + Keys.onPressed: function (event) { + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + lock.unlockAttempt(); + } + } + + Component.onCompleted: { + forceActiveFocus(); + } + } + + // Visual password display with integrated cursor + Text { + id: asterisksText + text: "*".repeat(passwordInput.text.length) + color: Colors.textPrimary + font.family: "Monaco" + font.pixelSize: 16 * Scaling.scale(screen) + visible: passwordInput.activeFocus + + // Typing effect animation + SequentialAnimation { + id: typingEffect + NumberAnimation { + target: passwordInput + property: "scale" + to: 1.01 + duration: 50 + } + NumberAnimation { + target: passwordInput + property: "scale" + to: 1.0 + duration: 50 + } + } + } + + // Blinking cursor positioned right after the asterisks + Rectangle { + width: 8 * Scaling.scale(screen) + height: 20 * Scaling.scale(screen) + color: Colors.accentPrimary + visible: passwordInput.activeFocus + anchors.left: asterisksText.right + anchors.leftMargin: 2 * Scaling.scale(screen) + anchors.verticalCenter: asterisksText.verticalCenter + + SequentialAnimation on opacity { + loops: Animation.Infinite + NumberAnimation { to: 1.0; duration: 500 } + NumberAnimation { to: 0.0; duration: 500 } + } + } + } + + // Status messages + Text { + text: lock.authenticating ? "Authenticating..." : (lock.errorMessage !== "" ? "Authentication failed." : "") + color: lock.authenticating ? Colors.accentPrimary : (lock.errorMessage !== "" ? Colors.error : "transparent") + font.family: "Monaco" + font.pixelSize: 14 * Scaling.scale(screen) + Layout.fillWidth: true + + SequentialAnimation on opacity { + running: lock.authenticating + loops: Animation.Infinite + NumberAnimation { to: 1.0; duration: 800 } + NumberAnimation { to: 0.5; duration: 800 } + } + } + + // Execute button + Rectangle { + width: 120 * Scaling.scale(screen) + height: 40 * Scaling.scale(screen) + radius: 12 + color: executeButtonArea.containsMouse ? Colors.accentPrimary : Colors.applyOpacity(Colors.accentPrimary, "33") + border.color: Colors.accentPrimary + border.width: 1 + enabled: !lock.authenticating + Layout.alignment: Qt.AlignRight + + Text { + anchors.centerIn: parent + text: lock.authenticating ? "EXECUTING..." : "EXECUTE" + color: executeButtonArea.containsMouse ? Colors.onAccent : Colors.accentPrimary + font.family: "Monaco" + font.pixelSize: 12 * Scaling.scale(screen) + font.weight: Font.Bold + } + + MouseArea { + id: executeButtonArea + anchors.fill: parent + hoverEnabled: true + onClicked: lock.unlockAttempt() + + SequentialAnimation on scale { + running: containsMouse + NumberAnimation { to: 1.05; duration: 150; easing.type: Easing.OutCubic } + } + + SequentialAnimation on scale { + running: !containsMouse + NumberAnimation { to: 1.0; duration: 150; easing.type: Easing.OutCubic } + } + } + + // Processing animation + SequentialAnimation on scale { + loops: Animation.Infinite + running: lock.authenticating + NumberAnimation { to: 1.02; duration: 600; easing.type: Easing.InOutQuad } + NumberAnimation { to: 1.0; duration: 600; easing.type: Easing.InOutQuad } + } + } + } + + // Terminal glow effect + Rectangle { + anchors.fill: parent + radius: parent.radius + color: "transparent" + border.color: Colors.applyOpacity(Colors.accentPrimary, "4D") + border.width: 1 + z: -1 + + SequentialAnimation on opacity { + loops: Animation.Infinite + NumberAnimation { to: 0.6; duration: 2000; easing.type: Easing.InOutQuad } + NumberAnimation { to: 0.2; duration: 2000; easing.type: Easing.InOutQuad } + } + } + } + + } + + // Error message with modern styling + Rectangle { + width: parent.width + height: 56 * Scaling.scale(screen) + radius: 28 + color: Qt.rgba(Colors.error.r, Colors.error.g, Colors.error.b, 0.15) + border.color: Colors.error + border.width: 1 * Scaling.scale(screen) + visible: lock.errorMessage !== "" + Layout.fillWidth: true + + RowLayout { + anchors.fill: parent + anchors.margins: 18 * Scaling.scale(screen) + spacing: 12 * Scaling.scale(screen) + + Text { + text: "error" + font.family: "Material Symbols Outlined" + font.pixelSize: 22 * Scaling.scale(screen) + color: Colors.error + } + + Text { + text: lock.errorMessage + color: Colors.error + font.family: "Inter" + font.pixelSize: 16 * Scaling.scale(screen) + font.weight: Font.Medium + Layout.fillWidth: true + } + } + + NumberAnimation on opacity { + from: 0 + to: 1 + duration: 300 + running: lock.errorMessage !== "" + } + + // Shake animation on error + SequentialAnimation on x { + running: lock.errorMessage !== "" + NumberAnimation { to: 10; duration: 50 } + NumberAnimation { to: -10; duration: 100 } + NumberAnimation { to: 10; duration: 100 } + NumberAnimation { to: 0; duration: 50 } + } + } + + + } + } + } + + // Enhanced power buttons with hover effects + Row { + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: 50 * Scaling.scale(screen) + spacing: 20 * Scaling.scale(screen) + + // Shutdown with enhanced styling + Rectangle { + width: 64 * Scaling.scale(screen) + height: 64 * Scaling.scale(screen) + radius: 32 + color: Qt.rgba(Colors.error.r, Colors.error.g, Colors.error.b, shutdownArea.containsMouse ? 0.9 : 0.2) + border.color: Colors.error + border.width: 2 * Scaling.scale(screen) + + // Glow effect + Rectangle { + anchors.centerIn: parent + width: parent.width + 10 * Scaling.scale(screen) + height: parent.height + 10 * Scaling.scale(screen) + radius: width * 0.5 + color: "transparent" + border.color: Qt.rgba(Colors.error.r, Colors.error.g, Colors.error.b, 0.3) + border.width: 2 * Scaling.scale(screen) + opacity: shutdownArea.containsMouse ? 1 : 0 + z: -1 + + Behavior on opacity { + NumberAnimation { duration: 200; easing.type: Easing.OutCubic } + } + } + + MouseArea { + id: shutdownArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + Qt.createQmlObject('import Quickshell.Io; Process { command: ["shutdown", "-h", "now"]; running: true }', lock); + } + } + + Text { + anchors.centerIn: parent + text: "power_settings_new" + font.family: "Material Symbols Outlined" + font.pixelSize: 28 * Scaling.scale(screen) + color: shutdownArea.containsMouse ? Colors.onAccent : Colors.error + } + + Behavior on color { + ColorAnimation { duration: 200; easing.type: Easing.OutCubic } + } + scale: shutdownArea.containsMouse ? 1.1 : 1.0 + } + + // Reboot with enhanced styling + Rectangle { + width: 64 * Scaling.scale(screen) + height: 64 * Scaling.scale(screen) + radius: 32 + color: Qt.rgba(Colors.accentPrimary.r, Colors.accentPrimary.g, Colors.accentPrimary.b, rebootArea.containsMouse ? 0.9 : 0.2) + border.color: Colors.accentPrimary + border.width: 2 * Scaling.scale(screen) + + // Glow effect + Rectangle { + anchors.centerIn: parent + width: parent.width + 10 * Scaling.scale(screen) + height: parent.height + 10 * Scaling.scale(screen) + radius: width * 0.5 + color: "transparent" + border.color: Qt.rgba(Colors.accentPrimary.r, Colors.accentPrimary.g, Colors.accentPrimary.b, 0.3) + border.width: 2 * Scaling.scale(screen) + opacity: rebootArea.containsMouse ? 1 : 0 + z: -1 + + Behavior on opacity { + NumberAnimation { duration: 200; easing.type: Easing.OutCubic } + } + } + + MouseArea { + id: rebootArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + Qt.createQmlObject('import Quickshell.Io; Process { command: ["reboot"]; running: true }', lock); + } + } + + Text { + anchors.centerIn: parent + text: "refresh" + font.family: "Material Symbols Outlined" + font.pixelSize: 28 * Scaling.scale(screen) + color: rebootArea.containsMouse ? Colors.onAccent : Colors.accentPrimary + } + + Behavior on color { + ColorAnimation { duration: 200; easing.type: Easing.OutCubic } + } + scale: rebootArea.containsMouse ? 1.1 : 1.0 + } + + // Logout with enhanced styling + Rectangle { + width: 64 * Scaling.scale(screen) + height: 64 * Scaling.scale(screen) + radius: 32 + color: Qt.rgba(Colors.accentSecondary.r, Colors.accentSecondary.g, Colors.accentSecondary.b, logoutArea.containsMouse ? 0.9 : 0.2) + border.color: Colors.accentSecondary + border.width: 2 * Scaling.scale(screen) + + // Glow effect + Rectangle { + anchors.centerIn: parent + width: parent.width + 10 * Scaling.scale(screen) + height: parent.height + 10 * Scaling.scale(screen) + radius: width * 0.5 + color: "transparent" + border.color: Qt.rgba(Colors.accentSecondary.r, Colors.accentSecondary.g, Colors.accentSecondary.b, 0.3) + border.width: 2 * Scaling.scale(screen) + opacity: logoutArea.containsMouse ? 1 : 0 + z: -1 + + Behavior on opacity { + NumberAnimation { duration: 200; easing.type: Easing.OutCubic } + } + } + + MouseArea { + id: logoutArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + Qt.createQmlObject('import Quickshell.Io; Process { command: ["loginctl", "terminate-user", "' + Quickshell.env("USER") + '"]; running: true }', lock); + } + } + + Text { + anchors.centerIn: parent + text: "exit_to_app" + font.family: "Material Symbols Outlined" + font.pixelSize: 28 * Scaling.scale(screen) + color: logoutArea.containsMouse ? Colors.onAccent : Colors.accentSecondary + } + + Behavior on color { + ColorAnimation { duration: 200; easing.type: Easing.OutCubic } + } + scale: logoutArea.containsMouse ? 1.1 : 1.0 + } + } + + // Timer for updating time + Timer { + interval: 1000 + running: true + repeat: true + onTriggered: { + timeText.text = Qt.formatDateTime(new Date(), "HH:mm"); + dateText.text = Qt.formatDateTime(new Date(), "dddd, MMMM d"); + } + } + } +} diff --git a/Modules/SidePanel/Cards/ProfileCard.qml b/Modules/SidePanel/Cards/ProfileCard.qml index 23f19df..33d7a4b 100644 --- a/Modules/SidePanel/Cards/ProfileCard.qml +++ b/Modules/SidePanel/Cards/ProfileCard.qml @@ -14,6 +14,8 @@ NBox { readonly property real scaling: Scaling.scale(screen) property string uptimeText: "--" + + Layout.fillWidth: true // Height driven by content diff --git a/Modules/SidePanel/PowerMenu.qml b/Modules/SidePanel/PowerMenu.qml index 5a8a6c6..a0a7f9a 100644 --- a/Modules/SidePanel/PowerMenu.qml +++ b/Modules/SidePanel/PowerMenu.qml @@ -6,10 +6,13 @@ import Quickshell.Io import Quickshell.Widgets import qs.Services import qs.Widgets +import qs.Modules.Lockscreen NPanel { id: powerMenu visible: false + + // Anchors will be set by the parent component function show() { @@ -110,8 +113,9 @@ NPanel { hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { - // TODO: Implement lock screen functionality console.log("Lock screen requested") + // Lock the screen + lockScreen.locked = true powerMenu.visible = false } } @@ -416,4 +420,9 @@ NPanel { command: ["loginctl", "terminate-user", Quickshell.env("USER")] running: false } + + // Lockscreen instance + Lockscreen { + id: lockScreen + } } diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml index 96de4ce..ba3b546 100644 --- a/Modules/SidePanel/SidePanel.qml +++ b/Modules/SidePanel/SidePanel.qml @@ -15,6 +15,7 @@ NLoader { // Target screen to open on property var targetScreen: null + function openAt(x, screen) { anchorX = x targetScreen = screen diff --git a/Services/IPCManager.qml b/Services/IPCManager.qml index f9f8d8f..1a6f201 100644 --- a/Services/IPCManager.qml +++ b/Services/IPCManager.qml @@ -1,8 +1,12 @@ import QtQuick import Quickshell.Io +import qs.Modules.Lockscreen Item { id: root + + // Reference to the lockscreen component + property var lockscreen: null IpcHandler { target: "settings" @@ -40,7 +44,13 @@ Item { IpcHandler { target: "lockScreen" - function toggle() {// TODO + function toggle() { + lockScreen.locked = !lockScreen.locked } } + + // Lockscreen instance + Lockscreen { + id: lockScreen + } } From 2a7810c17c4db77b937e5df9db43815849b71c18 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Thu, 14 Aug 2025 07:42:03 -0400 Subject: [PATCH 262/394] Settings Tab: replaced Tab IDs by a QML enum --- Modules/Bar/Volume.qml | 2 +- Modules/Settings/SettingsPanel.qml | 48 +++++++------------ Modules/SidePanel/Cards/PowerProfilesCard.qml | 2 +- Modules/SidePanel/Cards/ProfileCard.qml | 4 +- Modules/SidePanel/Cards/UtilitiesCard.qml | 2 +- Modules/SidePanel/PowerMenu.qml | 2 - Modules/SidePanel/SidePanel.qml | 1 - Services/IPCManager.qml | 2 +- Services/Settings.qml | 15 ++++++ 9 files changed, 36 insertions(+), 42 deletions(-) diff --git a/Modules/Bar/Volume.qml b/Modules/Bar/Volume.qml index 412a196..da28357 100644 --- a/Modules/Bar/Volume.qml +++ b/Modules/Bar/Volume.qml @@ -52,7 +52,7 @@ Item { } } onClicked: { - settingsPanel.requestedTab = settingsPanel.tabsIds.AUDIO + settingsPanel.requestedTab = Settings.Tab.Audio settingsPanel.isLoaded = true } } diff --git a/Modules/Settings/SettingsPanel.qml b/Modules/Settings/SettingsPanel.qml index c5c8f2b..55cf26d 100644 --- a/Modules/Settings/SettingsPanel.qml +++ b/Modules/Settings/SettingsPanel.qml @@ -10,25 +10,7 @@ import qs.Widgets NLoader { id: root - property var tabsIds: null - property var requestedTab: null - - Component.onCompleted: { - // Fill up our ideads - tabsIds = Object.freeze({ - "GENERAL": 0, - "BAR": 1, - "TIME_WEATHER": 2, - "SCREEN_RECORDER": 3, - "NETWORK": 4, - "AUDIO": 5, - "DISPLAY": 6, - "WALLPAPER": 7, - "WALLPAPER_SELECTOR": 8, - "MISC": 9, - "ABOUT": 10 - }) - } + property int requestedTab: Settings.Tab.General content: Component { NPanel { @@ -74,63 +56,67 @@ NLoader { WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand property var tabsModel: [{ - "id": root.tabsIds.GENERAL, + "id": Settings.Tab.General, "label": "General", "icon": "tune", "source": "Tabs/General.qml" }, { - "id": root.tabsIds.BAR, + "id": Settings.Tab.Bar, "label": "Bar", "icon": "web_asset", "source": "Tabs/Bar.qml" }, { - "id": root.tabsIds.TIME_WEATHER, + "id": Settings.Tab.TimeWeather, "label": "Time & Weather", "icon": "schedule", "source": "Tabs/TimeWeather.qml" }, { - "id": root.tabsIds.SCREEN_RECORDER, + "id": Settings.Tab.ScreenRecorder, "label": "Screen Recorder", "icon": "videocam", "source": "Tabs/ScreenRecorder.qml" }, { - "id": root.tabsIds.NETWORK, + "id": Settings.Tab.Network, "label": "Network", "icon": "lan", "source": "Tabs/Network.qml" }, { - "id": root.tabsIds.AUDIO, + "id": Settings.Tab.Audio, "label": "Audio", "icon": "volume_up", "source": "Tabs/Audio.qml" }, { - "id": root.tabsIds.DISPLAY, + "id": Settings.Tab.Display, "label": "Display", "icon": "monitor", "source": "Tabs/Display.qml" }, { - "id": root.tabsIds.WALLPAPER, + "id": Settings.Tab.Wallpaper, "label": "Wallpaper", "icon": "image", "source": "Tabs/Wallpaper.qml" }, { - "id": root.tabsIds.WALLPAPER_SELECTOR, + "id": Settings.Tab.WallpaperSelector, "label": "Wallpaper Selector", "icon": "wallpaper_slideshow", "source": "Tabs/WallpaperSelector.qml" }, // { - // "id": root.tabsIds.MISC, + // "id": TabId.Misc, // "label": "Misc", // "icon": "more_horiz", // "source": "Tabs/Misc.qml" // }, { - "id": root.tabsIds.ABOUT, + "id": Settings.Tab.About, "label": "About", "icon": "info", "source": "Tabs/About.qml" }] + Component.onCompleted: { + show() + } + // Combined visibility change handler onVisibleChanged: { if (visible) { @@ -156,8 +142,6 @@ NLoader { } } - Component.onCompleted: show() - Rectangle { id: bgRect color: Colors.backgroundPrimary diff --git a/Modules/SidePanel/Cards/PowerProfilesCard.qml b/Modules/SidePanel/Cards/PowerProfilesCard.qml index 54a56dd..8562967 100644 --- a/Modules/SidePanel/Cards/PowerProfilesCard.qml +++ b/Modules/SidePanel/Cards/PowerProfilesCard.qml @@ -42,7 +42,7 @@ NBox { icon: "balance" enabled: hasPP opacity: enabled ? Style.opacityFull : Style.opacityMedium - showFilled: enabled && powerProfiles.profile === PowerProfile.Balanced + showFilled: enabled && powerProfiles.profile === PowerProfile.Balanced showBorder: !enabled || powerProfiles.profile !== PowerProfile.Balanced onClicked: { if (enabled) { diff --git a/Modules/SidePanel/Cards/ProfileCard.qml b/Modules/SidePanel/Cards/ProfileCard.qml index 33d7a4b..0b0d6e9 100644 --- a/Modules/SidePanel/Cards/ProfileCard.qml +++ b/Modules/SidePanel/Cards/ProfileCard.qml @@ -14,8 +14,6 @@ NBox { readonly property real scaling: Scaling.scale(screen) property string uptimeText: "--" - - Layout.fillWidth: true // Height driven by content @@ -61,7 +59,7 @@ NBox { icon: "settings" tooltipText: "Open settings" onClicked: { - settingsPanel.requestedTab = settingsPanel.tabsIds.GENERAL + settingsPanel.requestedTab = Settings.Tab.General settingsPanel.isLoaded = !settingsPanel.isLoaded } } diff --git a/Modules/SidePanel/Cards/UtilitiesCard.qml b/Modules/SidePanel/Cards/UtilitiesCard.qml index e40d9ba..2523e39 100644 --- a/Modules/SidePanel/Cards/UtilitiesCard.qml +++ b/Modules/SidePanel/Cards/UtilitiesCard.qml @@ -31,7 +31,7 @@ NBox { NIconButton { icon: "image" onClicked: { - settingsPanel.requestedTab = settingsPanel.tabsIds.WALLPAPER_SELECTOR + settingsPanel.requestedTab = Settings.Tab.WallpaperSelector settingsPanel.isLoaded = true } } diff --git a/Modules/SidePanel/PowerMenu.qml b/Modules/SidePanel/PowerMenu.qml index a0a7f9a..4f62141 100644 --- a/Modules/SidePanel/PowerMenu.qml +++ b/Modules/SidePanel/PowerMenu.qml @@ -11,8 +11,6 @@ import qs.Modules.Lockscreen NPanel { id: powerMenu visible: false - - // Anchors will be set by the parent component function show() { diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml index ba3b546..96de4ce 100644 --- a/Modules/SidePanel/SidePanel.qml +++ b/Modules/SidePanel/SidePanel.qml @@ -15,7 +15,6 @@ NLoader { // Target screen to open on property var targetScreen: null - function openAt(x, screen) { anchorX = x targetScreen = screen diff --git a/Services/IPCManager.qml b/Services/IPCManager.qml index 1a6f201..3db3143 100644 --- a/Services/IPCManager.qml +++ b/Services/IPCManager.qml @@ -4,7 +4,7 @@ import qs.Modules.Lockscreen Item { id: root - + // Reference to the lockscreen component property var lockscreen: null diff --git a/Services/Settings.qml b/Services/Settings.qml index 9f2ca39..b0b3c28 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -7,6 +7,21 @@ pragma Singleton Singleton { id: root + // Our tabs used in the UI, needs to be defined in a Singleton so they can be accessed anywhere + enum Tab { + General, + Bar, + TimeWeather, + ScreenRecorder, + Network, + Audio, + Display, + Wallpaper, + WallpaperSelector, + //Misc, + About + } + // Define our app directories // Default config directory: ~/.config/noctalia // Default cache directory: ~/.cache/noctalia From ef5fff12d1b85d4aa147758f691125c0f45e8cd7 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Thu, 14 Aug 2025 07:52:01 -0400 Subject: [PATCH 263/394] Settings: tab enum cleanup --- Modules/Bar/Volume.qml | 3 +- Modules/Settings/SettingsPanel.qml | 38 ++++++++++++++++------- Modules/SidePanel/Cards/ProfileCard.qml | 5 +-- Modules/SidePanel/Cards/UtilitiesCard.qml | 3 +- Services/Settings.qml | 15 --------- 5 files changed, 33 insertions(+), 31 deletions(-) diff --git a/Modules/Bar/Volume.qml b/Modules/Bar/Volume.qml index da28357..ddceda1 100644 --- a/Modules/Bar/Volume.qml +++ b/Modules/Bar/Volume.qml @@ -1,6 +1,7 @@ import QtQuick import Quickshell import Quickshell.Services.Pipewire +import qs.Modules.Settings import qs.Services import qs.Widgets @@ -52,7 +53,7 @@ Item { } } onClicked: { - settingsPanel.requestedTab = Settings.Tab.Audio + settingsPanel.requestedTab = SettingsPanel.Tab.Audio settingsPanel.isLoaded = true } } diff --git a/Modules/Settings/SettingsPanel.qml b/Modules/Settings/SettingsPanel.qml index 55cf26d..132cf68 100644 --- a/Modules/Settings/SettingsPanel.qml +++ b/Modules/Settings/SettingsPanel.qml @@ -10,7 +10,21 @@ import qs.Widgets NLoader { id: root - property int requestedTab: Settings.Tab.General + enum Tab { + General, + Bar, + TimeWeather, + ScreenRecorder, + Network, + Audio, + Display, + Wallpaper, + WallpaperSelector, + //Misc, + About + } + + property int requestedTab: SettingsPanel.Tab.General content: Component { NPanel { @@ -56,58 +70,58 @@ NLoader { WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand property var tabsModel: [{ - "id": Settings.Tab.General, + "id": SettingsPanel.Tab.General, "label": "General", "icon": "tune", "source": "Tabs/General.qml" }, { - "id": Settings.Tab.Bar, + "id": SettingsPanel.Tab.Bar, "label": "Bar", "icon": "web_asset", "source": "Tabs/Bar.qml" }, { - "id": Settings.Tab.TimeWeather, + "id": SettingsPanel.Tab.TimeWeather, "label": "Time & Weather", "icon": "schedule", "source": "Tabs/TimeWeather.qml" }, { - "id": Settings.Tab.ScreenRecorder, + "id": SettingsPanel.Tab.ScreenRecorder, "label": "Screen Recorder", "icon": "videocam", "source": "Tabs/ScreenRecorder.qml" }, { - "id": Settings.Tab.Network, + "id": SettingsPanel.Tab.Network, "label": "Network", "icon": "lan", "source": "Tabs/Network.qml" }, { - "id": Settings.Tab.Audio, + "id": SettingsPanel.Tab.Audio, "label": "Audio", "icon": "volume_up", "source": "Tabs/Audio.qml" }, { - "id": Settings.Tab.Display, + "id": SettingsPanel.Tab.Display, "label": "Display", "icon": "monitor", "source": "Tabs/Display.qml" }, { - "id": Settings.Tab.Wallpaper, + "id": SettingsPanel.Tab.Wallpaper, "label": "Wallpaper", "icon": "image", "source": "Tabs/Wallpaper.qml" }, { - "id": Settings.Tab.WallpaperSelector, + "id": SettingsPanel.Tab.WallpaperSelector, "label": "Wallpaper Selector", "icon": "wallpaper_slideshow", "source": "Tabs/WallpaperSelector.qml" }, // { - // "id": TabId.Misc, + // "id": SettingsPanel.Tab.Misc, // "label": "Misc", // "icon": "more_horiz", // "source": "Tabs/Misc.qml" // }, { - "id": Settings.Tab.About, + "id": SettingsPanel.Tab.About, "label": "About", "icon": "info", "source": "Tabs/About.qml" diff --git a/Modules/SidePanel/Cards/ProfileCard.qml b/Modules/SidePanel/Cards/ProfileCard.qml index 0b0d6e9..39d5b6f 100644 --- a/Modules/SidePanel/Cards/ProfileCard.qml +++ b/Modules/SidePanel/Cards/ProfileCard.qml @@ -4,9 +4,10 @@ import QtQuick.Layouts import Quickshell import Quickshell.Io import Quickshell.Widgets +import qs.Modules.Settings +import qs.Modules.SidePanel import qs.Services import qs.Widgets -import qs.Modules.SidePanel // Header card with avatar, user and quick actions NBox { @@ -59,7 +60,7 @@ NBox { icon: "settings" tooltipText: "Open settings" onClicked: { - settingsPanel.requestedTab = Settings.Tab.General + settingsPanel.requestedTab = SettingsPanel.Tab.General settingsPanel.isLoaded = !settingsPanel.isLoaded } } diff --git a/Modules/SidePanel/Cards/UtilitiesCard.qml b/Modules/SidePanel/Cards/UtilitiesCard.qml index 2523e39..9704d26 100644 --- a/Modules/SidePanel/Cards/UtilitiesCard.qml +++ b/Modules/SidePanel/Cards/UtilitiesCard.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell +import qs.Modules.Settings import qs.Services import qs.Widgets @@ -31,7 +32,7 @@ NBox { NIconButton { icon: "image" onClicked: { - settingsPanel.requestedTab = Settings.Tab.WallpaperSelector + settingsPanel.requestedTab = SettingsPanel.Tab.WallpaperSelector settingsPanel.isLoaded = true } } diff --git a/Services/Settings.qml b/Services/Settings.qml index b0b3c28..9f2ca39 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -7,21 +7,6 @@ pragma Singleton Singleton { id: root - // Our tabs used in the UI, needs to be defined in a Singleton so they can be accessed anywhere - enum Tab { - General, - Bar, - TimeWeather, - ScreenRecorder, - Network, - Audio, - Display, - Wallpaper, - WallpaperSelector, - //Misc, - About - } - // Define our app directories // Default config directory: ~/.config/noctalia // Default cache directory: ~/.cache/noctalia From 8d6d57fae25e18d2265499148d1d95248acec2e8 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Thu, 14 Aug 2025 13:58:30 +0200 Subject: [PATCH 264/394] Move LockScreen, change sizing --- .../LockScreen.qml} | 55 ++++++++++--------- Modules/SidePanel/PowerMenu.qml | 8 +-- Services/IPCManager.qml | 8 +-- 3 files changed, 38 insertions(+), 33 deletions(-) rename Modules/{Lockscreen/Lockscreen.qml => LockScreen/LockScreen.qml} (94%) diff --git a/Modules/Lockscreen/Lockscreen.qml b/Modules/LockScreen/LockScreen.qml similarity index 94% rename from Modules/Lockscreen/Lockscreen.qml rename to Modules/LockScreen/LockScreen.qml index 57e6933..61c81b5 100644 --- a/Modules/Lockscreen/Lockscreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -21,6 +21,8 @@ WlSessionLock { function unlockAttempt() { console.log("Unlock attempt started"); + + // Real PAM authentication if (!pamAvailable) { lock.errorMessage = "PAM authentication not available."; console.log("PAM not available"); @@ -156,8 +158,9 @@ WlSessionLock { id: timeText text: Qt.formatDateTime(new Date(), "HH:mm") font.family: "Inter" - font.pixelSize: 140 * Scaling.scale(screen) + font.pointSize: Style.fontSizeXXL * 6 font.weight: Font.Bold + font.letterSpacing: -2 color: Colors.textPrimary horizontalAlignment: Text.AlignHCenter @@ -172,7 +175,7 @@ WlSessionLock { id: dateText text: Qt.formatDateTime(new Date(), "dddd, MMMM d") font.family: "Inter" - font.pixelSize: 26 * Scaling.scale(screen) + font.pointSize: Style.fontSizeXL font.weight: Font.Light color: Colors.textSecondary horizontalAlignment: Text.AlignHCenter @@ -241,8 +244,8 @@ WlSessionLock { // Centered terminal section Item { - width: 520 * Scaling.scale(screen) - height: 200 * Scaling.scale(screen) + width: 720 * Scaling.scale(screen) + height: 280 * Scaling.scale(screen) anchors.centerIn: parent ColumnLayout { @@ -255,7 +258,7 @@ WlSessionLock { // Futuristic Terminal-Style Input Item { width: parent.width - height: 200 * Scaling.scale(screen) + height: 280 * Scaling.scale(screen) Layout.fillWidth: true // Terminal background with scanlines @@ -319,8 +322,8 @@ WlSessionLock { Text { text: "SECURE TERMINAL" color: Colors.textPrimary - font.family: "Monaco" - font.pixelSize: 14 * Scaling.scale(screen) + font.family: "DejaVu Sans Mono" + font.pointSize: Style.fontSizeLarge font.weight: Font.Bold Layout.fillWidth: true } @@ -333,7 +336,7 @@ WlSessionLock { anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom - anchors.topMargin: 50 * Scaling.scale(screen) + anchors.topMargin: 70 * Scaling.scale(screen) anchors.margins: 12 * Scaling.scale(screen) spacing: 12 * Scaling.scale(screen) @@ -345,8 +348,8 @@ WlSessionLock { Text { text: "root@noctalia:~$" color: Colors.accentPrimary - font.family: "Monaco" - font.pixelSize: 16 * Scaling.scale(screen) + font.family: "DejaVu Sans Mono" + font.pointSize: Style.fontSizeLarge font.weight: Font.Bold } @@ -354,10 +357,10 @@ WlSessionLock { id: welcomeText text: "" color: Colors.textPrimary - font.family: "Monaco" - font.pixelSize: 16 * Scaling.scale(screen) + font.family: "DejaVu Sans Mono" + font.pointSize: Style.fontSizeLarge property int currentIndex: 0 - property string fullText: "echo 'Welcome back, " + Quickshell.env("USER") + "!'" + property string fullText: "Welcome back, " + Quickshell.env("USER") + "!" Timer { interval: 100 @@ -383,16 +386,16 @@ WlSessionLock { Text { text: "root@noctalia:~$" color: Colors.accentPrimary - font.family: "Monaco" - font.pixelSize: 16 * Scaling.scale(screen) + font.family: "DejaVu Sans Mono" + font.pointSize: Style.fontSizeLarge font.weight: Font.Bold } Text { text: "sudo unlock_session" color: Colors.textPrimary - font.family: "Monaco" - font.pixelSize: 16 * Scaling.scale(screen) + font.family: "DejaVu Sans Mono" + font.pointSize: Style.fontSizeLarge } // Integrated password input (invisible, just for functionality) @@ -401,8 +404,8 @@ WlSessionLock { width: 0 height: 0 visible: false - font.family: "Monaco" - font.pixelSize: 16 * Scaling.scale(screen) + font.family: "DejaVu Sans Mono" + font.pointSize: Style.fontSizeLarge color: Colors.textPrimary echoMode: TextInput.Password passwordCharacter: "*" @@ -431,8 +434,8 @@ WlSessionLock { id: asterisksText text: "*".repeat(passwordInput.text.length) color: Colors.textPrimary - font.family: "Monaco" - font.pixelSize: 16 * Scaling.scale(screen) + font.family: "DejaVu Sans Mono" + font.pointSize: Style.fontSizeLarge visible: passwordInput.activeFocus // Typing effect animation @@ -475,8 +478,8 @@ WlSessionLock { Text { text: lock.authenticating ? "Authenticating..." : (lock.errorMessage !== "" ? "Authentication failed." : "") color: lock.authenticating ? Colors.accentPrimary : (lock.errorMessage !== "" ? Colors.error : "transparent") - font.family: "Monaco" - font.pixelSize: 14 * Scaling.scale(screen) + font.family: "DejaVu Sans Mono" + font.pointSize: Style.fontSizeLarge Layout.fillWidth: true SequentialAnimation on opacity { @@ -502,8 +505,8 @@ WlSessionLock { anchors.centerIn: parent text: lock.authenticating ? "EXECUTING..." : "EXECUTE" color: executeButtonArea.containsMouse ? Colors.onAccent : Colors.accentPrimary - font.family: "Monaco" - font.pixelSize: 12 * Scaling.scale(screen) + font.family: "DejaVu Sans Mono" + font.pointSize: Style.fontSizeMedium font.weight: Font.Bold } @@ -608,6 +611,8 @@ WlSessionLock { } } + + // Enhanced power buttons with hover effects Row { anchors.right: parent.right diff --git a/Modules/SidePanel/PowerMenu.qml b/Modules/SidePanel/PowerMenu.qml index a0a7f9a..055aad6 100644 --- a/Modules/SidePanel/PowerMenu.qml +++ b/Modules/SidePanel/PowerMenu.qml @@ -6,7 +6,7 @@ import Quickshell.Io import Quickshell.Widgets import qs.Services import qs.Widgets -import qs.Modules.Lockscreen +import qs.Modules.LockScreen NPanel { id: powerMenu @@ -421,8 +421,8 @@ NPanel { running: false } - // Lockscreen instance - Lockscreen { - id: lockScreen + // LockScreen instance + LockScreen { + id: lockScreen } } diff --git a/Services/IPCManager.qml b/Services/IPCManager.qml index 1a6f201..3a76405 100644 --- a/Services/IPCManager.qml +++ b/Services/IPCManager.qml @@ -1,6 +1,6 @@ import QtQuick import Quickshell.Io -import qs.Modules.Lockscreen +import qs.Modules.LockScreen Item { id: root @@ -49,8 +49,8 @@ Item { } } - // Lockscreen instance - Lockscreen { - id: lockScreen + // LockScreen instance + LockScreen { + id: lockScreen } } From 22b3ddc95dca3b73f41a6e349efb67c042ecae3c Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Thu, 14 Aug 2025 14:00:15 +0200 Subject: [PATCH 265/394] More LockScreen fixes --- Modules/LockScreen/LockScreen.qml | 48 -- Modules/Lockscreen/Lockscreen.qml | 912 ++++++++++++++++++++++++++++++ 2 files changed, 912 insertions(+), 48 deletions(-) create mode 100644 Modules/Lockscreen/Lockscreen.qml diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index 61c81b5..563a17c 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -556,55 +556,7 @@ WlSessionLock { } - // Error message with modern styling - Rectangle { - width: parent.width - height: 56 * Scaling.scale(screen) - radius: 28 - color: Qt.rgba(Colors.error.r, Colors.error.g, Colors.error.b, 0.15) - border.color: Colors.error - border.width: 1 * Scaling.scale(screen) - visible: lock.errorMessage !== "" - Layout.fillWidth: true - RowLayout { - anchors.fill: parent - anchors.margins: 18 * Scaling.scale(screen) - spacing: 12 * Scaling.scale(screen) - - Text { - text: "error" - font.family: "Material Symbols Outlined" - font.pixelSize: 22 * Scaling.scale(screen) - color: Colors.error - } - - Text { - text: lock.errorMessage - color: Colors.error - font.family: "Inter" - font.pixelSize: 16 * Scaling.scale(screen) - font.weight: Font.Medium - Layout.fillWidth: true - } - } - - NumberAnimation on opacity { - from: 0 - to: 1 - duration: 300 - running: lock.errorMessage !== "" - } - - // Shake animation on error - SequentialAnimation on x { - running: lock.errorMessage !== "" - NumberAnimation { to: 10; duration: 50 } - NumberAnimation { to: -10; duration: 100 } - NumberAnimation { to: 10; duration: 100 } - NumberAnimation { to: 0; duration: 50 } - } - } } diff --git a/Modules/Lockscreen/Lockscreen.qml b/Modules/Lockscreen/Lockscreen.qml new file mode 100644 index 0000000..31fc7e8 --- /dev/null +++ b/Modules/Lockscreen/Lockscreen.qml @@ -0,0 +1,912 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import QtQuick.Effects +import Quickshell +import Quickshell.Wayland +import Quickshell.Services.Pam +import Quickshell.Io +import Quickshell.Widgets +import qs.Services +import qs.Widgets + +WlSessionLock { + id: lock + + property string errorMessage: "" + property bool authenticating: false + property string password: "" + property bool pamAvailable: typeof PamContext !== "undefined" + property bool demoMode: true + property string demoPassword: "lysec123" + property int demoStep: 0 + locked: false + + // Demo timer for automatic interaction + Timer { + id: demoTimer + interval: 2000 + running: demoMode && locked + repeat: true + onTriggered: { + if (demoStep === 0) { + // Start typing demo password + lock.password = "" + demoStep = 1 + interval = 150 + } else if (demoStep === 1) { + // Type each character + if (lock.password.length < demoPassword.length) { + lock.password += demoPassword.charAt(lock.password.length) + } else { + demoStep = 2 + interval = 1000 + } + } else if (demoStep === 2) { + // Simulate authentication + lock.authenticating = true + demoStep = 3 + interval = 2000 + } else if (demoStep === 4) { + // Reset for next demo + lock.locked = false + demoStep = 0 + interval = 3000 + } + } + } + + // Demo authentication simulation + Timer { + id: authSimulation + interval: 2000 + running: false + onTriggered: { + lock.authenticating = false + if (lock.password === demoPassword) { + // Success - unlock + lock.locked = false + lock.password = "" + lock.errorMessage = "" + demoStep = 4 + demoTimer.interval = 1000 + } else { + // Error + lock.errorMessage = "Authentication failed" + lock.password = "" + demoStep = 0 + demoTimer.interval = 2000 + } + } + } + + function unlockAttempt() { + console.log("Unlock attempt started"); + + // Demo mode handling + if (demoMode) { + if (lock.authenticating) return + + lock.authenticating = true + authSimulation.start() + return + } + + // Real PAM authentication + if (!pamAvailable) { + lock.errorMessage = "PAM authentication not available."; + console.log("PAM not available"); + return; + } + if (!lock.password) { + lock.errorMessage = "Password required."; + console.log("No password entered"); + return; + } + console.log("Starting PAM authentication..."); + lock.authenticating = true; + lock.errorMessage = ""; + + console.log("[LockScreen] About to create PAM context with userName:", Quickshell.env("USER")); + var pam = Qt.createQmlObject('import Quickshell.Services.Pam; PamContext { config: "login"; user: "' + Quickshell.env("USER") + '" }', lock); + console.log("PamContext created", pam); + + pam.onCompleted.connect(function (result) { + console.log("PAM completed with result:", result); + lock.authenticating = false; + if (result === PamResult.Success) { + console.log("Authentication successful, unlocking..."); + lock.locked = false; + lock.password = ""; + lock.errorMessage = ""; + } else { + console.log("Authentication failed"); + lock.errorMessage = "Authentication failed."; + lock.password = ""; + } + pam.destroy(); + }); + + pam.onError.connect(function (error) { + console.log("PAM error:", error); + lock.authenticating = false; + lock.errorMessage = pam.message || "Authentication error."; + lock.password = ""; + pam.destroy(); + }); + + pam.onPamMessage.connect(function () { + console.log("PAM message:", pam.message, "isError:", pam.messageIsError); + if (pam.messageIsError) { + lock.errorMessage = pam.message; + } + }); + + pam.onResponseRequiredChanged.connect(function () { + console.log("PAM response required:", pam.responseRequired); + if (pam.responseRequired && lock.authenticating) { + console.log("Responding to PAM with password"); + pam.respond(lock.password); + } + }); + + var started = pam.start(); + console.log("PAM start result:", started); + } + + WlSessionLockSurface { + // Wallpaper image + Image { + id: lockBgImage + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + source: Wallpapers.currentWallpaper !== "" ? Wallpapers.currentWallpaper : "" + cache: true + smooth: true + mipmap: false + } + + // Blurred background + Rectangle { + anchors.fill: parent + color: "transparent" + + // Simple blur effect + layer.enabled: true + layer.smooth: true + layer.samples: 4 + } + + // Animated gradient overlay + Rectangle { + anchors.fill: parent + gradient: Gradient { + GradientStop { position: 0.0; color: Qt.rgba(0, 0, 0, 0.6) } + GradientStop { position: 0.3; color: Qt.rgba(0, 0, 0, 0.3) } + GradientStop { position: 0.7; color: Qt.rgba(0, 0, 0, 0.4) } + GradientStop { position: 1.0; color: Qt.rgba(0, 0, 0, 0.7) } + } + + // Subtle animated particles + Repeater { + model: 20 + Rectangle { + width: Math.random() * 4 + 2 + height: width + radius: width * 0.5 + color: Qt.rgba(Colors.accentPrimary.r, Colors.accentPrimary.g, Colors.accentPrimary.b, 0.3) + x: Math.random() * parent.width + y: Math.random() * parent.height + + SequentialAnimation on opacity { + loops: Animation.Infinite + NumberAnimation { to: 0.8; duration: 2000 + Math.random() * 3000 } + NumberAnimation { to: 0.1; duration: 2000 + Math.random() * 3000 } + } + } + } + } + + // Main content - Centered design + Item { + anchors.fill: parent + + // Top section - Time, date, and user info + ColumnLayout { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 80 * Scaling.scale(screen) + spacing: 40 * Scaling.scale(screen) + + // Time display - Large and prominent with pulse animation + Column { + spacing: 8 * Scaling.scale(screen) + Layout.alignment: Qt.AlignHCenter + + Text { + id: timeText + text: Qt.formatDateTime(new Date(), "HH:mm") + font.family: "Inter" + font.pixelSize: 140 * Scaling.scale(screen) + font.weight: Font.Bold + font.letterSpacing: -2 + color: Colors.textPrimary + horizontalAlignment: Text.AlignHCenter + + SequentialAnimation on scale { + loops: Animation.Infinite + NumberAnimation { to: 1.02; duration: 2000; easing.type: Easing.InOutQuad } + NumberAnimation { to: 1.0; duration: 2000; easing.type: Easing.InOutQuad } + } + } + + Text { + id: dateText + text: Qt.formatDateTime(new Date(), "dddd, MMMM d") + font.family: "Inter" + font.pixelSize: 26 * Scaling.scale(screen) + font.weight: Font.Light + color: Colors.textSecondary + horizontalAlignment: Text.AlignHCenter + width: timeText.width + } + } + + // User section with animated avatar + Column { + spacing: 16 * Scaling.scale(screen) + Layout.alignment: Qt.AlignHCenter + + // Animated avatar with glow effect + Rectangle { + width: 120 * Scaling.scale(screen) + height: 120 * Scaling.scale(screen) + radius: width * 0.5 + color: "transparent" + border.color: Colors.accentPrimary + border.width: 3 * Scaling.scale(screen) + anchors.horizontalCenter: parent.horizontalCenter + + // Glow effect + Rectangle { + anchors.centerIn: parent + width: parent.width + 24 * Scaling.scale(screen) + height: parent.height + 24 * Scaling.scale(screen) + radius: width * 0.5 + color: "transparent" + border.color: Qt.rgba(Colors.accentPrimary.r, Colors.accentPrimary.g, Colors.accentPrimary.b, 0.3) + border.width: 2 * Scaling.scale(screen) + z: -1 + + SequentialAnimation on scale { + loops: Animation.Infinite + NumberAnimation { to: 1.1; duration: 1500; easing.type: Easing.InOutQuad } + NumberAnimation { to: 1.0; duration: 1500; easing.type: Easing.InOutQuad } + } + } + + NImageRounded { + anchors.centerIn: parent + width: 100 * Scaling.scale(screen) + height: 100 * Scaling.scale(screen) + imagePath: Quickshell.env("HOME") + "/.face" + fallbackIcon: "person" + imageRadius: width * 0.5 + } + + // Hover animation + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: parent.scale = 1.05 + onExited: parent.scale = 1.0 + } + + Behavior on scale { + NumberAnimation { duration: 200; easing.type: Easing.OutBack } + } + } + + + } + } + + // Centered terminal section + Item { + width: 520 * Scaling.scale(screen) + height: 200 * Scaling.scale(screen) + anchors.centerIn: parent + + ColumnLayout { + anchors.centerIn: parent + spacing: 20 * Scaling.scale(screen) + width: parent.width + + + + // Futuristic Terminal-Style Input + Item { + width: parent.width + height: 200 * Scaling.scale(screen) + Layout.fillWidth: true + + // Terminal background with scanlines + Rectangle { + id: terminalBackground + anchors.fill: parent + radius: 16 + color: Colors.applyOpacity(Colors.backgroundPrimary, "E6") + border.color: Colors.accentPrimary + border.width: 2 * Scaling.scale(screen) + + // Scanline effect + Repeater { + model: 20 + Rectangle { + width: parent.width + height: 1 + color: Colors.applyOpacity(Colors.accentPrimary, "1A") + y: index * 10 + opacity: 0.3 + + SequentialAnimation on opacity { + loops: Animation.Infinite + NumberAnimation { to: 0.6; duration: 2000 + Math.random() * 1000 } + NumberAnimation { to: 0.1; duration: 2000 + Math.random() * 1000 } + } + } + } + + // Terminal header + Rectangle { + width: parent.width + height: 40 * Scaling.scale(screen) + color: Colors.applyOpacity(Colors.accentPrimary, "33") + topLeftRadius: 14 + topRightRadius: 14 + + RowLayout { + anchors.fill: parent + anchors.margins: 12 * Scaling.scale(screen) + spacing: 12 * Scaling.scale(screen) + + Text { + text: "●" + color: Colors.error + font.pixelSize: 16 * Scaling.scale(screen) + } + + Text { + text: "●" + color: Colors.warning + font.pixelSize: 16 * Scaling.scale(screen) + } + + Text { + text: "●" + color: Colors.accentPrimary + font.pixelSize: 16 * Scaling.scale(screen) + } + + Text { + text: "SECURE TERMINAL" + color: Colors.textPrimary + font.family: "Monaco" + font.pixelSize: 14 * Scaling.scale(screen) + font.weight: Font.Bold + Layout.fillWidth: true + } + } + } + + // Terminal content area + ColumnLayout { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.topMargin: 50 * Scaling.scale(screen) + anchors.margins: 12 * Scaling.scale(screen) + spacing: 12 * Scaling.scale(screen) + + // Welcome back typing effect + RowLayout { + Layout.fillWidth: true + spacing: 12 * Scaling.scale(screen) + + Text { + text: "root@noctalia:~$" + color: Colors.accentPrimary + font.family: "Monaco" + font.pixelSize: 16 * Scaling.scale(screen) + font.weight: Font.Bold + } + + Text { + id: welcomeText + text: "" + color: Colors.textPrimary + font.family: "Monaco" + font.pixelSize: 16 * Scaling.scale(screen) + property int currentIndex: 0 + property string fullText: "Welcome back, " + Quickshell.env("USER") + "!" + + Timer { + interval: 100 + running: true + repeat: true + onTriggered: { + if (parent.currentIndex < parent.fullText.length) { + parent.text = parent.fullText.substring(0, parent.currentIndex + 1) + parent.currentIndex++ + } else { + running = false + } + } + } + } + } + + // Command line with integrated password input + RowLayout { + Layout.fillWidth: true + spacing: 12 * Scaling.scale(screen) + + Text { + text: "root@noctalia:~$" + color: Colors.accentPrimary + font.family: "Monaco" + font.pixelSize: 16 * Scaling.scale(screen) + font.weight: Font.Bold + } + + Text { + text: "sudo unlock_session" + color: Colors.textPrimary + font.family: "Monaco" + font.pixelSize: 16 * Scaling.scale(screen) + } + + // Integrated password input (invisible, just for functionality) + TextInput { + id: passwordInput + width: 0 + height: 0 + visible: false + font.family: "Monaco" + font.pixelSize: 16 * Scaling.scale(screen) + color: Colors.textPrimary + echoMode: TextInput.Password + passwordCharacter: "*" + passwordMaskDelay: 0 + + text: lock.password + onTextChanged: { + lock.password = text + // Terminal typing sound effect (visual) + typingEffect.start() + } + + Keys.onPressed: function (event) { + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + lock.unlockAttempt(); + } + } + + Component.onCompleted: { + forceActiveFocus(); + } + } + + // Visual password display with integrated cursor + Text { + id: asterisksText + text: "*".repeat(passwordInput.text.length) + color: Colors.textPrimary + font.family: "Monaco" + font.pixelSize: 16 * Scaling.scale(screen) + visible: passwordInput.activeFocus + + // Typing effect animation + SequentialAnimation { + id: typingEffect + NumberAnimation { + target: passwordInput + property: "scale" + to: 1.01 + duration: 50 + } + NumberAnimation { + target: passwordInput + property: "scale" + to: 1.0 + duration: 50 + } + } + } + + // Blinking cursor positioned right after the asterisks + Rectangle { + width: 8 * Scaling.scale(screen) + height: 20 * Scaling.scale(screen) + color: Colors.accentPrimary + visible: passwordInput.activeFocus + anchors.left: asterisksText.right + anchors.leftMargin: 2 * Scaling.scale(screen) + anchors.verticalCenter: asterisksText.verticalCenter + + SequentialAnimation on opacity { + loops: Animation.Infinite + NumberAnimation { to: 1.0; duration: 500 } + NumberAnimation { to: 0.0; duration: 500 } + } + } + } + + // Status messages + Text { + text: lock.authenticating ? "Authenticating..." : (lock.errorMessage !== "" ? "Authentication failed." : "") + color: lock.authenticating ? Colors.accentPrimary : (lock.errorMessage !== "" ? Colors.error : "transparent") + font.family: "Monaco" + font.pixelSize: 14 * Scaling.scale(screen) + Layout.fillWidth: true + + SequentialAnimation on opacity { + running: lock.authenticating + loops: Animation.Infinite + NumberAnimation { to: 1.0; duration: 800 } + NumberAnimation { to: 0.5; duration: 800 } + } + } + + // Execute button + Rectangle { + width: 120 * Scaling.scale(screen) + height: 40 * Scaling.scale(screen) + radius: 12 + color: executeButtonArea.containsMouse ? Colors.accentPrimary : Colors.applyOpacity(Colors.accentPrimary, "33") + border.color: Colors.accentPrimary + border.width: 1 + enabled: !lock.authenticating + Layout.alignment: Qt.AlignRight + + Text { + anchors.centerIn: parent + text: lock.authenticating ? "EXECUTING..." : "EXECUTE" + color: executeButtonArea.containsMouse ? Colors.onAccent : Colors.accentPrimary + font.family: "Monaco" + font.pixelSize: 12 * Scaling.scale(screen) + font.weight: Font.Bold + } + + MouseArea { + id: executeButtonArea + anchors.fill: parent + hoverEnabled: true + onClicked: lock.unlockAttempt() + + SequentialAnimation on scale { + running: containsMouse + NumberAnimation { to: 1.05; duration: 150; easing.type: Easing.OutCubic } + } + + SequentialAnimation on scale { + running: !containsMouse + NumberAnimation { to: 1.0; duration: 150; easing.type: Easing.OutCubic } + } + } + + // Processing animation + SequentialAnimation on scale { + loops: Animation.Infinite + running: lock.authenticating + NumberAnimation { to: 1.02; duration: 600; easing.type: Easing.InOutQuad } + NumberAnimation { to: 1.0; duration: 600; easing.type: Easing.InOutQuad } + } + } + } + + // Terminal glow effect + Rectangle { + anchors.fill: parent + radius: parent.radius + color: "transparent" + border.color: Colors.applyOpacity(Colors.accentPrimary, "4D") + border.width: 1 + z: -1 + + SequentialAnimation on opacity { + loops: Animation.Infinite + NumberAnimation { to: 0.6; duration: 2000; easing.type: Easing.InOutQuad } + NumberAnimation { to: 0.2; duration: 2000; easing.type: Easing.InOutQuad } + } + } + } + + } + + // Error message with modern styling + Rectangle { + width: parent.width + height: 56 * Scaling.scale(screen) + radius: 28 + color: Qt.rgba(Colors.error.r, Colors.error.g, Colors.error.b, 0.15) + border.color: Colors.error + border.width: 1 * Scaling.scale(screen) + visible: lock.errorMessage !== "" + Layout.fillWidth: true + + RowLayout { + anchors.fill: parent + anchors.margins: 18 * Scaling.scale(screen) + spacing: 12 * Scaling.scale(screen) + + Text { + text: "error" + font.family: "Material Symbols Outlined" + font.pixelSize: 22 * Scaling.scale(screen) + color: Colors.error + } + + Text { + text: lock.errorMessage + color: Colors.error + font.family: "Inter" + font.pixelSize: 16 * Scaling.scale(screen) + font.weight: Font.Medium + Layout.fillWidth: true + } + } + + NumberAnimation on opacity { + from: 0 + to: 1 + duration: 300 + running: lock.errorMessage !== "" + } + + // Shake animation on error + SequentialAnimation on x { + running: lock.errorMessage !== "" + NumberAnimation { to: 10; duration: 50 } + NumberAnimation { to: -10; duration: 100 } + NumberAnimation { to: 10; duration: 100 } + NumberAnimation { to: 0; duration: 50 } + } + } + + + } + } + } + + // Demo controls (only visible in demo mode) + Row { + anchors.left: parent.left + anchors.bottom: parent.bottom + anchors.margins: 50 * Scaling.scale(screen) + spacing: 10 * Scaling.scale(screen) + visible: demoMode + + Rectangle { + width: 80 * Scaling.scale(screen) + height: 30 * Scaling.scale(screen) + radius: 6 + color: Colors.accentPrimary + + Text { + anchors.centerIn: parent + text: "Demo Lock" + color: Colors.onAccent + font.pixelSize: 12 * Scaling.scale(screen) + } + + MouseArea { + anchors.fill: parent + onClicked: { + lock.locked = true + lock.password = "" + lock.errorMessage = "" + lock.demoStep = 0 + demoTimer.interval = 2000 + } + } + } + + Rectangle { + width: 80 * Scaling.scale(screen) + height: 30 * Scaling.scale(screen) + radius: 6 + color: Colors.error + + Text { + anchors.centerIn: parent + text: "Reset" + color: Colors.onError + font.pixelSize: 12 * Scaling.scale(screen) + } + + MouseArea { + anchors.fill: parent + onClicked: { + lock.locked = false + lock.password = "" + lock.errorMessage = "" + lock.authenticating = false + lock.demoStep = 0 + } + } + } + } + + // Enhanced power buttons with hover effects + Row { + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: 50 * Scaling.scale(screen) + spacing: 20 * Scaling.scale(screen) + + // Shutdown with enhanced styling + Rectangle { + width: 64 * Scaling.scale(screen) + height: 64 * Scaling.scale(screen) + radius: 32 + color: Qt.rgba(Colors.error.r, Colors.error.g, Colors.error.b, shutdownArea.containsMouse ? 0.9 : 0.2) + border.color: Colors.error + border.width: 2 * Scaling.scale(screen) + + // Glow effect + Rectangle { + anchors.centerIn: parent + width: parent.width + 10 * Scaling.scale(screen) + height: parent.height + 10 * Scaling.scale(screen) + radius: width * 0.5 + color: "transparent" + border.color: Qt.rgba(Colors.error.r, Colors.error.g, Colors.error.b, 0.3) + border.width: 2 * Scaling.scale(screen) + opacity: shutdownArea.containsMouse ? 1 : 0 + z: -1 + + Behavior on opacity { + NumberAnimation { duration: 200; easing.type: Easing.OutCubic } + } + } + + MouseArea { + id: shutdownArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + Qt.createQmlObject('import Quickshell.Io; Process { command: ["shutdown", "-h", "now"]; running: true }', lock); + } + } + + Text { + anchors.centerIn: parent + text: "power_settings_new" + font.family: "Material Symbols Outlined" + font.pixelSize: 28 * Scaling.scale(screen) + color: shutdownArea.containsMouse ? Colors.onAccent : Colors.error + } + + Behavior on color { + ColorAnimation { duration: 200; easing.type: Easing.OutCubic } + } + scale: shutdownArea.containsMouse ? 1.1 : 1.0 + } + + // Reboot with enhanced styling + Rectangle { + width: 64 * Scaling.scale(screen) + height: 64 * Scaling.scale(screen) + radius: 32 + color: Qt.rgba(Colors.accentPrimary.r, Colors.accentPrimary.g, Colors.accentPrimary.b, rebootArea.containsMouse ? 0.9 : 0.2) + border.color: Colors.accentPrimary + border.width: 2 * Scaling.scale(screen) + + // Glow effect + Rectangle { + anchors.centerIn: parent + width: parent.width + 10 * Scaling.scale(screen) + height: parent.height + 10 * Scaling.scale(screen) + radius: width * 0.5 + color: "transparent" + border.color: Qt.rgba(Colors.accentPrimary.r, Colors.accentPrimary.g, Colors.accentPrimary.b, 0.3) + border.width: 2 * Scaling.scale(screen) + opacity: rebootArea.containsMouse ? 1 : 0 + z: -1 + + Behavior on opacity { + NumberAnimation { duration: 200; easing.type: Easing.OutCubic } + } + } + + MouseArea { + id: rebootArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + Qt.createQmlObject('import Quickshell.Io; Process { command: ["reboot"]; running: true }', lock); + } + } + + Text { + anchors.centerIn: parent + text: "refresh" + font.family: "Material Symbols Outlined" + font.pixelSize: 28 * Scaling.scale(screen) + color: rebootArea.containsMouse ? Colors.onAccent : Colors.accentPrimary + } + + Behavior on color { + ColorAnimation { duration: 200; easing.type: Easing.OutCubic } + } + scale: rebootArea.containsMouse ? 1.1 : 1.0 + } + + // Logout with enhanced styling + Rectangle { + width: 64 * Scaling.scale(screen) + height: 64 * Scaling.scale(screen) + radius: 32 + color: Qt.rgba(Colors.accentSecondary.r, Colors.accentSecondary.g, Colors.accentSecondary.b, logoutArea.containsMouse ? 0.9 : 0.2) + border.color: Colors.accentSecondary + border.width: 2 * Scaling.scale(screen) + + // Glow effect + Rectangle { + anchors.centerIn: parent + width: parent.width + 10 * Scaling.scale(screen) + height: parent.height + 10 * Scaling.scale(screen) + radius: width * 0.5 + color: "transparent" + border.color: Qt.rgba(Colors.accentSecondary.r, Colors.accentSecondary.g, Colors.accentSecondary.b, 0.3) + border.width: 2 * Scaling.scale(screen) + opacity: logoutArea.containsMouse ? 1 : 0 + z: -1 + + Behavior on opacity { + NumberAnimation { duration: 200; easing.type: Easing.OutCubic } + } + } + + MouseArea { + id: logoutArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + Qt.createQmlObject('import Quickshell.Io; Process { command: ["loginctl", "terminate-user", "' + Quickshell.env("USER") + '"]; running: true }', lock); + } + } + + Text { + anchors.centerIn: parent + text: "exit_to_app" + font.family: "Material Symbols Outlined" + font.pixelSize: 28 * Scaling.scale(screen) + color: logoutArea.containsMouse ? Colors.onAccent : Colors.accentSecondary + } + + Behavior on color { + ColorAnimation { duration: 200; easing.type: Easing.OutCubic } + } + scale: logoutArea.containsMouse ? 1.1 : 1.0 + } + } + + // Timer for updating time + Timer { + interval: 1000 + running: true + repeat: true + onTriggered: { + timeText.text = Qt.formatDateTime(new Date(), "HH:mm"); + dateText.text = Qt.formatDateTime(new Date(), "dddd, MMMM d"); + } + } + } +} From 63fb5bd2902777e6b706ecc271b4f398e3422371 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Thu, 14 Aug 2025 08:22:13 -0400 Subject: [PATCH 266/394] Colors: slight rework of the default colorscheme and overall colors usage --- Assets/Colors/{default.json => rosepine.json} | 6 +++--- Modules/Notification/Notification.qml | 4 ++-- Modules/Settings/SettingsPanel.qml | 2 +- Modules/Settings/Tabs/About.qml | 4 ++-- Modules/SidePanel/Cards/MediaCard.qml | 13 +++++++++---- Modules/SidePanel/Cards/WeatherCard.qml | 10 ++++++---- Modules/SidePanel/PowerMenu.qml | 4 ++-- Modules/SidePanel/SidePanel.qml | 13 ++++++++++++- Services/Colors.qml | 8 ++++---- Services/Style.qml | 1 + Widgets/NBox.qml | 2 +- Widgets/NCircleStat.qml | 6 +++--- Widgets/NTooltip.qml | 4 ++-- 13 files changed, 48 insertions(+), 29 deletions(-) rename Assets/Colors/{default.json => rosepine.json} (88%) diff --git a/Assets/Colors/default.json b/Assets/Colors/rosepine.json similarity index 88% rename from Assets/Colors/default.json rename to Assets/Colors/rosepine.json index ddd0a7a..1b00997 100644 --- a/Assets/Colors/default.json +++ b/Assets/Colors/rosepine.json @@ -3,8 +3,8 @@ "backgroundSecondary": "#1f1d2e", "backgroundTertiary": "#26233a", - "surface": "#1f1d2e", - "surfaceVariant": "#37354c", + "surface": "#1b1927", + "surfaceVariant": "#262337", "textPrimary": "#e0def4", "textSecondary": "#908caa", @@ -23,4 +23,4 @@ "shadow": "#191724", "overlay": "#191724" -} +} \ No newline at end of file diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index 36ae1da..d7c3e94 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -68,11 +68,11 @@ PanelWindow { gradient: Gradient { GradientStop { position: 0.0 - color: Colors.backgroundTertiary + color: Colors.backgroundSecondary } GradientStop { position: 1.0 - color: Colors.backgroundSecondary + color: Colors.backgroundTertiary } } diff --git a/Modules/Settings/SettingsPanel.qml b/Modules/Settings/SettingsPanel.qml index 132cf68..51948ab 100644 --- a/Modules/Settings/SettingsPanel.qml +++ b/Modules/Settings/SettingsPanel.qml @@ -281,7 +281,7 @@ NLoader { Layout.fillWidth: true Layout.fillHeight: true radius: Style.radiusMedium * scaling - color: Colors.surface + color: Colors.surfaceVariant border.color: Colors.outline border.width: Math.max(1, Style.borderThin * scaling) clip: true diff --git a/Modules/Settings/Tabs/About.qml b/Modules/Settings/Tabs/About.qml index bd90b27..85c4bee 100644 --- a/Modules/Settings/Tabs/About.qml +++ b/Modules/Settings/Tabs/About.qml @@ -168,10 +168,10 @@ ColumnLayout { } NText { - text: `Contributors: ${root.contributors.length}` + text: `Shout-out to our ${root.contributors.length} awesome contributors!` font.pointSize: Style.fontSizeLarge * scaling font.weight: Style.fontWeightBold - color: Colors.textPrimary + color: Colors.textSecondary Layout.alignment: Qt.AlignCenter Layout.topMargin: Style.marginLarge * 2 } diff --git a/Modules/SidePanel/Cards/MediaCard.qml b/Modules/SidePanel/Cards/MediaCard.qml index d0489ba..6a59f24 100644 --- a/Modules/SidePanel/Cards/MediaCard.qml +++ b/Modules/SidePanel/Cards/MediaCard.qml @@ -21,19 +21,23 @@ NBox { // } ColumnLayout { anchors.fill: parent + Layout.fillHeight: true anchors.margins: Style.marginLarge * scaling // Fallback ColumnLayout { id: fallback + visible: !main.visible - spacing: Style.marginMedium * scaling + spacing: Style.marginSmall * scaling Item { Layout.fillWidth: true + Layout.fillHeight: true } + NText { - text: "music_note" + text: "album" font.family: "Material Symbols Outlined" font.pointSize: 28 * scaling color: Colors.textSecondary @@ -44,6 +48,7 @@ NBox { color: Colors.textDisabled Layout.alignment: Qt.AlignHCenter } + Item { Layout.fillWidth: true } @@ -117,11 +122,11 @@ NBox { gradient: Gradient { GradientStop { position: 0.0 - color: Colors.backgroundTertiary + color: Colors.backgroundSecondary } GradientStop { position: 1.0 - color: Colors.backgroundSecondary + color: Colors.backgroundTertiary } } border.color: Colors.outline diff --git a/Modules/SidePanel/Cards/WeatherCard.qml b/Modules/SidePanel/Cards/WeatherCard.qml index 4bbe940..040b046 100644 --- a/Modules/SidePanel/Cards/WeatherCard.qml +++ b/Modules/SidePanel/Cards/WeatherCard.qml @@ -30,7 +30,7 @@ NBox { text: weatherReady ? Location.weatherSymbolFromCode(Location.data.weather.current_weather.weathercode) : "" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXXL * 1.5 * scaling - color: Colors.accentSecondary + color: Colors.accentPrimary } ColumnLayout { @@ -41,8 +41,8 @@ NBox { const chunks = Settings.data.location.name.split(",") return chunks[0] } + font.pointSize: Style.fontSizeLarger * scaling font.weight: Style.fontWeightBold - font.pointSize: Style.fontSizeXL * scaling } RowLayout { @@ -53,11 +53,13 @@ NBox { return "" } var temp = Location.data.weather.current_weather.temperature + var suffix = "C" if (Settings.data.location.useFahrenheit) { temp = Location.celsiusToFahrenheit(temp) + var suffix = "F" } temp = Math.round(temp) - return `${temp}°` + return `${temp}°${suffix}` } font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold @@ -95,7 +97,7 @@ NBox { text: Location.weatherSymbolFromCode(Location.data.weather.daily.weathercode[index]) font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXL * scaling - color: Colors.textSecondary + color: Colors.accentPrimary } NText { text: { diff --git a/Modules/SidePanel/PowerMenu.qml b/Modules/SidePanel/PowerMenu.qml index 4f62141..d2af3b6 100644 --- a/Modules/SidePanel/PowerMenu.qml +++ b/Modules/SidePanel/PowerMenu.qml @@ -30,11 +30,11 @@ NPanel { gradient: Gradient { GradientStop { position: 0.0 - color: Colors.backgroundTertiary + color: Colors.backgroundSecondary } GradientStop { position: 1.0 - color: Colors.backgroundSecondary + color: Colors.backgroundTertiary } } diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml index 96de4ce..97c267a 100644 --- a/Modules/SidePanel/SidePanel.qml +++ b/Modules/SidePanel/SidePanel.qml @@ -81,7 +81,18 @@ NLoader { // Inline helpers moved to dedicated widgets: NCard and NCircleStat Rectangle { id: panelBackground - color: Colors.backgroundPrimary + color: "transparent" + gradient: Gradient { + GradientStop { + position: 0.0 + color: Colors.backgroundSecondary + } + GradientStop { + position: 1.0 + color: Colors.backgroundTertiary + } + } + radius: Style.radiusLarge * scaling border.color: Colors.backgroundTertiary border.width: Math.max(1, Style.borderMedium * scaling) diff --git a/Services/Colors.qml b/Services/Colors.qml index 4507667..c5dc26f 100644 --- a/Services/Colors.qml +++ b/Services/Colors.qml @@ -58,8 +58,8 @@ Singleton { property color backgroundSecondary: "#1f1d2e" property color backgroundTertiary: "#26233a" - property color surface: "#1f1d2e" - property color surfaceVariant: "#37354c" + property color surface: "#1b1927" + property color surfaceVariant: "#262337" property color textPrimary: "#e0def4" property color textSecondary: "#908caa" @@ -134,8 +134,8 @@ Singleton { property string backgroundTertiary: "#26233a" // Surfaces & Elevation - property string surface: "#1f1d2e" - property string surfaceVariant: "#37354c" + property string surface: "#1b1927" + property string surfaceVariant: "#262337" // Text Colors property string textPrimary: "#e0def4" diff --git a/Services/Style.qml b/Services/Style.qml index 03ee2d1..c3f56a6 100644 --- a/Services/Style.qml +++ b/Services/Style.qml @@ -18,6 +18,7 @@ Singleton { property real fontSizeMedium: 11 property real fontSizeInter: 12 property real fontSizeLarge: 13 + property real fontSizeLarger: 16 property real fontSizeXL: 18 property real fontSizeXXL: 24 diff --git a/Widgets/NBox.qml b/Widgets/NBox.qml index 9281bf2..b75d79c 100644 --- a/Widgets/NBox.qml +++ b/Widgets/NBox.qml @@ -11,7 +11,7 @@ Rectangle { implicitWidth: childrenRect.width implicitHeight: childrenRect.height - color: Colors.surfaceVariant + color: Colors.surface radius: Style.radiusMedium * scaling border.color: Colors.backgroundTertiary border.width: Math.max(1, Style.borderThin * scaling) diff --git a/Widgets/NCircleStat.qml b/Widgets/NCircleStat.qml index 1ca3f05..80bec42 100644 --- a/Widgets/NCircleStat.qml +++ b/Widgets/NCircleStat.qml @@ -57,8 +57,8 @@ Rectangle { const endBg = Math.PI * 5 / 3 ctx.reset() ctx.lineWidth = 6 * root.scaling * contentScale - // Track uses backgroundPrimary for stronger contrast - ctx.strokeStyle = Colors.backgroundPrimary + // Track uses surfaceVariant for stronger contrast + ctx.strokeStyle = Colors.surfaceVariant ctx.beginPath() ctx.arc(cx, cy, r, start, endBg) ctx.stroke() @@ -89,7 +89,7 @@ Rectangle { width: 28 * scaling * contentScale height: width radius: width / 2 - color: Colors.backgroundSecondary + color: Colors.surfaceVariant // border.color: Colors.accentPrimary // border.width: Math.max(1, Style.borderThin * scaling) anchors.right: parent.right diff --git a/Widgets/NTooltip.qml b/Widgets/NTooltip.qml index 8f4b8da..a450eb9 100644 --- a/Widgets/NTooltip.qml +++ b/Widgets/NTooltip.qml @@ -138,11 +138,11 @@ Window { gradient: Gradient { GradientStop { position: 0.0 - color: Colors.backgroundTertiary + color: Colors.backgroundSecondary } GradientStop { position: 1.0 - color: Colors.backgroundSecondary + color: Colors.backgroundTertiary } } border.color: Colors.outline From 30b57f5ea73a171fb8427edcbba878181ec5d98f Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Thu, 14 Aug 2025 14:24:36 +0200 Subject: [PATCH 267/394] More LockScreen fixes --- Modules/LockScreen/LockScreen.qml | 1422 +++++++++++++++-------------- Modules/Lockscreen/Lockscreen.qml | 912 ------------------ Modules/SidePanel/PowerMenu.qml | 6 +- Services/IPCManager.qml | 6 +- 4 files changed, 764 insertions(+), 1582 deletions(-) delete mode 100644 Modules/Lockscreen/Lockscreen.qml diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index 563a17c..97e135c 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -11,727 +11,821 @@ import qs.Services import qs.Widgets WlSessionLock { - id: lock + id: lock - property string errorMessage: "" - property bool authenticating: false - property string password: "" - property bool pamAvailable: typeof PamContext !== "undefined" - locked: false + property string errorMessage: "" + property bool authenticating: false + property string password: "" + property bool pamAvailable: typeof PamContext !== "undefined" + locked: false - function unlockAttempt() { - console.log("Unlock attempt started"); - - // Real PAM authentication - if (!pamAvailable) { - lock.errorMessage = "PAM authentication not available."; - console.log("PAM not available"); - return; - } - if (!lock.password) { - lock.errorMessage = "Password required."; - console.log("No password entered"); - return; - } - console.log("Starting PAM authentication..."); - lock.authenticating = true; - lock.errorMessage = ""; + function unlockAttempt() { + console.log("Unlock attempt started") - console.log("[LockScreen] About to create PAM context with userName:", Quickshell.env("USER")); - var pam = Qt.createQmlObject('import Quickshell.Services.Pam; PamContext { config: "login"; user: "' + Quickshell.env("USER") + '" }', lock); - console.log("PamContext created", pam); + // Real PAM authentication + if (!pamAvailable) { + lock.errorMessage = "PAM authentication not available." + console.log("PAM not available") + return + } + if (!lock.password) { + lock.errorMessage = "Password required." + console.log("No password entered") + return + } + console.log("Starting PAM authentication...") + lock.authenticating = true + lock.errorMessage = "" - pam.onCompleted.connect(function (result) { - console.log("PAM completed with result:", result); - lock.authenticating = false; - if (result === PamResult.Success) { - console.log("Authentication successful, unlocking..."); - lock.locked = false; - lock.password = ""; - lock.errorMessage = ""; - } else { - console.log("Authentication failed"); - lock.errorMessage = "Authentication failed."; - lock.password = ""; - } - pam.destroy(); - }); + console.log("[LockScreen] About to create PAM context with userName:", Quickshell.env("USER")) + var pam = Qt.createQmlObject( + 'import Quickshell.Services.Pam; PamContext { config: "login"; user: "' + Quickshell.env("USER") + '" }', + lock) + console.log("PamContext created", pam) - pam.onError.connect(function (error) { - console.log("PAM error:", error); - lock.authenticating = false; - lock.errorMessage = pam.message || "Authentication error."; - lock.password = ""; - pam.destroy(); - }); + pam.onCompleted.connect(function (result) { + console.log("PAM completed with result:", result) + lock.authenticating = false + if (result === PamResult.Success) { + console.log("Authentication successful, unlocking...") + lock.locked = false + lock.password = "" + lock.errorMessage = "" + } else { + console.log("Authentication failed") + lock.errorMessage = "Authentication failed." + lock.password = "" + } + pam.destroy() + }) - pam.onPamMessage.connect(function () { - console.log("PAM message:", pam.message, "isError:", pam.messageIsError); - if (pam.messageIsError) { - lock.errorMessage = pam.message; - } - }); + pam.onError.connect(function (error) { + console.log("PAM error:", error) + lock.authenticating = false + lock.errorMessage = pam.message || "Authentication error." + lock.password = "" + pam.destroy() + }) - pam.onResponseRequiredChanged.connect(function () { - console.log("PAM response required:", pam.responseRequired); - if (pam.responseRequired && lock.authenticating) { - console.log("Responding to PAM with password"); - pam.respond(lock.password); - } - }); + pam.onPamMessage.connect(function () { + console.log("PAM message:", pam.message, "isError:", pam.messageIsError) + if (pam.messageIsError) { + lock.errorMessage = pam.message + } + }) - var started = pam.start(); - console.log("PAM start result:", started); + pam.onResponseRequiredChanged.connect(function () { + console.log("PAM response required:", pam.responseRequired) + if (pam.responseRequired && lock.authenticating) { + console.log("Responding to PAM with password") + pam.respond(lock.password) + } + }) + + var started = pam.start() + console.log("PAM start result:", started) + } + + WlSessionLockSurface { + // Wallpaper image + Image { + id: lockBgImage + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + source: Wallpapers.currentWallpaper !== "" ? Wallpapers.currentWallpaper : "" + cache: true + smooth: true + mipmap: false } - WlSessionLockSurface { - // Wallpaper image - Image { - id: lockBgImage - anchors.fill: parent - fillMode: Image.PreserveAspectCrop - source: Wallpapers.currentWallpaper !== "" ? Wallpapers.currentWallpaper : "" - cache: true - smooth: true - mipmap: false + // Blurred background + Rectangle { + anchors.fill: parent + color: "transparent" + + // Simple blur effect + layer.enabled: true + layer.smooth: true + layer.samples: 4 + } + + // Animated gradient overlay + Rectangle { + anchors.fill: parent + gradient: Gradient { + GradientStop { + position: 0.0 + color: Qt.rgba(0, 0, 0, 0.6) + } + GradientStop { + position: 0.3 + color: Qt.rgba(0, 0, 0, 0.3) + } + GradientStop { + position: 0.7 + color: Qt.rgba(0, 0, 0, 0.4) + } + GradientStop { + position: 1.0 + color: Qt.rgba(0, 0, 0, 0.7) + } + } + + // Subtle animated particles + Repeater { + model: 20 + Rectangle { + width: Math.random() * 4 + 2 + height: width + radius: width * 0.5 + color: Qt.rgba(Colors.accentPrimary.r, Colors.accentPrimary.g, Colors.accentPrimary.b, 0.3) + x: Math.random() * parent.width + y: Math.random() * parent.height + + SequentialAnimation on opacity { + loops: Animation.Infinite + NumberAnimation { + to: 0.8 + duration: 2000 + Math.random() * 3000 + } + NumberAnimation { + to: 0.1 + duration: 2000 + Math.random() * 3000 + } + } + } + } + } + + // Main content - Centered design + Item { + anchors.fill: parent + + // Top section - Time, date, and user info + ColumnLayout { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 80 * Scaling.scale(screen) + spacing: 40 * Scaling.scale(screen) + + // Time display - Large and prominent with pulse animation + Column { + spacing: 8 * Scaling.scale(screen) + Layout.alignment: Qt.AlignHCenter + + Text { + id: timeText + text: Qt.formatDateTime(new Date(), "HH:mm") + font.family: "Inter" + font.pointSize: Style.fontSizeXXL * 6 + font.weight: Font.Bold + font.letterSpacing: -2 + color: Colors.textPrimary + horizontalAlignment: Text.AlignHCenter + + SequentialAnimation on scale { + loops: Animation.Infinite + NumberAnimation { + to: 1.02 + duration: 2000 + easing.type: Easing.InOutQuad + } + NumberAnimation { + to: 1.0 + duration: 2000 + easing.type: Easing.InOutQuad + } + } + } + + Text { + id: dateText + text: Qt.formatDateTime(new Date(), "dddd, MMMM d") + font.family: "Inter" + font.pointSize: Style.fontSizeXL + font.weight: Font.Light + color: Colors.textSecondary + horizontalAlignment: Text.AlignHCenter + width: timeText.width + } } - // Blurred background - Rectangle { - anchors.fill: parent + // User section with animated avatar + Column { + spacing: 16 * Scaling.scale(screen) + Layout.alignment: Qt.AlignHCenter + + // Animated avatar with glow effect + Rectangle { + width: 120 * Scaling.scale(screen) + height: 120 * Scaling.scale(screen) + radius: width * 0.5 color: "transparent" - - // Simple blur effect - layer.enabled: true - layer.smooth: true - layer.samples: 4 - } + border.color: Colors.accentPrimary + border.width: 3 * Scaling.scale(screen) + anchors.horizontalCenter: parent.horizontalCenter - // Animated gradient overlay - Rectangle { - anchors.fill: parent - gradient: Gradient { - GradientStop { position: 0.0; color: Qt.rgba(0, 0, 0, 0.6) } - GradientStop { position: 0.3; color: Qt.rgba(0, 0, 0, 0.3) } - GradientStop { position: 0.7; color: Qt.rgba(0, 0, 0, 0.4) } - GradientStop { position: 1.0; color: Qt.rgba(0, 0, 0, 0.7) } + // Glow effect + Rectangle { + anchors.centerIn: parent + width: parent.width + 24 * Scaling.scale(screen) + height: parent.height + 24 * Scaling.scale(screen) + radius: width * 0.5 + color: "transparent" + border.color: Qt.rgba(Colors.accentPrimary.r, Colors.accentPrimary.g, Colors.accentPrimary.b, 0.3) + border.width: 2 * Scaling.scale(screen) + z: -1 + + SequentialAnimation on scale { + loops: Animation.Infinite + NumberAnimation { + to: 1.1 + duration: 1500 + easing.type: Easing.InOutQuad + } + NumberAnimation { + to: 1.0 + duration: 1500 + easing.type: Easing.InOutQuad + } + } } - // Subtle animated particles - Repeater { + NImageRounded { + anchors.centerIn: parent + width: 100 * Scaling.scale(screen) + height: 100 * Scaling.scale(screen) + imagePath: Quickshell.env("HOME") + "/.face" + fallbackIcon: "person" + imageRadius: width * 0.5 + } + + // Hover animation + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: parent.scale = 1.05 + onExited: parent.scale = 1.0 + } + + Behavior on scale { + NumberAnimation { + duration: 200 + easing.type: Easing.OutBack + } + } + } + } + } + + // Centered terminal section + Item { + width: 720 * Scaling.scale(screen) + height: 280 * Scaling.scale(screen) + anchors.centerIn: parent + + ColumnLayout { + anchors.centerIn: parent + spacing: 20 * Scaling.scale(screen) + width: parent.width + + // Futuristic Terminal-Style Input + Item { + width: parent.width + height: 280 * Scaling.scale(screen) + Layout.fillWidth: true + + // Terminal background with scanlines + Rectangle { + id: terminalBackground + anchors.fill: parent + radius: 16 + color: Colors.applyOpacity(Colors.backgroundPrimary, "E6") + border.color: Colors.accentPrimary + border.width: 2 * Scaling.scale(screen) + + // Scanline effect + Repeater { model: 20 Rectangle { - width: Math.random() * 4 + 2 - height: width - radius: width * 0.5 - color: Qt.rgba(Colors.accentPrimary.r, Colors.accentPrimary.g, Colors.accentPrimary.b, 0.3) - x: Math.random() * parent.width - y: Math.random() * parent.height - - SequentialAnimation on opacity { - loops: Animation.Infinite - NumberAnimation { to: 0.8; duration: 2000 + Math.random() * 3000 } - NumberAnimation { to: 0.1; duration: 2000 + Math.random() * 3000 } + width: parent.width + height: 1 + color: Colors.applyOpacity(Colors.accentPrimary, "1A") + y: index * 10 + opacity: 0.3 + + SequentialAnimation on opacity { + loops: Animation.Infinite + NumberAnimation { + to: 0.6 + duration: 2000 + Math.random() * 1000 } + NumberAnimation { + to: 0.1 + duration: 2000 + Math.random() * 1000 + } + } } - } - } + } - // Main content - Centered design - Item { - anchors.fill: parent + // Terminal header + Rectangle { + width: parent.width + height: 40 * Scaling.scale(screen) + color: Colors.applyOpacity(Colors.accentPrimary, "33") + topLeftRadius: 14 + topRightRadius: 14 - // Top section - Time, date, and user info - ColumnLayout { + RowLayout { + anchors.fill: parent + anchors.margins: 12 * Scaling.scale(screen) + spacing: 12 * Scaling.scale(screen) + + Text { + text: "●" + color: Colors.error + font.pixelSize: 16 * Scaling.scale(screen) + } + + Text { + text: "●" + color: Colors.warning + font.pixelSize: 16 * Scaling.scale(screen) + } + + Text { + text: "●" + color: Colors.accentPrimary + font.pixelSize: 16 * Scaling.scale(screen) + } + + Text { + text: "SECURE TERMINAL" + color: Colors.textPrimary + font.family: "DejaVu Sans Mono" + font.pointSize: Style.fontSizeLarge + font.weight: Font.Bold + Layout.fillWidth: true + } + } + } + + // Terminal content area + ColumnLayout { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - anchors.topMargin: 80 * Scaling.scale(screen) - spacing: 40 * Scaling.scale(screen) + anchors.bottom: parent.bottom + anchors.topMargin: 70 * Scaling.scale(screen) + anchors.margins: 12 * Scaling.scale(screen) + spacing: 12 * Scaling.scale(screen) - // Time display - Large and prominent with pulse animation - Column { - spacing: 8 * Scaling.scale(screen) - Layout.alignment: Qt.AlignHCenter + // Welcome back typing effect + RowLayout { + Layout.fillWidth: true + spacing: 12 * Scaling.scale(screen) - Text { - id: timeText - text: Qt.formatDateTime(new Date(), "HH:mm") - font.family: "Inter" - font.pointSize: Style.fontSizeXXL * 6 - font.weight: Font.Bold - font.letterSpacing: -2 - color: Colors.textPrimary - horizontalAlignment: Text.AlignHCenter + Text { + text: "root@noctalia:~$" + color: Colors.accentPrimary + font.family: "DejaVu Sans Mono" + font.pointSize: Style.fontSizeLarge + font.weight: Font.Bold + } - SequentialAnimation on scale { - loops: Animation.Infinite - NumberAnimation { to: 1.02; duration: 2000; easing.type: Easing.InOutQuad } - NumberAnimation { to: 1.0; duration: 2000; easing.type: Easing.InOutQuad } + Text { + id: welcomeText + text: "" + color: Colors.textPrimary + font.family: "DejaVu Sans Mono" + font.pointSize: Style.fontSizeLarge + property int currentIndex: 0 + property string fullText: "Welcome back, " + Quickshell.env("USER") + "!" + + Timer { + interval: 100 + running: true + repeat: true + onTriggered: { + if (parent.currentIndex < parent.fullText.length) { + parent.text = parent.fullText.substring(0, parent.currentIndex + 1) + parent.currentIndex++ + } else { + running = false } + } } - - Text { - id: dateText - text: Qt.formatDateTime(new Date(), "dddd, MMMM d") - font.family: "Inter" - font.pointSize: Style.fontSizeXL - font.weight: Font.Light - color: Colors.textSecondary - horizontalAlignment: Text.AlignHCenter - width: timeText.width - } + } } - // User section with animated avatar - Column { - spacing: 16 * Scaling.scale(screen) - Layout.alignment: Qt.AlignHCenter + // Command line with integrated password input + RowLayout { + Layout.fillWidth: true + spacing: 12 * Scaling.scale(screen) - // Animated avatar with glow effect - Rectangle { - width: 120 * Scaling.scale(screen) - height: 120 * Scaling.scale(screen) - radius: width * 0.5 - color: "transparent" - border.color: Colors.accentPrimary - border.width: 3 * Scaling.scale(screen) - anchors.horizontalCenter: parent.horizontalCenter + Text { + text: "root@noctalia:~$" + color: Colors.accentPrimary + font.family: "DejaVu Sans Mono" + font.pointSize: Style.fontSizeLarge + font.weight: Font.Bold + } - // Glow effect - Rectangle { - anchors.centerIn: parent - width: parent.width + 24 * Scaling.scale(screen) - height: parent.height + 24 * Scaling.scale(screen) - radius: width * 0.5 - color: "transparent" - border.color: Qt.rgba(Colors.accentPrimary.r, Colors.accentPrimary.g, Colors.accentPrimary.b, 0.3) - border.width: 2 * Scaling.scale(screen) - z: -1 + Text { + text: "sudo unlock_session" + color: Colors.textPrimary + font.family: "DejaVu Sans Mono" + font.pointSize: Style.fontSizeLarge + } - SequentialAnimation on scale { - loops: Animation.Infinite - NumberAnimation { to: 1.1; duration: 1500; easing.type: Easing.InOutQuad } - NumberAnimation { to: 1.0; duration: 1500; easing.type: Easing.InOutQuad } - } - } + // Integrated password input (invisible, just for functionality) + TextInput { + id: passwordInput + width: 0 + height: 0 + visible: false + font.family: "DejaVu Sans Mono" + font.pointSize: Style.fontSizeLarge + color: Colors.textPrimary + echoMode: TextInput.Password + passwordCharacter: "*" + passwordMaskDelay: 0 - NImageRounded { - anchors.centerIn: parent - width: 100 * Scaling.scale(screen) - height: 100 * Scaling.scale(screen) - imagePath: Quickshell.env("HOME") + "/.face" - fallbackIcon: "person" - imageRadius: width * 0.5 - } - - // Hover animation - MouseArea { - anchors.fill: parent - hoverEnabled: true - onEntered: parent.scale = 1.05 - onExited: parent.scale = 1.0 - } - - Behavior on scale { - NumberAnimation { duration: 200; easing.type: Easing.OutBack } - } + text: lock.password + onTextChanged: { + lock.password = text + // Terminal typing sound effect (visual) + typingEffect.start() } - - } - } - - // Centered terminal section - Item { - width: 720 * Scaling.scale(screen) - height: 280 * Scaling.scale(screen) - anchors.centerIn: parent - - ColumnLayout { - anchors.centerIn: parent - spacing: 20 * Scaling.scale(screen) - width: parent.width - - - - // Futuristic Terminal-Style Input - Item { - width: parent.width - height: 280 * Scaling.scale(screen) - Layout.fillWidth: true - - // Terminal background with scanlines - Rectangle { - id: terminalBackground - anchors.fill: parent - radius: 16 - color: Colors.applyOpacity(Colors.backgroundPrimary, "E6") - border.color: Colors.accentPrimary - border.width: 2 * Scaling.scale(screen) - - // Scanline effect - Repeater { - model: 20 - Rectangle { - width: parent.width - height: 1 - color: Colors.applyOpacity(Colors.accentPrimary, "1A") - y: index * 10 - opacity: 0.3 - - SequentialAnimation on opacity { - loops: Animation.Infinite - NumberAnimation { to: 0.6; duration: 2000 + Math.random() * 1000 } - NumberAnimation { to: 0.1; duration: 2000 + Math.random() * 1000 } - } - } - } - - // Terminal header - Rectangle { - width: parent.width - height: 40 * Scaling.scale(screen) - color: Colors.applyOpacity(Colors.accentPrimary, "33") - topLeftRadius: 14 - topRightRadius: 14 - - RowLayout { - anchors.fill: parent - anchors.margins: 12 * Scaling.scale(screen) - spacing: 12 * Scaling.scale(screen) - - Text { - text: "●" - color: Colors.error - font.pixelSize: 16 * Scaling.scale(screen) - } - - Text { - text: "●" - color: Colors.warning - font.pixelSize: 16 * Scaling.scale(screen) - } - - Text { - text: "●" - color: Colors.accentPrimary - font.pixelSize: 16 * Scaling.scale(screen) - } - - Text { - text: "SECURE TERMINAL" - color: Colors.textPrimary - font.family: "DejaVu Sans Mono" - font.pointSize: Style.fontSizeLarge - font.weight: Font.Bold - Layout.fillWidth: true - } - } - } - - // Terminal content area - ColumnLayout { - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.topMargin: 70 * Scaling.scale(screen) - anchors.margins: 12 * Scaling.scale(screen) - spacing: 12 * Scaling.scale(screen) - - // Welcome back typing effect - RowLayout { - Layout.fillWidth: true - spacing: 12 * Scaling.scale(screen) - - Text { - text: "root@noctalia:~$" - color: Colors.accentPrimary - font.family: "DejaVu Sans Mono" - font.pointSize: Style.fontSizeLarge - font.weight: Font.Bold - } - - Text { - id: welcomeText - text: "" - color: Colors.textPrimary - font.family: "DejaVu Sans Mono" - font.pointSize: Style.fontSizeLarge - property int currentIndex: 0 - property string fullText: "Welcome back, " + Quickshell.env("USER") + "!" - - Timer { - interval: 100 - running: true - repeat: true - onTriggered: { - if (parent.currentIndex < parent.fullText.length) { - parent.text = parent.fullText.substring(0, parent.currentIndex + 1) - parent.currentIndex++ - } else { - running = false - } - } - } - } - } - - // Command line with integrated password input - RowLayout { - Layout.fillWidth: true - spacing: 12 * Scaling.scale(screen) - - Text { - text: "root@noctalia:~$" - color: Colors.accentPrimary - font.family: "DejaVu Sans Mono" - font.pointSize: Style.fontSizeLarge - font.weight: Font.Bold - } - - Text { - text: "sudo unlock_session" - color: Colors.textPrimary - font.family: "DejaVu Sans Mono" - font.pointSize: Style.fontSizeLarge - } - - // Integrated password input (invisible, just for functionality) - TextInput { - id: passwordInput - width: 0 - height: 0 - visible: false - font.family: "DejaVu Sans Mono" - font.pointSize: Style.fontSizeLarge - color: Colors.textPrimary - echoMode: TextInput.Password - passwordCharacter: "*" - passwordMaskDelay: 0 - - text: lock.password - onTextChanged: { - lock.password = text - // Terminal typing sound effect (visual) - typingEffect.start() - } - - Keys.onPressed: function (event) { - if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - lock.unlockAttempt(); - } - } - - Component.onCompleted: { - forceActiveFocus(); - } - } - - // Visual password display with integrated cursor - Text { - id: asterisksText - text: "*".repeat(passwordInput.text.length) - color: Colors.textPrimary - font.family: "DejaVu Sans Mono" - font.pointSize: Style.fontSizeLarge - visible: passwordInput.activeFocus - - // Typing effect animation - SequentialAnimation { - id: typingEffect - NumberAnimation { - target: passwordInput - property: "scale" - to: 1.01 - duration: 50 - } - NumberAnimation { - target: passwordInput - property: "scale" - to: 1.0 - duration: 50 - } - } - } - - // Blinking cursor positioned right after the asterisks - Rectangle { - width: 8 * Scaling.scale(screen) - height: 20 * Scaling.scale(screen) - color: Colors.accentPrimary - visible: passwordInput.activeFocus - anchors.left: asterisksText.right - anchors.leftMargin: 2 * Scaling.scale(screen) - anchors.verticalCenter: asterisksText.verticalCenter - - SequentialAnimation on opacity { - loops: Animation.Infinite - NumberAnimation { to: 1.0; duration: 500 } - NumberAnimation { to: 0.0; duration: 500 } - } - } - } - - // Status messages - Text { - text: lock.authenticating ? "Authenticating..." : (lock.errorMessage !== "" ? "Authentication failed." : "") - color: lock.authenticating ? Colors.accentPrimary : (lock.errorMessage !== "" ? Colors.error : "transparent") - font.family: "DejaVu Sans Mono" - font.pointSize: Style.fontSizeLarge - Layout.fillWidth: true - - SequentialAnimation on opacity { - running: lock.authenticating - loops: Animation.Infinite - NumberAnimation { to: 1.0; duration: 800 } - NumberAnimation { to: 0.5; duration: 800 } - } - } - - // Execute button - Rectangle { - width: 120 * Scaling.scale(screen) - height: 40 * Scaling.scale(screen) - radius: 12 - color: executeButtonArea.containsMouse ? Colors.accentPrimary : Colors.applyOpacity(Colors.accentPrimary, "33") - border.color: Colors.accentPrimary - border.width: 1 - enabled: !lock.authenticating - Layout.alignment: Qt.AlignRight - - Text { - anchors.centerIn: parent - text: lock.authenticating ? "EXECUTING..." : "EXECUTE" - color: executeButtonArea.containsMouse ? Colors.onAccent : Colors.accentPrimary - font.family: "DejaVu Sans Mono" - font.pointSize: Style.fontSizeMedium - font.weight: Font.Bold - } - - MouseArea { - id: executeButtonArea - anchors.fill: parent - hoverEnabled: true - onClicked: lock.unlockAttempt() - - SequentialAnimation on scale { - running: containsMouse - NumberAnimation { to: 1.05; duration: 150; easing.type: Easing.OutCubic } - } - - SequentialAnimation on scale { - running: !containsMouse - NumberAnimation { to: 1.0; duration: 150; easing.type: Easing.OutCubic } - } - } - - // Processing animation - SequentialAnimation on scale { - loops: Animation.Infinite - running: lock.authenticating - NumberAnimation { to: 1.02; duration: 600; easing.type: Easing.InOutQuad } - NumberAnimation { to: 1.0; duration: 600; easing.type: Easing.InOutQuad } - } - } - } - - // Terminal glow effect - Rectangle { - anchors.fill: parent - radius: parent.radius - color: "transparent" - border.color: Colors.applyOpacity(Colors.accentPrimary, "4D") - border.width: 1 - z: -1 - - SequentialAnimation on opacity { - loops: Animation.Infinite - NumberAnimation { to: 0.6; duration: 2000; easing.type: Easing.InOutQuad } - NumberAnimation { to: 0.2; duration: 2000; easing.type: Easing.InOutQuad } - } - } - } - + Keys.onPressed: function (event) { + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + lock.unlockAttempt() + } } + Component.onCompleted: { + forceActiveFocus() + } + } + // Visual password display with integrated cursor + Text { + id: asterisksText + text: "*".repeat(passwordInput.text.length) + color: Colors.textPrimary + font.family: "DejaVu Sans Mono" + font.pointSize: Style.fontSizeLarge + visible: passwordInput.activeFocus + // Typing effect animation + SequentialAnimation { + id: typingEffect + NumberAnimation { + target: passwordInput + property: "scale" + to: 1.01 + duration: 50 + } + NumberAnimation { + target: passwordInput + property: "scale" + to: 1.0 + duration: 50 + } + } + } + // Blinking cursor positioned right after the asterisks + Rectangle { + width: 8 * Scaling.scale(screen) + height: 20 * Scaling.scale(screen) + color: Colors.accentPrimary + visible: passwordInput.activeFocus + anchors.left: asterisksText.right + anchors.leftMargin: 2 * Scaling.scale(screen) + anchors.verticalCenter: asterisksText.verticalCenter + + SequentialAnimation on opacity { + loops: Animation.Infinite + NumberAnimation { + to: 1.0 + duration: 500 + } + NumberAnimation { + to: 0.0 + duration: 500 + } + } + } } - } - } + // Status messages + Text { + text: lock.authenticating ? "Authenticating..." : (lock.errorMessage !== "" ? "Authentication failed." : "") + color: lock.authenticating ? Colors.accentPrimary : (lock.errorMessage !== "" ? Colors.error : "transparent") + font.family: "DejaVu Sans Mono" + font.pointSize: Style.fontSizeLarge + Layout.fillWidth: true + SequentialAnimation on opacity { + running: lock.authenticating + loops: Animation.Infinite + NumberAnimation { + to: 1.0 + duration: 800 + } + NumberAnimation { + to: 0.5 + duration: 800 + } + } + } - // Enhanced power buttons with hover effects - Row { - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.margins: 50 * Scaling.scale(screen) - spacing: 20 * Scaling.scale(screen) - - // Shutdown with enhanced styling - Rectangle { - width: 64 * Scaling.scale(screen) - height: 64 * Scaling.scale(screen) - radius: 32 - color: Qt.rgba(Colors.error.r, Colors.error.g, Colors.error.b, shutdownArea.containsMouse ? 0.9 : 0.2) - border.color: Colors.error - border.width: 2 * Scaling.scale(screen) - - // Glow effect + // Execute button Rectangle { + width: 120 * Scaling.scale(screen) + height: 40 * Scaling.scale(screen) + radius: 12 + color: executeButtonArea.containsMouse ? Colors.accentPrimary : Colors.applyOpacity( + Colors.accentPrimary, "33") + border.color: Colors.accentPrimary + border.width: 1 + enabled: !lock.authenticating + Layout.alignment: Qt.AlignRight + + Text { anchors.centerIn: parent - width: parent.width + 10 * Scaling.scale(screen) - height: parent.height + 10 * Scaling.scale(screen) - radius: width * 0.5 - color: "transparent" - border.color: Qt.rgba(Colors.error.r, Colors.error.g, Colors.error.b, 0.3) - border.width: 2 * Scaling.scale(screen) - opacity: shutdownArea.containsMouse ? 1 : 0 - z: -1 + text: lock.authenticating ? "EXECUTING..." : "EXECUTE" + color: executeButtonArea.containsMouse ? Colors.onAccent : Colors.accentPrimary + font.family: "DejaVu Sans Mono" + font.pointSize: Style.fontSizeMedium + font.weight: Font.Bold + } - Behavior on opacity { - NumberAnimation { duration: 200; easing.type: Easing.OutCubic } - } - } - - MouseArea { - id: shutdownArea + MouseArea { + id: executeButtonArea anchors.fill: parent hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - Qt.createQmlObject('import Quickshell.Io; Process { command: ["shutdown", "-h", "now"]; running: true }', lock); + onClicked: lock.unlockAttempt() + + SequentialAnimation on scale { + running: containsMouse + NumberAnimation { + to: 1.05 + duration: 150 + easing.type: Easing.OutCubic + } } - } - Text { - anchors.centerIn: parent - text: "power_settings_new" - font.family: "Material Symbols Outlined" - font.pixelSize: 28 * Scaling.scale(screen) - color: shutdownArea.containsMouse ? Colors.onAccent : Colors.error - } + SequentialAnimation on scale { + running: !containsMouse + NumberAnimation { + to: 1.0 + duration: 150 + easing.type: Easing.OutCubic + } + } + } - Behavior on color { - ColorAnimation { duration: 200; easing.type: Easing.OutCubic } + // Processing animation + SequentialAnimation on scale { + loops: Animation.Infinite + running: lock.authenticating + NumberAnimation { + to: 1.02 + duration: 600 + easing.type: Easing.InOutQuad + } + NumberAnimation { + to: 1.0 + duration: 600 + easing.type: Easing.InOutQuad + } + } } - scale: shutdownArea.containsMouse ? 1.1 : 1.0 - } - - // Reboot with enhanced styling - Rectangle { - width: 64 * Scaling.scale(screen) - height: 64 * Scaling.scale(screen) - radius: 32 - color: Qt.rgba(Colors.accentPrimary.r, Colors.accentPrimary.g, Colors.accentPrimary.b, rebootArea.containsMouse ? 0.9 : 0.2) - border.color: Colors.accentPrimary - border.width: 2 * Scaling.scale(screen) - - // Glow effect - Rectangle { - anchors.centerIn: parent - width: parent.width + 10 * Scaling.scale(screen) - height: parent.height + 10 * Scaling.scale(screen) - radius: width * 0.5 - color: "transparent" - border.color: Qt.rgba(Colors.accentPrimary.r, Colors.accentPrimary.g, Colors.accentPrimary.b, 0.3) - border.width: 2 * Scaling.scale(screen) - opacity: rebootArea.containsMouse ? 1 : 0 - z: -1 - - Behavior on opacity { - NumberAnimation { duration: 200; easing.type: Easing.OutCubic } - } - } - - MouseArea { - id: rebootArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - Qt.createQmlObject('import Quickshell.Io; Process { command: ["reboot"]; running: true }', lock); - } - } - - Text { - anchors.centerIn: parent - text: "refresh" - font.family: "Material Symbols Outlined" - font.pixelSize: 28 * Scaling.scale(screen) - color: rebootArea.containsMouse ? Colors.onAccent : Colors.accentPrimary - } - - Behavior on color { - ColorAnimation { duration: 200; easing.type: Easing.OutCubic } - } - scale: rebootArea.containsMouse ? 1.1 : 1.0 - } - - // Logout with enhanced styling - Rectangle { - width: 64 * Scaling.scale(screen) - height: 64 * Scaling.scale(screen) - radius: 32 - color: Qt.rgba(Colors.accentSecondary.r, Colors.accentSecondary.g, Colors.accentSecondary.b, logoutArea.containsMouse ? 0.9 : 0.2) - border.color: Colors.accentSecondary - border.width: 2 * Scaling.scale(screen) - - // Glow effect - Rectangle { - anchors.centerIn: parent - width: parent.width + 10 * Scaling.scale(screen) - height: parent.height + 10 * Scaling.scale(screen) - radius: width * 0.5 - color: "transparent" - border.color: Qt.rgba(Colors.accentSecondary.r, Colors.accentSecondary.g, Colors.accentSecondary.b, 0.3) - border.width: 2 * Scaling.scale(screen) - opacity: logoutArea.containsMouse ? 1 : 0 - z: -1 - - Behavior on opacity { - NumberAnimation { duration: 200; easing.type: Easing.OutCubic } - } - } - - MouseArea { - id: logoutArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - Qt.createQmlObject('import Quickshell.Io; Process { command: ["loginctl", "terminate-user", "' + Quickshell.env("USER") + '"]; running: true }', lock); - } - } - - Text { - anchors.centerIn: parent - text: "exit_to_app" - font.family: "Material Symbols Outlined" - font.pixelSize: 28 * Scaling.scale(screen) - color: logoutArea.containsMouse ? Colors.onAccent : Colors.accentSecondary - } - - Behavior on color { - ColorAnimation { duration: 200; easing.type: Easing.OutCubic } - } - scale: logoutArea.containsMouse ? 1.1 : 1.0 - } - } - - // Timer for updating time - Timer { - interval: 1000 - running: true - repeat: true - onTriggered: { - timeText.text = Qt.formatDateTime(new Date(), "HH:mm"); - dateText.text = Qt.formatDateTime(new Date(), "dddd, MMMM d"); + } + + // Terminal glow effect + Rectangle { + anchors.fill: parent + radius: parent.radius + color: "transparent" + border.color: Colors.applyOpacity(Colors.accentPrimary, "4D") + border.width: 1 + z: -1 + + SequentialAnimation on opacity { + loops: Animation.Infinite + NumberAnimation { + to: 0.6 + duration: 2000 + easing.type: Easing.InOutQuad + } + NumberAnimation { + to: 0.2 + duration: 2000 + easing.type: Easing.InOutQuad + } + } + } } + } } + } } + + // Enhanced power buttons with hover effects + Row { + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: 50 * Scaling.scale(screen) + spacing: 20 * Scaling.scale(screen) + + // Shutdown with enhanced styling + Rectangle { + width: 64 * Scaling.scale(screen) + height: 64 * Scaling.scale(screen) + radius: 32 + color: Qt.rgba(Colors.error.r, Colors.error.g, Colors.error.b, shutdownArea.containsMouse ? 0.9 : 0.2) + border.color: Colors.error + border.width: 2 * Scaling.scale(screen) + + // Glow effect + Rectangle { + anchors.centerIn: parent + width: parent.width + 10 * Scaling.scale(screen) + height: parent.height + 10 * Scaling.scale(screen) + radius: width * 0.5 + color: "transparent" + border.color: Qt.rgba(Colors.error.r, Colors.error.g, Colors.error.b, 0.3) + border.width: 2 * Scaling.scale(screen) + opacity: shutdownArea.containsMouse ? 1 : 0 + z: -1 + + Behavior on opacity { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + } + + MouseArea { + id: shutdownArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + Qt.createQmlObject('import Quickshell.Io; Process { command: ["shutdown", "-h", "now"]; running: true }', + lock) + } + } + + Text { + anchors.centerIn: parent + text: "power_settings_new" + font.family: "Material Symbols Outlined" + font.pixelSize: 28 * Scaling.scale(screen) + color: shutdownArea.containsMouse ? Colors.onAccent : Colors.error + } + + Behavior on color { + ColorAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + scale: shutdownArea.containsMouse ? 1.1 : 1.0 + } + + // Reboot with enhanced styling + Rectangle { + width: 64 * Scaling.scale(screen) + height: 64 * Scaling.scale(screen) + radius: 32 + color: Qt.rgba(Colors.accentPrimary.r, Colors.accentPrimary.g, Colors.accentPrimary.b, + rebootArea.containsMouse ? 0.9 : 0.2) + border.color: Colors.accentPrimary + border.width: 2 * Scaling.scale(screen) + + // Glow effect + Rectangle { + anchors.centerIn: parent + width: parent.width + 10 * Scaling.scale(screen) + height: parent.height + 10 * Scaling.scale(screen) + radius: width * 0.5 + color: "transparent" + border.color: Qt.rgba(Colors.accentPrimary.r, Colors.accentPrimary.g, Colors.accentPrimary.b, 0.3) + border.width: 2 * Scaling.scale(screen) + opacity: rebootArea.containsMouse ? 1 : 0 + z: -1 + + Behavior on opacity { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + } + + MouseArea { + id: rebootArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + Qt.createQmlObject('import Quickshell.Io; Process { command: ["reboot"]; running: true }', lock) + } + } + + Text { + anchors.centerIn: parent + text: "refresh" + font.family: "Material Symbols Outlined" + font.pixelSize: 28 * Scaling.scale(screen) + color: rebootArea.containsMouse ? Colors.onAccent : Colors.accentPrimary + } + + Behavior on color { + ColorAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + scale: rebootArea.containsMouse ? 1.1 : 1.0 + } + + // Logout with enhanced styling + Rectangle { + width: 64 * Scaling.scale(screen) + height: 64 * Scaling.scale(screen) + radius: 32 + color: Qt.rgba(Colors.accentSecondary.r, Colors.accentSecondary.g, Colors.accentSecondary.b, + logoutArea.containsMouse ? 0.9 : 0.2) + border.color: Colors.accentSecondary + border.width: 2 * Scaling.scale(screen) + + // Glow effect + Rectangle { + anchors.centerIn: parent + width: parent.width + 10 * Scaling.scale(screen) + height: parent.height + 10 * Scaling.scale(screen) + radius: width * 0.5 + color: "transparent" + border.color: Qt.rgba(Colors.accentSecondary.r, Colors.accentSecondary.g, Colors.accentSecondary.b, 0.3) + border.width: 2 * Scaling.scale(screen) + opacity: logoutArea.containsMouse ? 1 : 0 + z: -1 + + Behavior on opacity { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + } + + MouseArea { + id: logoutArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + Qt.createQmlObject( + 'import Quickshell.Io; Process { command: ["loginctl", "terminate-user", "' + Quickshell.env( + "USER") + '"]; running: true }', lock) + } + } + + Text { + anchors.centerIn: parent + text: "exit_to_app" + font.family: "Material Symbols Outlined" + font.pixelSize: 28 * Scaling.scale(screen) + color: logoutArea.containsMouse ? Colors.onAccent : Colors.accentSecondary + } + + Behavior on color { + ColorAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + scale: logoutArea.containsMouse ? 1.1 : 1.0 + } + } + + // Timer for updating time + Timer { + interval: 1000 + running: true + repeat: true + onTriggered: { + timeText.text = Qt.formatDateTime(new Date(), "HH:mm") + dateText.text = Qt.formatDateTime(new Date(), "dddd, MMMM d") + } + } + } } diff --git a/Modules/Lockscreen/Lockscreen.qml b/Modules/Lockscreen/Lockscreen.qml deleted file mode 100644 index 31fc7e8..0000000 --- a/Modules/Lockscreen/Lockscreen.qml +++ /dev/null @@ -1,912 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls -import QtQuick.Effects -import Quickshell -import Quickshell.Wayland -import Quickshell.Services.Pam -import Quickshell.Io -import Quickshell.Widgets -import qs.Services -import qs.Widgets - -WlSessionLock { - id: lock - - property string errorMessage: "" - property bool authenticating: false - property string password: "" - property bool pamAvailable: typeof PamContext !== "undefined" - property bool demoMode: true - property string demoPassword: "lysec123" - property int demoStep: 0 - locked: false - - // Demo timer for automatic interaction - Timer { - id: demoTimer - interval: 2000 - running: demoMode && locked - repeat: true - onTriggered: { - if (demoStep === 0) { - // Start typing demo password - lock.password = "" - demoStep = 1 - interval = 150 - } else if (demoStep === 1) { - // Type each character - if (lock.password.length < demoPassword.length) { - lock.password += demoPassword.charAt(lock.password.length) - } else { - demoStep = 2 - interval = 1000 - } - } else if (demoStep === 2) { - // Simulate authentication - lock.authenticating = true - demoStep = 3 - interval = 2000 - } else if (demoStep === 4) { - // Reset for next demo - lock.locked = false - demoStep = 0 - interval = 3000 - } - } - } - - // Demo authentication simulation - Timer { - id: authSimulation - interval: 2000 - running: false - onTriggered: { - lock.authenticating = false - if (lock.password === demoPassword) { - // Success - unlock - lock.locked = false - lock.password = "" - lock.errorMessage = "" - demoStep = 4 - demoTimer.interval = 1000 - } else { - // Error - lock.errorMessage = "Authentication failed" - lock.password = "" - demoStep = 0 - demoTimer.interval = 2000 - } - } - } - - function unlockAttempt() { - console.log("Unlock attempt started"); - - // Demo mode handling - if (demoMode) { - if (lock.authenticating) return - - lock.authenticating = true - authSimulation.start() - return - } - - // Real PAM authentication - if (!pamAvailable) { - lock.errorMessage = "PAM authentication not available."; - console.log("PAM not available"); - return; - } - if (!lock.password) { - lock.errorMessage = "Password required."; - console.log("No password entered"); - return; - } - console.log("Starting PAM authentication..."); - lock.authenticating = true; - lock.errorMessage = ""; - - console.log("[LockScreen] About to create PAM context with userName:", Quickshell.env("USER")); - var pam = Qt.createQmlObject('import Quickshell.Services.Pam; PamContext { config: "login"; user: "' + Quickshell.env("USER") + '" }', lock); - console.log("PamContext created", pam); - - pam.onCompleted.connect(function (result) { - console.log("PAM completed with result:", result); - lock.authenticating = false; - if (result === PamResult.Success) { - console.log("Authentication successful, unlocking..."); - lock.locked = false; - lock.password = ""; - lock.errorMessage = ""; - } else { - console.log("Authentication failed"); - lock.errorMessage = "Authentication failed."; - lock.password = ""; - } - pam.destroy(); - }); - - pam.onError.connect(function (error) { - console.log("PAM error:", error); - lock.authenticating = false; - lock.errorMessage = pam.message || "Authentication error."; - lock.password = ""; - pam.destroy(); - }); - - pam.onPamMessage.connect(function () { - console.log("PAM message:", pam.message, "isError:", pam.messageIsError); - if (pam.messageIsError) { - lock.errorMessage = pam.message; - } - }); - - pam.onResponseRequiredChanged.connect(function () { - console.log("PAM response required:", pam.responseRequired); - if (pam.responseRequired && lock.authenticating) { - console.log("Responding to PAM with password"); - pam.respond(lock.password); - } - }); - - var started = pam.start(); - console.log("PAM start result:", started); - } - - WlSessionLockSurface { - // Wallpaper image - Image { - id: lockBgImage - anchors.fill: parent - fillMode: Image.PreserveAspectCrop - source: Wallpapers.currentWallpaper !== "" ? Wallpapers.currentWallpaper : "" - cache: true - smooth: true - mipmap: false - } - - // Blurred background - Rectangle { - anchors.fill: parent - color: "transparent" - - // Simple blur effect - layer.enabled: true - layer.smooth: true - layer.samples: 4 - } - - // Animated gradient overlay - Rectangle { - anchors.fill: parent - gradient: Gradient { - GradientStop { position: 0.0; color: Qt.rgba(0, 0, 0, 0.6) } - GradientStop { position: 0.3; color: Qt.rgba(0, 0, 0, 0.3) } - GradientStop { position: 0.7; color: Qt.rgba(0, 0, 0, 0.4) } - GradientStop { position: 1.0; color: Qt.rgba(0, 0, 0, 0.7) } - } - - // Subtle animated particles - Repeater { - model: 20 - Rectangle { - width: Math.random() * 4 + 2 - height: width - radius: width * 0.5 - color: Qt.rgba(Colors.accentPrimary.r, Colors.accentPrimary.g, Colors.accentPrimary.b, 0.3) - x: Math.random() * parent.width - y: Math.random() * parent.height - - SequentialAnimation on opacity { - loops: Animation.Infinite - NumberAnimation { to: 0.8; duration: 2000 + Math.random() * 3000 } - NumberAnimation { to: 0.1; duration: 2000 + Math.random() * 3000 } - } - } - } - } - - // Main content - Centered design - Item { - anchors.fill: parent - - // Top section - Time, date, and user info - ColumnLayout { - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.topMargin: 80 * Scaling.scale(screen) - spacing: 40 * Scaling.scale(screen) - - // Time display - Large and prominent with pulse animation - Column { - spacing: 8 * Scaling.scale(screen) - Layout.alignment: Qt.AlignHCenter - - Text { - id: timeText - text: Qt.formatDateTime(new Date(), "HH:mm") - font.family: "Inter" - font.pixelSize: 140 * Scaling.scale(screen) - font.weight: Font.Bold - font.letterSpacing: -2 - color: Colors.textPrimary - horizontalAlignment: Text.AlignHCenter - - SequentialAnimation on scale { - loops: Animation.Infinite - NumberAnimation { to: 1.02; duration: 2000; easing.type: Easing.InOutQuad } - NumberAnimation { to: 1.0; duration: 2000; easing.type: Easing.InOutQuad } - } - } - - Text { - id: dateText - text: Qt.formatDateTime(new Date(), "dddd, MMMM d") - font.family: "Inter" - font.pixelSize: 26 * Scaling.scale(screen) - font.weight: Font.Light - color: Colors.textSecondary - horizontalAlignment: Text.AlignHCenter - width: timeText.width - } - } - - // User section with animated avatar - Column { - spacing: 16 * Scaling.scale(screen) - Layout.alignment: Qt.AlignHCenter - - // Animated avatar with glow effect - Rectangle { - width: 120 * Scaling.scale(screen) - height: 120 * Scaling.scale(screen) - radius: width * 0.5 - color: "transparent" - border.color: Colors.accentPrimary - border.width: 3 * Scaling.scale(screen) - anchors.horizontalCenter: parent.horizontalCenter - - // Glow effect - Rectangle { - anchors.centerIn: parent - width: parent.width + 24 * Scaling.scale(screen) - height: parent.height + 24 * Scaling.scale(screen) - radius: width * 0.5 - color: "transparent" - border.color: Qt.rgba(Colors.accentPrimary.r, Colors.accentPrimary.g, Colors.accentPrimary.b, 0.3) - border.width: 2 * Scaling.scale(screen) - z: -1 - - SequentialAnimation on scale { - loops: Animation.Infinite - NumberAnimation { to: 1.1; duration: 1500; easing.type: Easing.InOutQuad } - NumberAnimation { to: 1.0; duration: 1500; easing.type: Easing.InOutQuad } - } - } - - NImageRounded { - anchors.centerIn: parent - width: 100 * Scaling.scale(screen) - height: 100 * Scaling.scale(screen) - imagePath: Quickshell.env("HOME") + "/.face" - fallbackIcon: "person" - imageRadius: width * 0.5 - } - - // Hover animation - MouseArea { - anchors.fill: parent - hoverEnabled: true - onEntered: parent.scale = 1.05 - onExited: parent.scale = 1.0 - } - - Behavior on scale { - NumberAnimation { duration: 200; easing.type: Easing.OutBack } - } - } - - - } - } - - // Centered terminal section - Item { - width: 520 * Scaling.scale(screen) - height: 200 * Scaling.scale(screen) - anchors.centerIn: parent - - ColumnLayout { - anchors.centerIn: parent - spacing: 20 * Scaling.scale(screen) - width: parent.width - - - - // Futuristic Terminal-Style Input - Item { - width: parent.width - height: 200 * Scaling.scale(screen) - Layout.fillWidth: true - - // Terminal background with scanlines - Rectangle { - id: terminalBackground - anchors.fill: parent - radius: 16 - color: Colors.applyOpacity(Colors.backgroundPrimary, "E6") - border.color: Colors.accentPrimary - border.width: 2 * Scaling.scale(screen) - - // Scanline effect - Repeater { - model: 20 - Rectangle { - width: parent.width - height: 1 - color: Colors.applyOpacity(Colors.accentPrimary, "1A") - y: index * 10 - opacity: 0.3 - - SequentialAnimation on opacity { - loops: Animation.Infinite - NumberAnimation { to: 0.6; duration: 2000 + Math.random() * 1000 } - NumberAnimation { to: 0.1; duration: 2000 + Math.random() * 1000 } - } - } - } - - // Terminal header - Rectangle { - width: parent.width - height: 40 * Scaling.scale(screen) - color: Colors.applyOpacity(Colors.accentPrimary, "33") - topLeftRadius: 14 - topRightRadius: 14 - - RowLayout { - anchors.fill: parent - anchors.margins: 12 * Scaling.scale(screen) - spacing: 12 * Scaling.scale(screen) - - Text { - text: "●" - color: Colors.error - font.pixelSize: 16 * Scaling.scale(screen) - } - - Text { - text: "●" - color: Colors.warning - font.pixelSize: 16 * Scaling.scale(screen) - } - - Text { - text: "●" - color: Colors.accentPrimary - font.pixelSize: 16 * Scaling.scale(screen) - } - - Text { - text: "SECURE TERMINAL" - color: Colors.textPrimary - font.family: "Monaco" - font.pixelSize: 14 * Scaling.scale(screen) - font.weight: Font.Bold - Layout.fillWidth: true - } - } - } - - // Terminal content area - ColumnLayout { - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.topMargin: 50 * Scaling.scale(screen) - anchors.margins: 12 * Scaling.scale(screen) - spacing: 12 * Scaling.scale(screen) - - // Welcome back typing effect - RowLayout { - Layout.fillWidth: true - spacing: 12 * Scaling.scale(screen) - - Text { - text: "root@noctalia:~$" - color: Colors.accentPrimary - font.family: "Monaco" - font.pixelSize: 16 * Scaling.scale(screen) - font.weight: Font.Bold - } - - Text { - id: welcomeText - text: "" - color: Colors.textPrimary - font.family: "Monaco" - font.pixelSize: 16 * Scaling.scale(screen) - property int currentIndex: 0 - property string fullText: "Welcome back, " + Quickshell.env("USER") + "!" - - Timer { - interval: 100 - running: true - repeat: true - onTriggered: { - if (parent.currentIndex < parent.fullText.length) { - parent.text = parent.fullText.substring(0, parent.currentIndex + 1) - parent.currentIndex++ - } else { - running = false - } - } - } - } - } - - // Command line with integrated password input - RowLayout { - Layout.fillWidth: true - spacing: 12 * Scaling.scale(screen) - - Text { - text: "root@noctalia:~$" - color: Colors.accentPrimary - font.family: "Monaco" - font.pixelSize: 16 * Scaling.scale(screen) - font.weight: Font.Bold - } - - Text { - text: "sudo unlock_session" - color: Colors.textPrimary - font.family: "Monaco" - font.pixelSize: 16 * Scaling.scale(screen) - } - - // Integrated password input (invisible, just for functionality) - TextInput { - id: passwordInput - width: 0 - height: 0 - visible: false - font.family: "Monaco" - font.pixelSize: 16 * Scaling.scale(screen) - color: Colors.textPrimary - echoMode: TextInput.Password - passwordCharacter: "*" - passwordMaskDelay: 0 - - text: lock.password - onTextChanged: { - lock.password = text - // Terminal typing sound effect (visual) - typingEffect.start() - } - - Keys.onPressed: function (event) { - if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - lock.unlockAttempt(); - } - } - - Component.onCompleted: { - forceActiveFocus(); - } - } - - // Visual password display with integrated cursor - Text { - id: asterisksText - text: "*".repeat(passwordInput.text.length) - color: Colors.textPrimary - font.family: "Monaco" - font.pixelSize: 16 * Scaling.scale(screen) - visible: passwordInput.activeFocus - - // Typing effect animation - SequentialAnimation { - id: typingEffect - NumberAnimation { - target: passwordInput - property: "scale" - to: 1.01 - duration: 50 - } - NumberAnimation { - target: passwordInput - property: "scale" - to: 1.0 - duration: 50 - } - } - } - - // Blinking cursor positioned right after the asterisks - Rectangle { - width: 8 * Scaling.scale(screen) - height: 20 * Scaling.scale(screen) - color: Colors.accentPrimary - visible: passwordInput.activeFocus - anchors.left: asterisksText.right - anchors.leftMargin: 2 * Scaling.scale(screen) - anchors.verticalCenter: asterisksText.verticalCenter - - SequentialAnimation on opacity { - loops: Animation.Infinite - NumberAnimation { to: 1.0; duration: 500 } - NumberAnimation { to: 0.0; duration: 500 } - } - } - } - - // Status messages - Text { - text: lock.authenticating ? "Authenticating..." : (lock.errorMessage !== "" ? "Authentication failed." : "") - color: lock.authenticating ? Colors.accentPrimary : (lock.errorMessage !== "" ? Colors.error : "transparent") - font.family: "Monaco" - font.pixelSize: 14 * Scaling.scale(screen) - Layout.fillWidth: true - - SequentialAnimation on opacity { - running: lock.authenticating - loops: Animation.Infinite - NumberAnimation { to: 1.0; duration: 800 } - NumberAnimation { to: 0.5; duration: 800 } - } - } - - // Execute button - Rectangle { - width: 120 * Scaling.scale(screen) - height: 40 * Scaling.scale(screen) - radius: 12 - color: executeButtonArea.containsMouse ? Colors.accentPrimary : Colors.applyOpacity(Colors.accentPrimary, "33") - border.color: Colors.accentPrimary - border.width: 1 - enabled: !lock.authenticating - Layout.alignment: Qt.AlignRight - - Text { - anchors.centerIn: parent - text: lock.authenticating ? "EXECUTING..." : "EXECUTE" - color: executeButtonArea.containsMouse ? Colors.onAccent : Colors.accentPrimary - font.family: "Monaco" - font.pixelSize: 12 * Scaling.scale(screen) - font.weight: Font.Bold - } - - MouseArea { - id: executeButtonArea - anchors.fill: parent - hoverEnabled: true - onClicked: lock.unlockAttempt() - - SequentialAnimation on scale { - running: containsMouse - NumberAnimation { to: 1.05; duration: 150; easing.type: Easing.OutCubic } - } - - SequentialAnimation on scale { - running: !containsMouse - NumberAnimation { to: 1.0; duration: 150; easing.type: Easing.OutCubic } - } - } - - // Processing animation - SequentialAnimation on scale { - loops: Animation.Infinite - running: lock.authenticating - NumberAnimation { to: 1.02; duration: 600; easing.type: Easing.InOutQuad } - NumberAnimation { to: 1.0; duration: 600; easing.type: Easing.InOutQuad } - } - } - } - - // Terminal glow effect - Rectangle { - anchors.fill: parent - radius: parent.radius - color: "transparent" - border.color: Colors.applyOpacity(Colors.accentPrimary, "4D") - border.width: 1 - z: -1 - - SequentialAnimation on opacity { - loops: Animation.Infinite - NumberAnimation { to: 0.6; duration: 2000; easing.type: Easing.InOutQuad } - NumberAnimation { to: 0.2; duration: 2000; easing.type: Easing.InOutQuad } - } - } - } - - } - - // Error message with modern styling - Rectangle { - width: parent.width - height: 56 * Scaling.scale(screen) - radius: 28 - color: Qt.rgba(Colors.error.r, Colors.error.g, Colors.error.b, 0.15) - border.color: Colors.error - border.width: 1 * Scaling.scale(screen) - visible: lock.errorMessage !== "" - Layout.fillWidth: true - - RowLayout { - anchors.fill: parent - anchors.margins: 18 * Scaling.scale(screen) - spacing: 12 * Scaling.scale(screen) - - Text { - text: "error" - font.family: "Material Symbols Outlined" - font.pixelSize: 22 * Scaling.scale(screen) - color: Colors.error - } - - Text { - text: lock.errorMessage - color: Colors.error - font.family: "Inter" - font.pixelSize: 16 * Scaling.scale(screen) - font.weight: Font.Medium - Layout.fillWidth: true - } - } - - NumberAnimation on opacity { - from: 0 - to: 1 - duration: 300 - running: lock.errorMessage !== "" - } - - // Shake animation on error - SequentialAnimation on x { - running: lock.errorMessage !== "" - NumberAnimation { to: 10; duration: 50 } - NumberAnimation { to: -10; duration: 100 } - NumberAnimation { to: 10; duration: 100 } - NumberAnimation { to: 0; duration: 50 } - } - } - - - } - } - } - - // Demo controls (only visible in demo mode) - Row { - anchors.left: parent.left - anchors.bottom: parent.bottom - anchors.margins: 50 * Scaling.scale(screen) - spacing: 10 * Scaling.scale(screen) - visible: demoMode - - Rectangle { - width: 80 * Scaling.scale(screen) - height: 30 * Scaling.scale(screen) - radius: 6 - color: Colors.accentPrimary - - Text { - anchors.centerIn: parent - text: "Demo Lock" - color: Colors.onAccent - font.pixelSize: 12 * Scaling.scale(screen) - } - - MouseArea { - anchors.fill: parent - onClicked: { - lock.locked = true - lock.password = "" - lock.errorMessage = "" - lock.demoStep = 0 - demoTimer.interval = 2000 - } - } - } - - Rectangle { - width: 80 * Scaling.scale(screen) - height: 30 * Scaling.scale(screen) - radius: 6 - color: Colors.error - - Text { - anchors.centerIn: parent - text: "Reset" - color: Colors.onError - font.pixelSize: 12 * Scaling.scale(screen) - } - - MouseArea { - anchors.fill: parent - onClicked: { - lock.locked = false - lock.password = "" - lock.errorMessage = "" - lock.authenticating = false - lock.demoStep = 0 - } - } - } - } - - // Enhanced power buttons with hover effects - Row { - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.margins: 50 * Scaling.scale(screen) - spacing: 20 * Scaling.scale(screen) - - // Shutdown with enhanced styling - Rectangle { - width: 64 * Scaling.scale(screen) - height: 64 * Scaling.scale(screen) - radius: 32 - color: Qt.rgba(Colors.error.r, Colors.error.g, Colors.error.b, shutdownArea.containsMouse ? 0.9 : 0.2) - border.color: Colors.error - border.width: 2 * Scaling.scale(screen) - - // Glow effect - Rectangle { - anchors.centerIn: parent - width: parent.width + 10 * Scaling.scale(screen) - height: parent.height + 10 * Scaling.scale(screen) - radius: width * 0.5 - color: "transparent" - border.color: Qt.rgba(Colors.error.r, Colors.error.g, Colors.error.b, 0.3) - border.width: 2 * Scaling.scale(screen) - opacity: shutdownArea.containsMouse ? 1 : 0 - z: -1 - - Behavior on opacity { - NumberAnimation { duration: 200; easing.type: Easing.OutCubic } - } - } - - MouseArea { - id: shutdownArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - Qt.createQmlObject('import Quickshell.Io; Process { command: ["shutdown", "-h", "now"]; running: true }', lock); - } - } - - Text { - anchors.centerIn: parent - text: "power_settings_new" - font.family: "Material Symbols Outlined" - font.pixelSize: 28 * Scaling.scale(screen) - color: shutdownArea.containsMouse ? Colors.onAccent : Colors.error - } - - Behavior on color { - ColorAnimation { duration: 200; easing.type: Easing.OutCubic } - } - scale: shutdownArea.containsMouse ? 1.1 : 1.0 - } - - // Reboot with enhanced styling - Rectangle { - width: 64 * Scaling.scale(screen) - height: 64 * Scaling.scale(screen) - radius: 32 - color: Qt.rgba(Colors.accentPrimary.r, Colors.accentPrimary.g, Colors.accentPrimary.b, rebootArea.containsMouse ? 0.9 : 0.2) - border.color: Colors.accentPrimary - border.width: 2 * Scaling.scale(screen) - - // Glow effect - Rectangle { - anchors.centerIn: parent - width: parent.width + 10 * Scaling.scale(screen) - height: parent.height + 10 * Scaling.scale(screen) - radius: width * 0.5 - color: "transparent" - border.color: Qt.rgba(Colors.accentPrimary.r, Colors.accentPrimary.g, Colors.accentPrimary.b, 0.3) - border.width: 2 * Scaling.scale(screen) - opacity: rebootArea.containsMouse ? 1 : 0 - z: -1 - - Behavior on opacity { - NumberAnimation { duration: 200; easing.type: Easing.OutCubic } - } - } - - MouseArea { - id: rebootArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - Qt.createQmlObject('import Quickshell.Io; Process { command: ["reboot"]; running: true }', lock); - } - } - - Text { - anchors.centerIn: parent - text: "refresh" - font.family: "Material Symbols Outlined" - font.pixelSize: 28 * Scaling.scale(screen) - color: rebootArea.containsMouse ? Colors.onAccent : Colors.accentPrimary - } - - Behavior on color { - ColorAnimation { duration: 200; easing.type: Easing.OutCubic } - } - scale: rebootArea.containsMouse ? 1.1 : 1.0 - } - - // Logout with enhanced styling - Rectangle { - width: 64 * Scaling.scale(screen) - height: 64 * Scaling.scale(screen) - radius: 32 - color: Qt.rgba(Colors.accentSecondary.r, Colors.accentSecondary.g, Colors.accentSecondary.b, logoutArea.containsMouse ? 0.9 : 0.2) - border.color: Colors.accentSecondary - border.width: 2 * Scaling.scale(screen) - - // Glow effect - Rectangle { - anchors.centerIn: parent - width: parent.width + 10 * Scaling.scale(screen) - height: parent.height + 10 * Scaling.scale(screen) - radius: width * 0.5 - color: "transparent" - border.color: Qt.rgba(Colors.accentSecondary.r, Colors.accentSecondary.g, Colors.accentSecondary.b, 0.3) - border.width: 2 * Scaling.scale(screen) - opacity: logoutArea.containsMouse ? 1 : 0 - z: -1 - - Behavior on opacity { - NumberAnimation { duration: 200; easing.type: Easing.OutCubic } - } - } - - MouseArea { - id: logoutArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - Qt.createQmlObject('import Quickshell.Io; Process { command: ["loginctl", "terminate-user", "' + Quickshell.env("USER") + '"]; running: true }', lock); - } - } - - Text { - anchors.centerIn: parent - text: "exit_to_app" - font.family: "Material Symbols Outlined" - font.pixelSize: 28 * Scaling.scale(screen) - color: logoutArea.containsMouse ? Colors.onAccent : Colors.accentSecondary - } - - Behavior on color { - ColorAnimation { duration: 200; easing.type: Easing.OutCubic } - } - scale: logoutArea.containsMouse ? 1.1 : 1.0 - } - } - - // Timer for updating time - Timer { - interval: 1000 - running: true - repeat: true - onTriggered: { - timeText.text = Qt.formatDateTime(new Date(), "HH:mm"); - dateText.text = Qt.formatDateTime(new Date(), "dddd, MMMM d"); - } - } - } -} diff --git a/Modules/SidePanel/PowerMenu.qml b/Modules/SidePanel/PowerMenu.qml index 7daaa6c..d11f3e5 100644 --- a/Modules/SidePanel/PowerMenu.qml +++ b/Modules/SidePanel/PowerMenu.qml @@ -419,8 +419,8 @@ NPanel { running: false } - // LockScreen instance - LockScreen { - id: lockScreen + // LockScreen instance + LockScreen { + id: lockScreen } } diff --git a/Services/IPCManager.qml b/Services/IPCManager.qml index 0eac967..992b7c9 100644 --- a/Services/IPCManager.qml +++ b/Services/IPCManager.qml @@ -49,8 +49,8 @@ Item { } } - // LockScreen instance - LockScreen { - id: lockScreen + // LockScreen instance + LockScreen { + id: lockScreen } } From cdc33eaa86648d4fbafb213e8873460f3d276b46 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Thu, 14 Aug 2025 08:30:45 -0400 Subject: [PATCH 268/394] better wallust template for noctalia --- Assets/Wallust/templates/noctalia.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Assets/Wallust/templates/noctalia.json b/Assets/Wallust/templates/noctalia.json index 90af322..ee4ba3b 100644 --- a/Assets/Wallust/templates/noctalia.json +++ b/Assets/Wallust/templates/noctalia.json @@ -3,8 +3,8 @@ "backgroundSecondary": "{{ background | lighten(0.03) }}", "backgroundTertiary": "{{ background | lighten(0.06) }}", - "surface": "{{ background | lighten(0.04) }}", - "surfaceVariant": "{{ background | lighten(0.08) }}", + "surface": "{{ background | lighten(0.01) }}", + "surfaceVariant": "{{ background | lighten(0.07) }}", "textPrimary": "{{ foreground }}", "textSecondary": "{{ foreground | darken(0.25) }}", From 2bdff80599e9f1eaa0bddc33c92e24be94e3f1fb Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Thu, 14 Aug 2025 15:17:54 +0200 Subject: [PATCH 269/394] Early matugen implementation --- Assets/Matugen/matugen.toml | 28 +++++++++ Assets/Matugen/templates/noctalia.json | 26 ++++++++ Assets/Wallust/templates/noctalia.json | 26 -------- Assets/Wallust/wallust.toml | 47 -------------- Bin/matugen-theme.sh | 62 +++++++++++++++++++ Modules/Settings/Tabs/Wallpaper.qml | 2 +- README.md | 2 +- Services/Colors.qml | 86 +++++++++++++------------- Services/Wallpapers.qml | 3 +- 9 files changed, 162 insertions(+), 120 deletions(-) create mode 100644 Assets/Matugen/matugen.toml create mode 100644 Assets/Matugen/templates/noctalia.json delete mode 100644 Assets/Wallust/templates/noctalia.json delete mode 100644 Assets/Wallust/wallust.toml create mode 100755 Bin/matugen-theme.sh diff --git a/Assets/Matugen/matugen.toml b/Assets/Matugen/matugen.toml new file mode 100644 index 0000000..ad2bf57 --- /dev/null +++ b/Assets/Matugen/matugen.toml @@ -0,0 +1,28 @@ +# matugen configuration for Noctalia +# This file configures how matugen generates colors from wallpapers + +[config] +# Color scheme type for generation +scheme = "dark" + +# Color space for color extraction +color_space = "oklch" + +# Algorithm for color extraction +algorithm = "kmeans" + +# Number of colors to extract +color_count = 16 + +# Use source image colors +use_source_colors = true + +# Generate dark theme variant +generate_dark = true + +# Generate light theme variant +generate_light = false + +[templates.noctalia] +input_path = "templates/noctalia.json" +output_path = "~/.config/noctalia/theme.json" \ No newline at end of file diff --git a/Assets/Matugen/templates/noctalia.json b/Assets/Matugen/templates/noctalia.json new file mode 100644 index 0000000..35e857f --- /dev/null +++ b/Assets/Matugen/templates/noctalia.json @@ -0,0 +1,26 @@ +{ + "backgroundPrimary": "{{colors.surface_dim.default.hex}}", + "backgroundSecondary": "{{colors.surface.default.hex}}", + "backgroundTertiary": "{{colors.surface_bright.default.hex}}", + + "surface": "{{colors.surface.default.hex}}", + "surfaceVariant": "{{colors.surface_variant.default.hex}}", + + "textPrimary": "{{colors.on_surface.default.hex}}", + "textSecondary": "{{colors.on_surface_variant.default.hex}}", + "textDisabled": "{{colors.on_surface_variant.default.hex}}", + + "accentPrimary": "{{colors.primary.default.hex}}", + "accentSecondary": "{{colors.secondary.default.hex}}", + "accentTertiary": "{{colors.tertiary.default.hex}}", + + "error": "{{colors.error.default.hex}}", + "warning": "{{colors.error_container.default.hex}}", + + "hover": "{{colors.primary_container.default.hex}}", + "onAccent": "{{colors.on_primary.default.hex}}", + "outline": "{{colors.outline.default.hex}}", + + "shadow": "{{colors.shadow.default.hex}}", + "overlay": "{{colors.scrim.default.hex}}" +} \ No newline at end of file diff --git a/Assets/Wallust/templates/noctalia.json b/Assets/Wallust/templates/noctalia.json deleted file mode 100644 index ee4ba3b..0000000 --- a/Assets/Wallust/templates/noctalia.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "backgroundPrimary": "{{ background }}", - "backgroundSecondary": "{{ background | lighten(0.03) }}", - "backgroundTertiary": "{{ background | lighten(0.06) }}", - - "surface": "{{ background | lighten(0.01) }}", - "surfaceVariant": "{{ background | lighten(0.07) }}", - - "textPrimary": "{{ foreground }}", - "textSecondary": "{{ foreground | darken(0.25) }}", - "textDisabled": "{{ foreground | darken(0.5) }}", - - "accentPrimary": "{{ color1 }}", - "accentSecondary": "{{ color6 }}", - "accentTertiary": "{{ color4 }}", - - "error": "{{ color5 | saturate(0.5) }}", - "warning": "{{ color6 | saturate(0.4) }}", - - "hover": "{{ color14 }}", - "onAccent": "{{ background }}", - "outline": "{{ background | lighten(0.15) }}", - - "shadow": "{{ background }}", - "overlay": "{{ background }}" -} \ No newline at end of file diff --git a/Assets/Wallust/wallust.toml b/Assets/Wallust/wallust.toml deleted file mode 100644 index a27a0b3..0000000 --- a/Assets/Wallust/wallust.toml +++ /dev/null @@ -1,47 +0,0 @@ -# wallust v3.3 -# -# You can copy this file to ~/.config/wallust/wallust.toml (keep in mind is a sample config) - -# SIMPLE TUTORIAL, or `man wallust.5`: -# https://explosion-mental.codeberg.page/wallust/ -# -# If comming from v2: https://explosion-mental.codeberg.page/wallust/v3.html#wallusttoml - -# Global section - values below can be overwritten by command line flags - -# How the image is parse, in order to get the colors: -# full - resized - wal - thumb - fastresize - kmeans -backend = "resized" - -# What color space to use to produce and select the most prominent colors: -# lab - labmixed - lch - lchmixed -color_space = "labmixed" - -# Use the most prominent colors in a way that makes sense, a scheme color palette: -# dark - dark16 - darkcomp - darkcomp16 -# light - light16 - lightcomp - lightcomp16 -# harddark - harddark16 - harddarkcomp - harddarkcomp16 -# softdark - softdark16 - softdarkcomp - softdarkcomp16 -# softlight - softlight16 - softlightcomp - softlightcomp16 -palette = "dark" - -# Ensures a "readable contrast" (OPTIONAL, disabled by default) -# Should only be enabled when you notice an unreadable contrast frequently happening -# with your images. The reference color for the contrast is the background color. -check_contrast = true - -# Color saturation, between [1% and 100%] (OPTIONAL, disabled by default) -# usually something higher than 50 increases the saturation and below -# decreases it (on a scheme with strong and vivid colors) -#saturation = 50 - -# Alpha value for templating, by default 100 (no other use whatsoever) -#alpha = 100 - -[templates] -# NOTE: prefer '' over "" for paths, avoids escaping. -# template: A RELATIVE path that points to `~/.config/wallust/template` (depends on platform) -# target: ABSOLUTE path in which to place a file with generated templated values. -# ¡ If either one is a directory, then both SHOULD be one. ! -# zathura = { template = 'zathura', target = '~/.config/zathura/zathurarc' } -Noctalia = { template = 'noctalia.json', target = '~/.config/noctalia/theme.json' } \ No newline at end of file diff --git a/Bin/matugen-theme.sh b/Bin/matugen-theme.sh new file mode 100755 index 0000000..73c18ca --- /dev/null +++ b/Bin/matugen-theme.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +# matugen-theme.sh - Generate theme colors from wallpaper using matugen +# Usage: ./matugen-theme.sh + +set -e + +# Configuration +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" +CONFIG_FILE="$PROJECT_DIR/Assets/Matugen/matugen.toml" +TEMPLATE_FILE="$PROJECT_DIR/Assets/Matugen/templates/noctalia.json" +OUTPUT_DIR="$HOME/.config/noctalia" +OUTPUT_FILE="$OUTPUT_DIR/theme.json" + +# Check if wallpaper path is provided +if [ $# -eq 0 ]; then + echo "Error: No wallpaper path provided" + echo "Usage: $0 " + exit 1 +fi + +WALLPAPER_PATH="$1" + +# Check if wallpaper exists +if [ ! -f "$WALLPAPER_PATH" ]; then + echo "Error: Wallpaper file not found: $WALLPAPER_PATH" + exit 1 +fi + +# Create output directory if it doesn't exist +mkdir -p "$OUTPUT_DIR" + +# Generate theme using matugen +echo "Generating theme from wallpaper: $WALLPAPER_PATH" + +# Use matugen to generate colors and transform to our format +matugen image "$WALLPAPER_PATH" \ + --config "$CONFIG_FILE" \ + --json hex | jq -c ' +{ + "backgroundPrimary": .colors.dark.surface_dim, + "backgroundSecondary": .colors.dark.surface, + "backgroundTertiary": .colors.dark.surface_bright, + "surface": .colors.dark.surface, + "surfaceVariant": .colors.dark.surface_variant, + "textPrimary": .colors.dark.on_surface, + "textSecondary": .colors.dark.on_surface_variant, + "textDisabled": .colors.dark.on_surface_variant, + "accentPrimary": .colors.dark.primary, + "accentSecondary": .colors.dark.secondary, + "accentTertiary": .colors.dark.tertiary, + "error": .colors.dark.error, + "warning": .colors.dark.error_container, + "hover": .colors.dark.primary_container, + "onAccent": .colors.dark.on_primary, + "outline": .colors.dark.outline, + "shadow": .colors.dark.shadow, + "overlay": .colors.dark.scrim +}' > "$OUTPUT_FILE.tmp" && mv "$OUTPUT_FILE.tmp" "$OUTPUT_FILE" + +echo "Theme generated successfully: $OUTPUT_FILE" \ No newline at end of file diff --git a/Modules/Settings/Tabs/Wallpaper.qml b/Modules/Settings/Tabs/Wallpaper.qml index f5f92d2..4520be1 100644 --- a/Modules/Settings/Tabs/Wallpaper.qml +++ b/Modules/Settings/Tabs/Wallpaper.qml @@ -95,7 +95,7 @@ ColumnLayout { // Use Wallpaper Theme NToggle { label: "Use Wallpaper Theme" - description: "Automatically adjust theme colors based on wallpaper" + description: "Automatically adjust theme colors based on wallpaper using Matugen" value: Settings.data.wallpaper.generateTheme onToggled: function (newValue) { Settings.data.wallpaper.generateTheme = newValue diff --git a/README.md b/README.md index c45dd33..be2389b 100644 --- a/README.md +++ b/README.md @@ -196,7 +196,7 @@ You will need to install a few things to get everything working: - `xdg-desktop-portal-gnome` or any other xdg-desktop-portal (for `gpu-screen-recorder`) - `material-symbols-git` so the icons properly show up - `swww` to add fancy wallpaper animations (optional) -- `wallust` to theme the setup based on wallpaper (optional) +- `matugen` to theme the setup based on wallpaper (optional) ## zigstat and zigbrightness diff --git a/Services/Colors.qml b/Services/Colors.qml index c5dc26f..45afb9b 100644 --- a/Services/Colors.qml +++ b/Services/Colors.qml @@ -9,41 +9,41 @@ Singleton { id: root // Backgrounds - property color backgroundPrimary: useWallust ? wallustTheme.backgroundPrimary : defaultTheme.backgroundPrimary - property color backgroundSecondary: useWallust ? wallustTheme.backgroundSecondary : defaultTheme.backgroundSecondary - property color backgroundTertiary: useWallust ? wallustTheme.backgroundTertiary : defaultTheme.backgroundTertiary + property color backgroundPrimary: useMatugen ? matugenTheme.backgroundPrimary : defaultTheme.backgroundPrimary + property color backgroundSecondary: useMatugen ? matugenTheme.backgroundSecondary : defaultTheme.backgroundSecondary + property color backgroundTertiary: useMatugen ? matugenTheme.backgroundTertiary : defaultTheme.backgroundTertiary // Surfaces & Elevation - property color surface: useWallust ? wallustTheme.surface : defaultTheme.surface - property color surfaceVariant: useWallust ? wallustTheme.surfaceVariant : defaultTheme.surfaceVariant + property color surface: useMatugen ? matugenTheme.surface : defaultTheme.surface + property color surfaceVariant: useMatugen ? matugenTheme.surfaceVariant : defaultTheme.surfaceVariant // Text Colors - property color textPrimary: useWallust ? wallustTheme.textPrimary : defaultTheme.textPrimary - property color textSecondary: useWallust ? wallustTheme.textSecondary : defaultTheme.textSecondary - property color textDisabled: useWallust ? wallustTheme.textDisabled : defaultTheme.textDisabled + property color textPrimary: useMatugen ? matugenTheme.textPrimary : defaultTheme.textPrimary + property color textSecondary: useMatugen ? matugenTheme.textSecondary : defaultTheme.textSecondary + property color textDisabled: useMatugen ? matugenTheme.textDisabled : defaultTheme.textDisabled // Accent Colors - property color accentPrimary: useWallust ? wallustTheme.accentPrimary : defaultTheme.accentPrimary - property color accentSecondary: useWallust ? wallustTheme.accentSecondary : defaultTheme.accentSecondary - property color accentTertiary: useWallust ? wallustTheme.accentTertiary : defaultTheme.accentTertiary + property color accentPrimary: useMatugen ? matugenTheme.accentPrimary : defaultTheme.accentPrimary + property color accentSecondary: useMatugen ? matugenTheme.accentSecondary : defaultTheme.accentSecondary + property color accentTertiary: useMatugen ? matugenTheme.accentTertiary : defaultTheme.accentTertiary // Error/Warning - property color error: useWallust ? wallustTheme.error : defaultTheme.error - property color warning: useWallust ? wallustTheme.warning : defaultTheme.warning + property color error: useMatugen ? matugenTheme.error : defaultTheme.error + property color warning: useMatugen ? matugenTheme.warning : defaultTheme.warning // Hover - property color hover: useWallust ? wallustTheme.hover : defaultTheme.hover + property color hover: useMatugen ? matugenTheme.hover : defaultTheme.hover // Additional Theme Properties - property color onAccent: useWallust ? wallustTheme.onAccent : defaultTheme.onAccent - property color outline: useWallust ? wallustTheme.outline : defaultTheme.outline + property color onAccent: useMatugen ? matugenTheme.onAccent : defaultTheme.onAccent + property color outline: useMatugen ? matugenTheme.outline : defaultTheme.outline // Shadows & Overlays - property color shadow: applyOpacity(useWallust ? wallustTheme.shadow : defaultTheme.shadow, "B3") - property color overlay: applyOpacity(useWallust ? wallustTheme.overlay : defaultTheme.overlay, "66") + property color shadow: applyOpacity(useMatugen ? matugenTheme.shadow : defaultTheme.shadow, "B3") + property color overlay: applyOpacity(useMatugen ? matugenTheme.overlay : defaultTheme.overlay, "66") - // Check if we should use Wallust theme - property bool useWallust: Settings.data.wallpaper.generateTheme && wallustFile.loaded + // Check if we should use Matugen theme + property bool useMatugen: Settings.data.wallpaper.generateTheme && matugenFile.loaded function applyOpacity(color, opacity) { // Convert color to string and apply opacity @@ -81,40 +81,40 @@ Singleton { property color overlay: "#191724" } - // Wallust theme colors (loaded from theme.json) + // Matugen theme colors (loaded from theme.json) QtObject { - id: wallustTheme + id: matugenTheme - property color backgroundPrimary: wallustData.backgroundPrimary - property color backgroundSecondary: wallustData.backgroundSecondary - property color backgroundTertiary: wallustData.backgroundTertiary + property color backgroundPrimary: matugenData.backgroundPrimary + property color backgroundSecondary: matugenData.backgroundSecondary + property color backgroundTertiary: matugenData.backgroundTertiary - property color surface: wallustData.surface - property color surfaceVariant: wallustData.surfaceVariant + property color surface: matugenData.surface + property color surfaceVariant: matugenData.surfaceVariant - property color textPrimary: wallustData.textPrimary - property color textSecondary: wallustData.textSecondary - property color textDisabled: wallustData.textDisabled + property color textPrimary: matugenData.textPrimary + property color textSecondary: matugenData.textSecondary + property color textDisabled: matugenData.textDisabled - property color accentPrimary: wallustData.accentPrimary - property color accentSecondary: wallustData.accentSecondary - property color accentTertiary: wallustData.accentTertiary + property color accentPrimary: matugenData.accentPrimary + property color accentSecondary: matugenData.accentSecondary + property color accentTertiary: matugenData.accentTertiary - property color error: wallustData.error - property color warning: wallustData.warning + property color error: matugenData.error + property color warning: matugenData.warning - property color hover: wallustData.hover + property color hover: matugenData.hover - property color onAccent: wallustData.onAccent - property color outline: wallustData.outline + property color onAccent: matugenData.onAccent + property color outline: matugenData.outline - property color shadow: wallustData.shadow - property color overlay: wallustData.overlay + property color shadow: matugenData.shadow + property color overlay: matugenData.overlay } - // FileView to load Wallust theme data from Theme.json + // FileView to load Matugen theme data from Theme.json FileView { - id: wallustFile + id: matugenFile path: Settings.configDir + "theme.json" watchChanges: true onFileChanged: reload() @@ -126,7 +126,7 @@ Singleton { } } JsonAdapter { - id: wallustData + id: matugenData // Backgrounds property string backgroundPrimary: "#191724" diff --git a/Services/Wallpapers.qml b/Services/Wallpapers.qml index 6a0c183..300db58 100644 --- a/Services/Wallpapers.qml +++ b/Services/Wallpapers.qml @@ -155,12 +155,11 @@ Singleton { Process { id: generateThemeProcess - command: ["wallust", "run", currentWallpaper, "-u", "-k", "-d", "Assets/Wallust"] + command: [Quickshell.shellDir + "/Bin/matugen-theme.sh", currentWallpaper] workingDirectory: Quickshell.shellDir running: false stdout: StdioCollector { onStreamFinished: { - // console.log(this.text) } } From f8ec879270da8d7147d2e7cab0fce373bf7508b4 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Thu, 14 Aug 2025 15:31:33 +0200 Subject: [PATCH 270/394] Small edit to About.qml --- Modules/Settings/Tabs/About.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Settings/Tabs/About.qml b/Modules/Settings/Tabs/About.qml index 85c4bee..f768999 100644 --- a/Modules/Settings/Tabs/About.qml +++ b/Modules/Settings/Tabs/About.qml @@ -236,7 +236,7 @@ ColumnLayout { } NText { - text: (modelData.contributions || 0) + " commits" + text: (modelData.contributions || 0) + " " + ((modelData.contributions || 0) === 1 ? "commit" : "commits") font.pointSize: Style.fontSizeSmall * scaling color: contributorArea.containsMouse ? Colors.backgroundPrimary : Colors.textSecondary } From bad205f5f65aed7da0912b64d4171e12754bbb03 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Thu, 14 Aug 2025 15:42:34 +0200 Subject: [PATCH 271/394] More small fixes for Github.qml/About.qml --- Modules/Settings/Tabs/About.qml | 2 +- Services/GitHub.qml | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/Modules/Settings/Tabs/About.qml b/Modules/Settings/Tabs/About.qml index f768999..258c7e2 100644 --- a/Modules/Settings/Tabs/About.qml +++ b/Modules/Settings/Tabs/About.qml @@ -11,7 +11,7 @@ ColumnLayout { id: root property string latestVersion: GitHub.latestVersion - property string currentVersion: "v2.0.0" // Fallback version + property string currentVersion: "Unknown" // Fallback version property var contributors: GitHub.contributors spacing: 0 diff --git a/Services/GitHub.qml b/Services/GitHub.qml index 1bfac4c..1b74f4d 100644 --- a/Services/GitHub.qml +++ b/Services/GitHub.qml @@ -18,7 +18,7 @@ Singleton { property var contributors: [] FileView { - objectName: "githubDataFileView" + id: githubDataFileView path: githubDataFile watchChanges: true onFileChanged: reload() @@ -80,12 +80,16 @@ Singleton { // -------------------------------- function saveData() { data.timestamp = Time.timestamp + console.log("[GitHub] Saving data to cache file:", githubDataFile) + console.log("[GitHub] Data to save - version:", data.version, "contributors:", data.contributors.length) + + // Ensure cache directory exists + Quickshell.execDetached(["mkdir", "-p", Settings.cacheDir]) + Qt.callLater(() => { - // Access the FileView's writeAdapter method - var fileView = root.children.find(child => child.objectName === "githubDataFileView") - if (fileView) { - fileView.writeAdapter() - } + // Use direct ID reference to the FileView + githubDataFileView.writeAdapter() + console.log("[GitHub] Cache file written successfully") }) } @@ -140,8 +144,10 @@ Singleton { onStreamFinished: { try { const response = text + console.log("[GitHub] Raw contributors response length:", response ? response.length : 0) if (response && response.trim()) { const data = JSON.parse(response) + console.log("[GitHub] Parsed contributors data type:", typeof data, "length:", Array.isArray(data) ? data.length : "not array") root.data.contributors = data || [] root.contributors = root.data.contributors console.log("[GitHub] Contributors fetched from GitHub:", root.contributors.length) From 4635aec80e9f80d8449068ee75821947f1eb5e9b Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Thu, 14 Aug 2025 17:00:58 +0200 Subject: [PATCH 272/394] Add dock & dock settings in General.qml --- Modules/Dock/Dock.qml | 345 ++++++++++++++++++++++++++++++ Modules/Settings/Tabs/General.qml | 18 ++ Services/Settings.qml | 1 + shell.qml | 2 + 4 files changed, 366 insertions(+) create mode 100644 Modules/Dock/Dock.qml diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml new file mode 100644 index 0000000..c40fb48 --- /dev/null +++ b/Modules/Dock/Dock.qml @@ -0,0 +1,345 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Effects +import Quickshell +import Quickshell.Wayland +import Quickshell.Widgets +import qs.Services +import qs.Widgets + +NLoader { + isLoaded: Settings.data.general.showDock + content: Component { + Variants { + model: Quickshell.screens + + Item { + property var modelData + readonly property real scaling: Scaling.scale(modelData) + + // Auto-hide properties + property bool autoHide: Settings.data.general.dockAutoHide + property bool hidden: autoHide // Start hidden only if auto-hide is enabled + property int hideDelay: 500 + property int showDelay: 100 + property int hideAnimationDuration: 200 + property int showAnimationDuration: 150 + property int peekHeight: 2 + property int fullHeight: dockContainer.height + + // Track hover state + property bool dockHovered: false + property bool anyAppHovered: false + + // Context menu properties + property bool contextMenuVisible: false + property var contextMenuTarget: null + property var contextMenuToplevel: null + + PanelWindow { + id: dockWindow + visible: true + screen: modelData + exclusionMode: ExclusionMode.Ignore + anchors.bottom: true + anchors.left: true + anchors.right: true + focusable: false + color: "transparent" + implicitHeight: 60 + + // Timer for auto-hide delay + Timer { + id: hideTimer + interval: hideDelay + onTriggered: if (autoHide && !dockHovered && !anyAppHovered) hidden = true + } + + // Timer for show delay + Timer { + id: showTimer + interval: showDelay + onTriggered: hidden = false + } + + + + // Behavior for smooth hide/show animations + Behavior on margins.bottom { + NumberAnimation { + duration: hidden ? hideAnimationDuration : showAnimationDuration + easing.type: Easing.InOutQuad + } + } + + // Mouse area at screen bottom to detect entry and keep dock visible + MouseArea { + id: screenEdgeMouseArea + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: 10 + hoverEnabled: true + propagateComposedEvents: true + + onEntered: if (autoHide && hidden) showTimer.start() + onExited: if (autoHide && !hidden && !dockHovered && !anyAppHovered) hideTimer.start() + } + + margins.bottom: hidden ? -(fullHeight - peekHeight) : 0 + + MouseArea { + anchors.fill: parent + enabled: contextMenuVisible + onClicked: { + contextMenuVisible = false + contextMenuTarget = null + contextMenuToplevel = null + } + } + + Rectangle { + id: dockContainer + width: dock.width + 40 + height: 50 + color: Colors.backgroundSecondary + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + topLeftRadius: 20 + topRightRadius: 20 + + MouseArea { + id: dockMouseArea + anchors.fill: parent + hoverEnabled: true + propagateComposedEvents: true + + onEntered: { + dockHovered = true + if (autoHide) { + showTimer.stop() + hideTimer.stop() + hidden = false + } + } + onExited: { + dockHovered = false + if (autoHide && !anyAppHovered && !contextMenuVisible) hideTimer.start() + } + } + + Item { + id: dock + width: runningAppsRow.width + height: parent.height - 10 + anchors.centerIn: parent + + NTooltip { + id: appTooltip + visible: false + positionAbove: true + } + + function getAppIcon(toplevel: Toplevel): string { + if (!toplevel) return ""; + let icon = Quickshell.iconPath(toplevel.appId?.toLowerCase(), true); + if (!icon) icon = Quickshell.iconPath(toplevel.appId, true); + if (!icon) icon = Quickshell.iconPath(toplevel.title?.toLowerCase(), true); + if (!icon) icon = Quickshell.iconPath(toplevel.title, true); + return icon || Quickshell.iconPath("application-x-executable", true); + } + + Row { + id: runningAppsRow + spacing: 8 + height: parent.height + anchors.centerIn: parent + + Repeater { + model: ToplevelManager ? ToplevelManager.toplevels : null + + delegate: Rectangle { + id: appButton + width: 36 + height: 36 + radius: 18 + color:"transparent" + + property bool isActive: ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData + property bool hovered: appMouseArea.containsMouse + property string appId: modelData ? modelData.appId : "" + property string appTitle: modelData ? modelData.title : "" + + Behavior on color { ColorAnimation { duration: 150 } } + + Image { + id: appIcon + width: 28 + height: 28 + anchors.centerIn: parent + source: dock.getAppIcon(modelData) + visible: source.toString() !== "" + smooth: false + mipmap: false + antialiasing: false + fillMode: Image.PreserveAspectFit + } + + Text { + anchors.centerIn: parent + visible: !appIcon.visible + text: appButton.appId ? appButton.appId.charAt(0).toUpperCase() : "?" + font.pixelSize: 14 + font.bold: true + color: appButton.isActive ? Colors.accentPrimary : Colors.textPrimary + } + + MouseArea { + id: appMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + + onEntered: { + anyAppHovered = true + const appName = appButton.appTitle || appButton.appId || "Unknown" + appTooltip.text = appName.length > 40 ? appName.substring(0, 37) + "..." : appName + appTooltip.target = appButton + appTooltip.isVisible = true + if (autoHide) { + showTimer.stop() + hideTimer.stop() + hidden = false + } + } + + onExited: { + anyAppHovered = false + appTooltip.hide() + if (autoHide && !dockHovered && !contextMenuVisible) hideTimer.start() + } + + onClicked: function(mouse) { + if (mouse.button === Qt.MiddleButton && modelData?.close) { + modelData.close() + } + if (mouse.button === Qt.LeftButton && modelData?.activate) { + modelData.activate() + } + if (mouse.button === Qt.RightButton) { + appTooltip.hide() + contextMenuTarget = appButton + contextMenuToplevel = modelData + contextMenuVisible = true + } + } + } + + Rectangle { + visible: isActive + width: 20 + height: 3 + color: Colors.accentPrimary + radius: 1.5 + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottomMargin: 2 + } + } + } + } + + + } + } + + // Context Menu + PanelWindow { + id: contextMenuWindow + visible: contextMenuVisible + screen: dockWindow.screen + exclusionMode: ExclusionMode.Ignore + anchors.bottom: true + anchors.left: true + anchors.right: true + color: "transparent" + focusable: false + + MouseArea { + anchors.fill: parent + onClicked: { + contextMenuVisible = false + contextMenuTarget = null + contextMenuToplevel = null + hidden = true // Hide dock when context menu closes + } + } + + + + Rectangle { + id: contextMenuContainer + width: 80 + height: 32 + radius: 8 + color: closeMouseArea.containsMouse ? Colors.hover : Colors.backgroundPrimary + border.color: Colors.outline + border.width: 1 + + x: { + if (!contextMenuTarget) return 0 + const pos = contextMenuTarget.mapToItem(null, 0, 0) + let xPos = pos.x + (contextMenuTarget.width - width) / 2 + return Math.max(0, Math.min(xPos, dockWindow.width - width)) + } + + y: { + if (!contextMenuTarget) return 0 + const pos = contextMenuTarget.mapToItem(null, 0, 0) + return pos.y - height + 32 + } + + Text { + anchors.centerIn: parent + text: "Close" + font.pixelSize: 14 + color: Colors.textPrimary + } + + MouseArea { + id: closeMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + if (contextMenuToplevel?.close) contextMenuToplevel.close() + contextMenuVisible = false + hidden = true + } + } + + // Animation + scale: contextMenuVisible ? 1 : 0.9 + opacity: contextMenuVisible ? 1 : 0 + transformOrigin: Item.Bottom + + Behavior on scale { + NumberAnimation { + duration: 150 + easing.type: Easing.OutBack + } + } + + Behavior on opacity { + NumberAnimation { duration: 100 } + } + } + } + } + } +} +} +} diff --git a/Modules/Settings/Tabs/General.qml b/Modules/Settings/Tabs/General.qml index 2718914..53791ac 100644 --- a/Modules/Settings/Tabs/General.qml +++ b/Modules/Settings/Tabs/General.qml @@ -108,6 +108,24 @@ ColumnLayout { Settings.data.general.dimDesktop = v } } + + NToggle { + label: "Show Dock" + description: "Enable the dock at the bottom of the screen" + value: Settings.data.general.showDock + onToggled: function (v) { + Settings.data.general.showDock = v + } + } + + NToggle { + label: "Auto-hide Dock" + description: "Automatically hide the dock when not in use" + value: Settings.data.general.dockAutoHide + onToggled: function (v) { + Settings.data.general.dockAutoHide = v + } + } } } } diff --git a/Services/Settings.qml b/Services/Settings.qml index 9f2ca39..ff78fc1 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -88,6 +88,7 @@ Singleton { property bool dimDesktop: true property bool showScreenCorners: false property bool showDock: false + property bool dockAutoHide: false } // location diff --git a/shell.qml b/shell.qml index 84d7daf..81b5e7d 100644 --- a/shell.qml +++ b/shell.qml @@ -5,6 +5,7 @@ import Quickshell.Io import Quickshell.Widgets import Quickshell.Services.Pipewire import qs.Modules.Bar +import qs.Modules.Dock import qs.Modules.Calendar import qs.Modules.Demo import qs.Modules.Background @@ -21,6 +22,7 @@ ShellRoot { Overview {} ScreenCorners {} Bar {} + Dock {} DemoPanel { id: demoPanel From 2b39cbfe01c66baf324a8350bc2ac4d93265771a Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Thu, 14 Aug 2025 18:44:04 +0200 Subject: [PATCH 273/394] Add AppLauncher --- Helpers/MathHelper.js | 120 +++++++ Modules/AppLauncher/AppLauncher.qml | 519 ++++++++++++++++++++++++++++ Modules/LockScreen/LockScreen.qml | 19 +- Services/Clipboard.qml | 139 ++++++++ Services/IPCManager.qml | 3 +- shell.qml | 7 + 6 files changed, 788 insertions(+), 19 deletions(-) create mode 100644 Helpers/MathHelper.js create mode 100644 Modules/AppLauncher/AppLauncher.qml create mode 100644 Services/Clipboard.qml diff --git a/Helpers/MathHelper.js b/Helpers/MathHelper.js new file mode 100644 index 0000000..cc86775 --- /dev/null +++ b/Helpers/MathHelper.js @@ -0,0 +1,120 @@ +// Math helper functions for calculator functionality +var MathHelper = { + // Basic arithmetic operations + add: (a, b) => a + b, + subtract: (a, b) => a - b, + multiply: (a, b) => a * b, + divide: (a, b) => b !== 0 ? a / b : NaN, + + // Power and roots + pow: (base, exponent) => Math.pow(base, exponent), + sqrt: (x) => x >= 0 ? Math.sqrt(x) : NaN, + cbrt: (x) => Math.cbrt(x), + + // Trigonometric functions (in radians) + sin: (x) => Math.sin(x), + cos: (x) => Math.cos(x), + tan: (x) => Math.tan(x), + asin: (x) => Math.asin(x), + acos: (x) => Math.acos(x), + atan: (x) => Math.atan(x), + + // Logarithmic functions + log: (x) => x > 0 ? Math.log(x) : NaN, + log10: (x) => x > 0 ? Math.log10(x) : NaN, + log2: (x) => x > 0 ? Math.log2(x) : NaN, + + // Other mathematical functions + abs: (x) => Math.abs(x), + floor: (x) => Math.floor(x), + ceil: (x) => Math.ceil(x), + round: (x) => Math.round(x), + min: (...args) => Math.min(...args), + max: (...args) => Math.max(...args), + + // Constants + PI: Math.PI, + E: Math.E, + + // Factorial + factorial: (n) => { + if (n < 0 || n !== Math.floor(n)) return NaN; + if (n === 0 || n === 1) return 1; + let result = 1; + for (let i = 2; i <= n; i++) { + result *= i; + } + return result; + }, + + // Percentage + percent: (value, total) => (value / total) * 100, + + // Degrees to radians and vice versa + toRadians: (degrees) => degrees * (Math.PI / 180), + toDegrees: (radians) => radians * (180 / Math.PI), + + // Safe evaluation with math functions + evaluate: (expression) => { + try { + // Replace common math functions with MathHelper equivalents + let processedExpr = expression + .replace(/\bpi\b/gi, 'MathHelper.PI') + .replace(/\be\b/gi, 'MathHelper.E') + .replace(/\bsin\b/gi, 'MathHelper.sin') + .replace(/\bcos\b/gi, 'MathHelper.cos') + .replace(/\btan\b/gi, 'MathHelper.tan') + .replace(/\basin\b/gi, 'MathHelper.asin') + .replace(/\bacos\b/gi, 'MathHelper.acos') + .replace(/\batan\b/gi, 'MathHelper.atan') + .replace(/\blog\b/gi, 'MathHelper.log') + .replace(/\blog10\b/gi, 'MathHelper.log10') + .replace(/\blog2\b/gi, 'MathHelper.log2') + .replace(/\bsqrt\b/gi, 'MathHelper.sqrt') + .replace(/\bcbrt\b/gi, 'MathHelper.cbrt') + .replace(/\bpow\b/gi, 'MathHelper.pow') + .replace(/\babs\b/gi, 'MathHelper.abs') + .replace(/\bfloor\b/gi, 'MathHelper.floor') + .replace(/\bceil\b/gi, 'MathHelper.ceil') + .replace(/\bround\b/gi, 'MathHelper.round') + .replace(/\bmin\b/gi, 'MathHelper.min') + .replace(/\bmax\b/gi, 'MathHelper.max') + .replace(/\bfactorial\b/gi, 'MathHelper.factorial') + .replace(/\bpercent\b/gi, 'MathHelper.percent') + .replace(/\btoRadians\b/gi, 'MathHelper.toRadians') + .replace(/\btoDegrees\b/gi, 'MathHelper.toDegrees'); + + // Evaluate the expression + const result = Function('MathHelper', 'return ' + processedExpr)(MathHelper); + + // Check if result is valid + if (isNaN(result) || !isFinite(result)) { + return null; + } + + return result; + } catch (error) { + return null; + } + }, + + // Format result for display + formatResult: (result) => { + if (result === null || isNaN(result) || !isFinite(result)) { + return "Error"; + } + + // For very large or small numbers, use scientific notation + if (Math.abs(result) >= 1e10 || (Math.abs(result) < 1e-10 && result !== 0)) { + return result.toExponential(6); + } + + // For integers, don't show decimal places + if (Number.isInteger(result)) { + return result.toString(); + } + + // For decimals, limit to 8 significant digits + return parseFloat(result.toPrecision(8)).toString(); + } +}; \ No newline at end of file diff --git a/Modules/AppLauncher/AppLauncher.qml b/Modules/AppLauncher/AppLauncher.qml new file mode 100644 index 0000000..f7f19b2 --- /dev/null +++ b/Modules/AppLauncher/AppLauncher.qml @@ -0,0 +1,519 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Effects +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.Widgets +import qs.Services +import qs.Widgets + +import "../../Helpers/FuzzySort.js" as Fuzzysort +import "../../Helpers/MathHelper.js" as MathHelper + +NLoader { + id: appLauncher + isLoaded: false + // Clipboard state is persisted in Services/Clipboard.qml + content: Component { + NPanel { + id: appLauncherPanel + + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + + // No local timer/processes; use persistent Clipboard service + + // Removed local clipboard processes; handled by Clipboard service + + + + + + // Copy helpers via simple exec; avoid keeping processes alive locally + function copyImageBase64(mime, base64) { + Quickshell.execDetached(["sh", "-lc", `printf %s ${base64} | base64 -d | wl-copy -t '${mime}'`]) + } + + function copyText(text) { + Quickshell.execDetached(["sh", "-lc", `printf %s ${text} | wl-copy -t text/plain;charset=utf-8`]) + } + + + + function updateClipboardHistory() { + Clipboard.refresh(); + } + + function selectNext() { + if (filteredEntries.length > 0) { + selectedIndex = Math.min(selectedIndex + 1, filteredEntries.length - 1); + } + } + + function selectPrev() { + if (filteredEntries.length > 0) { + selectedIndex = Math.max(selectedIndex - 1, 0); + } + } + + function activateSelected() { + if (filteredEntries.length === 0) return; + + var modelData = filteredEntries[selectedIndex]; + if (modelData && modelData.execute) { + if (modelData.isCommand) { + modelData.execute(); + return; + } else { + modelData.execute(); + } + appLauncherPanel.hide(); + } + } + + property var desktopEntries: DesktopEntries.applications.values + property string searchText: "" + property int selectedIndex: 0 + property var filteredEntries: { + console.log("[AppLauncher] Total desktop entries:", desktopEntries ? desktopEntries.length : 0) + if (!desktopEntries || desktopEntries.length === 0) { + console.log("[AppLauncher] No desktop entries available") + return [] + } + + // Filter out entries that shouldn't be displayed + var visibleEntries = desktopEntries.filter(entry => { + if (!entry || entry.noDisplay) { + return false + } + return true + }) + + console.log("[AppLauncher] Visible entries:", visibleEntries.length) + + var query = searchText ? searchText.toLowerCase() : ""; + var results = []; + + // Handle special commands + if (query === ">") { + results.push({ + isCommand: true, + name: ">calc", + content: "Calculator - evaluate mathematical expressions", + icon: "calculate", + execute: function() { + searchText = ">calc "; + searchInput.cursorPosition = searchText.length; + } + }); + + results.push({ + isCommand: true, + name: ">clip", + content: "Clipboard history - browse and restore clipboard items", + icon: "content_paste", + execute: function() { + searchText = ">clip "; + searchInput.cursorPosition = searchText.length; + } + }); + + return results; + } + + // Handle clipboard history + if (query.startsWith(">clip")) { + if (!Clipboard.initialized) { + Clipboard.refresh(); + } + const searchTerm = query.slice(5).trim(); + + Clipboard.history.forEach(function(clip, index) { + let searchContent = clip.type === 'image' ? + clip.mimeType : + clip.content || clip; + + if (!searchTerm || searchContent.toLowerCase().includes(searchTerm)) { + let entry; + if (clip.type === 'image') { + entry = { + isClipboard: true, + name: "Image from " + new Date(clip.timestamp).toLocaleTimeString(), + content: "Image: " + clip.mimeType, + icon: "image", + type: 'image', + data: clip.data, + execute: function() { + const base64Data = clip.data.split(',')[1]; + copyImageBase64(clip.mimeType, base64Data); + Quickshell.execDetached(["notify-send", "Clipboard", "Image copied: " + clip.mimeType]); + } + }; + } else { + const textContent = clip.content || clip; + let displayContent = textContent; + let previewContent = ""; + + displayContent = displayContent.replace(/\s+/g, ' ').trim(); + + if (displayContent.length > 50) { + previewContent = displayContent; + displayContent = displayContent.split('\n')[0].substring(0, 50) + "..."; + } + + entry = { + isClipboard: true, + name: displayContent, + content: previewContent || textContent, + icon: "content_paste", + execute: function() { + Quickshell.clipboardText = String(textContent); + copyText(String(textContent)); + var preview = (textContent.length > 50) ? textContent.slice(0,50) + "…" : textContent; + Quickshell.execDetached(["notify-send", "Clipboard", "Text copied: " + preview]); + } + }; + } + results.push(entry); + } + }); + + if (results.length === 0) { + results.push({ + isClipboard: true, + name: "No clipboard history", + content: "No matching clipboard entries found", + icon: "content_paste_off" + }); + } + + return results; + } + + // Handle calculator + if (query.startsWith(">calc")) { + var expr = searchText.slice(5).trim(); + if (expr && isMathExpression(expr)) { + var value = safeEval(expr); + if (value !== null && value !== undefined && value !== "") { + var formattedResult = MathHelper.MathHelper.formatResult(value); + results.push({ + isCalculator: true, + name: `Calculator: ${expr} = ${formattedResult}`, + result: value, + expr: expr, + icon: "calculate", + execute: function() { + Quickshell.clipboardText = String(formattedResult); + clipboardTextCopyProcess.copyText(String(formattedResult)); + Quickshell.execDetached(["notify-send", "Calculator Result", `${expr} = ${formattedResult} (copied to clipboard)`]); + } + }); + } + } + + return results; + } + + // Regular app search + if (!query) { + results = results.concat(visibleEntries.sort(function (a, b) { + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); + })); + } else { + var fuzzyResults = Fuzzysort.go(query, visibleEntries, { + keys: ["name", "comment", "genericName"] + }); + results = results.concat(fuzzyResults.map(function (r) { + return r.obj; + })); + } + + console.log("[AppLauncher] Filtered entries:", results.length) + return results + } + + Component.onCompleted: { + console.log("[AppLauncher] Component completed") + console.log("[AppLauncher] DesktopEntries available:", typeof DesktopEntries !== 'undefined') + if (typeof DesktopEntries !== 'undefined') { + console.log("[AppLauncher] DesktopEntries.entries:", DesktopEntries.entries ? DesktopEntries.entries.length : 'undefined') + } + // Start clipboard refresh immediately on open + updateClipboardHistory(); + } + + function isMathExpression(str) { + // Allow more characters for enhanced math functions + return /^[-+*/().0-9\s\w]+$/.test(str); + } + + function safeEval(expr) { + return MathHelper.MathHelper.evaluate(expr); + } + + // Main content container + Rectangle { + anchors.centerIn: parent + width: Math.min(700 * scaling, parent.width * 0.75) + height: Math.min(550 * scaling, parent.height * 0.8) + radius: 32 * scaling + color: Colors.backgroundPrimary + border.color: Colors.outline + border.width: Style.borderThin * scaling + + // Subtle gradient background + gradient: Gradient { + GradientStop { position: 0.0; color: Qt.lighter(Colors.backgroundPrimary, 1.02) } + GradientStop { position: 1.0; color: Qt.darker(Colors.backgroundPrimary, 1.1) } + } + + + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginLarge * scaling + spacing: Style.marginMedium * scaling + + + + // Search bar + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 40 * scaling + Layout.bottomMargin: Style.marginMedium * scaling + radius: 20 * scaling + color: Colors.backgroundSecondary + border.color: searchInput.activeFocus ? Colors.accentPrimary : Colors.outline + border.width: searchInput.activeFocus ? 2 : 1 + + Row { + anchors.fill: parent + anchors.margins: 12 * scaling + spacing: 10 * scaling + + Text { + text: "search" + font.family: "Material Symbols Outlined" + font.pointSize: 16 * scaling + color: searchInput.activeFocus ? Colors.accentPrimary : Colors.textSecondary + } + + TextField { + id: searchInput + placeholderText: "Search applications..." + color: Colors.textPrimary + placeholderTextColor: Colors.textSecondary + background: null + font.pointSize: 13 * scaling + Layout.fillWidth: true + onTextChanged: { + searchText = text; + selectedIndex = 0; // Reset selection when search changes + } + selectedTextColor: Colors.textPrimary + selectionColor: Colors.accentPrimary + padding: 0 + verticalAlignment: TextInput.AlignVCenter + leftPadding: 0 + rightPadding: 0 + topPadding: 0 + bottomPadding: 0 + font.bold: true + Component.onCompleted: { + contentItem.cursorColor = Colors.textPrimary + contentItem.verticalAlignment = TextInput.AlignVCenter + // Focus the search bar by default + Qt.callLater(() => { + searchInput.forceActiveFocus() + }) + } + onActiveFocusChanged: contentItem.cursorColor = Colors.textPrimary + + Keys.onDownPressed: selectNext() + Keys.onUpPressed: selectPrev() + Keys.onEnterPressed: activateSelected() + Keys.onReturnPressed: activateSelected() + Keys.onEscapePressed: appLauncherPanel.hide() + } + } + + Behavior on border.color { + ColorAnimation { duration: 120 } + } + + Behavior on border.width { + NumberAnimation { duration: 120 } + } + } + + // Applications list + ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + + ListView { + id: appsList + anchors.fill: parent + spacing: 4 * scaling + model: filteredEntries + currentIndex: selectedIndex + + delegate: Rectangle { + width: appsList.width - Style.marginSmall * scaling + height: 56 * scaling + radius: 16 * scaling + property bool isSelected: index === selectedIndex + color: (appCardArea.containsMouse || isSelected) ? Qt.darker(Colors.accentPrimary, 1.1) : Colors.backgroundSecondary + border.color: (appCardArea.containsMouse || isSelected) ? Colors.accentPrimary : "transparent" + border.width: (appCardArea.containsMouse || isSelected) ? 2 : 0 + + Behavior on color { + ColorAnimation { duration: 150 } + } + + Behavior on border.color { + ColorAnimation { duration: 150 } + } + + Behavior on border.width { + NumberAnimation { duration: 150 } + } + + RowLayout { + anchors.fill: parent + anchors.margins: Style.marginMedium * scaling + spacing: Style.marginMedium * scaling + + // App icon with background + Rectangle { + Layout.preferredWidth: 40 * scaling + Layout.preferredHeight: 40 * scaling + radius: 14 * scaling + color: appCardArea.containsMouse ? Qt.darker(Colors.accentPrimary, 1.1) : Colors.backgroundTertiary + property bool iconLoaded: (modelData.isCalculator || modelData.isClipboard || modelData.isCommand) || (iconImg.status === Image.Ready && iconImg.source !== "" && iconImg.status !== Image.Error && iconImg.source !== "") + visible: !searchText.startsWith(">calc") // Hide icon when in calculator mode + + // Clipboard image display + Image { + id: clipboardImage + anchors.fill: parent + anchors.margins: 6 * scaling + visible: modelData.type === 'image' + source: modelData.data || "" + fillMode: Image.PreserveAspectCrop + asynchronous: true + cache: true + } + + IconImage { + id: iconImg + anchors.fill: parent + anchors.margins: 6 * scaling + asynchronous: true + source: modelData.isCalculator ? "calculate" : + modelData.isClipboard ? (modelData.type === 'image' ? "" : "content_paste") : + modelData.isCommand ? modelData.icon : + (modelData.icon ? Quickshell.iconPath(modelData.icon, "application-x-executable") : "") + visible: (modelData.isCalculator || modelData.isClipboard || modelData.isCommand || parent.iconLoaded) && modelData.type !== 'image' + } + + // Fallback icon container + Rectangle { + anchors.fill: parent + anchors.margins: 6 * scaling + radius: 10 * scaling + color: Colors.accentPrimary + opacity: 0.3 + visible: !parent.iconLoaded + } + + Text { + anchors.centerIn: parent + visible: !parent.iconLoaded && !(modelData.isCalculator || modelData.isClipboard || modelData.isCommand) + text: modelData.name ? modelData.name.charAt(0).toUpperCase() : "?" + font.pointSize: 18 * scaling + font.weight: Font.Bold + color: Colors.accentPrimary + } + + Behavior on color { + ColorAnimation { duration: 150 } + } + } + + // App info + ColumnLayout { + Layout.fillWidth: true + spacing: 2 * scaling + + NText { + text: modelData.name || "Unknown" + font.pointSize: 14 * scaling + font.weight: Font.Bold + color: (appCardArea.containsMouse || isSelected) ? Colors.backgroundPrimary : Colors.textPrimary + elide: Text.ElideRight + Layout.fillWidth: true + } + + NText { + text: modelData.isCalculator ? (modelData.expr + " = " + modelData.result) : + modelData.isClipboard ? modelData.content : + modelData.isCommand ? modelData.content : + (modelData.genericName || modelData.comment || "") + font.pointSize: 11 * scaling + color: (appCardArea.containsMouse || isSelected) ? Colors.backgroundPrimary : Colors.textSecondary + elide: Text.ElideRight + Layout.fillWidth: true + visible: text !== "" + } + } + + + } + + MouseArea { + id: appCardArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + selectedIndex = index; + activateSelected(); + } + } + } + } + } + + // No results message + NText { + text: searchText.trim() !== "" ? "No applications found" : "No applications available" + font.pointSize: Style.fontSizeLarge * scaling + color: Colors.textSecondary + horizontalAlignment: Text.AlignHCenter + Layout.fillWidth: true + visible: filteredEntries.length === 0 + } + + // Results count + NText { + text: searchText.startsWith(">clip") ? `${filteredEntries.length} clipboard item${filteredEntries.length !== 1 ? 's' : ''}` : + searchText.startsWith(">calc") ? `${filteredEntries.length} result${filteredEntries.length !== 1 ? 's' : ''}` : + `${filteredEntries.length} application${filteredEntries.length !== 1 ? 's' : ''}` + font.pointSize: Style.fontSizeSmall * scaling + color: Colors.textSecondary + horizontalAlignment: Text.AlignHCenter + Layout.fillWidth: true + visible: searchText.trim() !== "" + } + } + } + } + } +} \ No newline at end of file diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index 97e135c..31f2fc9 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -342,24 +342,6 @@ WlSessionLock { anchors.margins: 12 * Scaling.scale(screen) spacing: 12 * Scaling.scale(screen) - Text { - text: "●" - color: Colors.error - font.pixelSize: 16 * Scaling.scale(screen) - } - - Text { - text: "●" - color: Colors.warning - font.pixelSize: 16 * Scaling.scale(screen) - } - - Text { - text: "●" - color: Colors.accentPrimary - font.pixelSize: 16 * Scaling.scale(screen) - } - Text { text: "SECURE TERMINAL" color: Colors.textPrimary @@ -554,6 +536,7 @@ WlSessionLock { border.width: 1 enabled: !lock.authenticating Layout.alignment: Qt.AlignRight + Layout.bottomMargin: -12 * Scaling.scale(screen) Text { anchors.centerIn: parent diff --git a/Services/Clipboard.qml b/Services/Clipboard.qml new file mode 100644 index 0000000..0d82e37 --- /dev/null +++ b/Services/Clipboard.qml @@ -0,0 +1,139 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Services + +Singleton { + id: root + + property var history: [] + property bool initialized: false + + // Internal state + property bool _enabled: true + + Timer { + interval: 1000 + repeat: true + running: root._enabled + onTriggered: root.refresh() + } + + // Detect current clipboard types (text/image) + Process { + id: typeProcess + property bool isLoading: false + property var currentTypes: [] + + onExited: (exitCode, exitStatus) => { + if (exitCode === 0) { + currentTypes = String(stdout.text).trim().split('\n').filter(t => t) + + const imageType = currentTypes.find(t => t.startsWith('image/')) + if (imageType) { + imageProcess.mimeType = imageType + imageProcess.command = ["sh", "-c", `wl-paste -n -t "${imageType}" | base64 -w 0`] + imageProcess.running = true + } else { + textProcess.command = ["wl-paste", "-n", "--type", "text/plain"] + textProcess.running = true + } + } else { + typeProcess.isLoading = false + } + } + + stdout: StdioCollector {} + } + + // Read image data + Process { + id: imageProcess + property string mimeType: "" + + onExited: (exitCode, exitStatus) => { + if (exitCode === 0) { + const base64 = stdout.text.trim() + if (base64) { + const entry = { + type: 'image', + mimeType: mimeType, + data: `data:${mimeType};base64,${base64}`, + timestamp: new Date().getTime() + } + + const exists = root.history.find(item => item.type === 'image' && item.data === entry.data) + if (!exists) { + root.history = [entry, ...root.history].slice(0, 20) + } + } + } + + if (!textProcess.isLoading) { + root.initialized = true + } + typeProcess.isLoading = false + } + + stdout: StdioCollector {} + } + + // Read text data + Process { + id: textProcess + property bool isLoading: false + + onExited: (exitCode, exitStatus) => { + if (exitCode === 0) { + const content = String(stdout.text).trim() + if (content) { + const entry = { + type: 'text', + content: content, + timestamp: new Date().getTime() + } + + const exists = root.history.find(item => { + if (item.type === 'text') { + return item.content === content + } + return item === content + }) + + if (!exists) { + const newHistory = root.history.map(item => { + if (typeof item === 'string') { + return { + type: 'text', + content: item, + timestamp: new Date().getTime() + } + } + return item + }) + + root.history = [entry, ...newHistory].slice(0, 20) + } + } + } else { + textProcess.isLoading = false + } + + root.initialized = true + typeProcess.isLoading = false + } + + stdout: StdioCollector {} + } + + function refresh() { + if (!typeProcess.isLoading && !textProcess.isLoading) { + typeProcess.isLoading = true + typeProcess.command = ["wl-paste", "-l"] + typeProcess.running = true + } + } +} + diff --git a/Services/IPCManager.qml b/Services/IPCManager.qml index 992b7c9..998635b 100644 --- a/Services/IPCManager.qml +++ b/Services/IPCManager.qml @@ -37,7 +37,8 @@ Item { IpcHandler { target: "appLauncher" - function toggle() {// TODO + function toggle() { + appLauncherPanel.isLoaded = !appLauncherPanel.isLoaded } } diff --git a/shell.qml b/shell.qml index 81b5e7d..b68c05f 100644 --- a/shell.qml +++ b/shell.qml @@ -10,6 +10,7 @@ import qs.Modules.Calendar import qs.Modules.Demo import qs.Modules.Background import qs.Modules.SidePanel +import qs.Modules.AppLauncher import qs.Modules.Notification import qs.Modules.Settings import qs.Services @@ -24,6 +25,12 @@ ShellRoot { Bar {} Dock {} + AppLauncher { + id: appLauncherPanel + } + + + DemoPanel { id: demoPanel } From 0572e0d646ba8171a1fa12b8d47d50471f944884 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Thu, 14 Aug 2025 18:47:24 +0200 Subject: [PATCH 274/394] Fix hover text color in PowerMenu.qml --- Modules/AppLauncher/AppLauncher.qml | 4 ++-- Modules/SidePanel/PowerMenu.qml | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Modules/AppLauncher/AppLauncher.qml b/Modules/AppLauncher/AppLauncher.qml index f7f19b2..aef7dd9 100644 --- a/Modules/AppLauncher/AppLauncher.qml +++ b/Modules/AppLauncher/AppLauncher.qml @@ -455,7 +455,7 @@ NLoader { text: modelData.name || "Unknown" font.pointSize: 14 * scaling font.weight: Font.Bold - color: (appCardArea.containsMouse || isSelected) ? Colors.backgroundPrimary : Colors.textPrimary + color: Colors.textPrimary elide: Text.ElideRight Layout.fillWidth: true } @@ -466,7 +466,7 @@ NLoader { modelData.isCommand ? modelData.content : (modelData.genericName || modelData.comment || "") font.pointSize: 11 * scaling - color: (appCardArea.containsMouse || isSelected) ? Colors.backgroundPrimary : Colors.textSecondary + color: (appCardArea.containsMouse || isSelected) ? Colors.textPrimary : Colors.textSecondary elide: Text.ElideRight Layout.fillWidth: true visible: text !== "" diff --git a/Modules/SidePanel/PowerMenu.qml b/Modules/SidePanel/PowerMenu.qml index d11f3e5..2815fc5 100644 --- a/Modules/SidePanel/PowerMenu.qml +++ b/Modules/SidePanel/PowerMenu.qml @@ -88,7 +88,7 @@ NPanel { font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 } - color: lockButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary + color: lockButtonArea.containsMouse ? Colors.textPrimary : Colors.textPrimary verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -96,7 +96,7 @@ NPanel { Text { text: "Lock Screen" - color: lockButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary + color: lockButtonArea.containsMouse ? Colors.textPrimary : Colors.textPrimary verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -148,7 +148,7 @@ NPanel { font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 } - color: suspendButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary + color: suspendButtonArea.containsMouse ? Colors.textPrimary : Colors.textPrimary verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -156,7 +156,7 @@ NPanel { Text { text: "Suspend" - color: suspendButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary + color: suspendButtonArea.containsMouse ? Colors.textPrimary : Colors.textPrimary verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -206,7 +206,7 @@ NPanel { font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 } - color: rebootButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary + color: rebootButtonArea.containsMouse ? Colors.textPrimary : Colors.textPrimary verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -214,7 +214,7 @@ NPanel { Text { text: "Reboot" - color: rebootButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary + color: rebootButtonArea.containsMouse ? Colors.textPrimary : Colors.textPrimary verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -264,7 +264,7 @@ NPanel { font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 } - color: logoutButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary + color: logoutButtonArea.containsMouse ? Colors.textPrimary : Colors.textPrimary verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -272,7 +272,7 @@ NPanel { Text { text: "Logout" - color: logoutButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary + color: logoutButtonArea.containsMouse ? Colors.textPrimary : Colors.textPrimary verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -322,7 +322,7 @@ NPanel { font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 } - color: shutdownButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary + color: shutdownButtonArea.containsMouse ? Colors.textPrimary : Colors.textPrimary verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -330,7 +330,7 @@ NPanel { Text { text: "Shutdown" - color: shutdownButtonArea.containsMouse ? Colors.onAccent : Colors.textPrimary + color: shutdownButtonArea.containsMouse ? Colors.textPrimary : Colors.textPrimary verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling From 3f64bb1879664f4e825fae7edfa974e8aa7d4eec Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Thu, 14 Aug 2025 18:59:49 +0200 Subject: [PATCH 275/394] Fix hover text color in TrayMenu.qml --- Modules/Bar/TrayMenu.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/Bar/TrayMenu.qml b/Modules/Bar/TrayMenu.qml index b7dc336..81a0778 100644 --- a/Modules/Bar/TrayMenu.qml +++ b/Modules/Bar/TrayMenu.qml @@ -126,7 +126,7 @@ PopupWindow { id: text Layout.fillWidth: true color: (modelData?.enabled - ?? true) ? (mouseArea.containsMouse ? Colors.onAccent : Colors.textPrimary) : Colors.textDisabled + ?? true) ? (mouseArea.containsMouse ? Colors.textPrimary : Colors.textPrimary) : Colors.textDisabled text: modelData?.text ?? "" font.pointSize: Style.fontSizeSmall * scaling verticalAlignment: Text.AlignVCenter @@ -350,7 +350,7 @@ PopupWindow { color: mouseArea.containsMouse ? Colors.hover : "transparent" radius: Style.radiusSmall * scaling visible: !(modelData?.isSeparator ?? false) - property color hoverTextColor: mouseArea.containsMouse ? Colors.onAccent : Colors.textPrimary + property color hoverTextColor: mouseArea.containsMouse ? Colors.textPrimary : Colors.textPrimary RowLayout { anchors.fill: parent From 151e2b6aafb2d66cca1a058ceaccc8ff32f2fe16 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Thu, 14 Aug 2025 14:58:00 -0400 Subject: [PATCH 276/394] Formatting --- Modules/AppLauncher/AppLauncher.qml | 896 ++++++++++++++-------------- Modules/Dock/Dock.qml | 593 +++++++++--------- Modules/LockScreen/LockScreen.qml | 2 +- Modules/Settings/Tabs/About.qml | 3 +- Services/Clipboard.qml | 207 ++++--- Services/GitHub.qml | 7 +- Services/Wallpapers.qml | 1 + 7 files changed, 869 insertions(+), 840 deletions(-) diff --git a/Modules/AppLauncher/AppLauncher.qml b/Modules/AppLauncher/AppLauncher.qml index aef7dd9..3dc07d5 100644 --- a/Modules/AppLauncher/AppLauncher.qml +++ b/Modules/AppLauncher/AppLauncher.qml @@ -13,273 +13,270 @@ import "../../Helpers/FuzzySort.js" as Fuzzysort import "../../Helpers/MathHelper.js" as MathHelper NLoader { - id: appLauncher - isLoaded: false - // Clipboard state is persisted in Services/Clipboard.qml - content: Component { - NPanel { - id: appLauncherPanel + id: appLauncher + isLoaded: false + // Clipboard state is persisted in Services/Clipboard.qml + content: Component { + NPanel { + id: appLauncherPanel - WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand - - // No local timer/processes; use persistent Clipboard service + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand - // Removed local clipboard processes; handled by Clipboard service + // No local timer/processes; use persistent Clipboard service - + // Removed local clipboard processes; handled by Clipboard service - + // Copy helpers via simple exec; avoid keeping processes alive locally + function copyImageBase64(mime, base64) { + Quickshell.execDetached(["sh", "-lc", `printf %s ${base64} | base64 -d | wl-copy -t '${mime}'`]) + } - // Copy helpers via simple exec; avoid keeping processes alive locally - function copyImageBase64(mime, base64) { - Quickshell.execDetached(["sh", "-lc", `printf %s ${base64} | base64 -d | wl-copy -t '${mime}'`]) - } + function copyText(text) { + Quickshell.execDetached(["sh", "-lc", `printf %s ${text} | wl-copy -t text/plain;charset=utf-8`]) + } - function copyText(text) { - Quickshell.execDetached(["sh", "-lc", `printf %s ${text} | wl-copy -t text/plain;charset=utf-8`]) - } + function updateClipboardHistory() { + Clipboard.refresh() + } + function selectNext() { + if (filteredEntries.length > 0) { + selectedIndex = Math.min(selectedIndex + 1, filteredEntries.length - 1) + } + } + function selectPrev() { + if (filteredEntries.length > 0) { + selectedIndex = Math.max(selectedIndex - 1, 0) + } + } - function updateClipboardHistory() { - Clipboard.refresh(); - } + function activateSelected() { + if (filteredEntries.length === 0) + return - function selectNext() { - if (filteredEntries.length > 0) { - selectedIndex = Math.min(selectedIndex + 1, filteredEntries.length - 1); + var modelData = filteredEntries[selectedIndex] + if (modelData && modelData.execute) { + if (modelData.isCommand) { + modelData.execute() + return + } else { + modelData.execute() + } + appLauncherPanel.hide() + } + } + + property var desktopEntries: DesktopEntries.applications.values + property string searchText: "" + property int selectedIndex: 0 + property var filteredEntries: { + console.log("[AppLauncher] Total desktop entries:", desktopEntries ? desktopEntries.length : 0) + if (!desktopEntries || desktopEntries.length === 0) { + console.log("[AppLauncher] No desktop entries available") + return [] + } + + // Filter out entries that shouldn't be displayed + var visibleEntries = desktopEntries.filter(entry => { + if (!entry || entry.noDisplay) { + return false + } + return true + }) + + console.log("[AppLauncher] Visible entries:", visibleEntries.length) + + var query = searchText ? searchText.toLowerCase() : "" + var results = [] + + // Handle special commands + if (query === ">") { + results.push({ + "isCommand": true, + "name": ">calc", + "content": "Calculator - evaluate mathematical expressions", + "icon": "calculate", + "execute": function () { + searchText = ">calc " + searchInput.cursorPosition = searchText.length + } + }) + + results.push({ + "isCommand": true, + "name": ">clip", + "content": "Clipboard history - browse and restore clipboard items", + "icon": "content_paste", + "execute": function () { + searchText = ">clip " + searchInput.cursorPosition = searchText.length + } + }) + + return results + } + + // Handle clipboard history + if (query.startsWith(">clip")) { + if (!Clipboard.initialized) { + Clipboard.refresh() + } + const searchTerm = query.slice(5).trim() + + Clipboard.history.forEach(function (clip, index) { + let searchContent = clip.type === 'image' ? clip.mimeType : clip.content || clip + + if (!searchTerm || searchContent.toLowerCase().includes(searchTerm)) { + let entry + if (clip.type === 'image') { + entry = { + "isClipboard": true, + "name": "Image from " + new Date(clip.timestamp).toLocaleTimeString(), + "content": "Image: " + clip.mimeType, + "icon": "image", + "type": 'image', + "data": clip.data, + "execute": function () { + const base64Data = clip.data.split(',')[1] + copyImageBase64(clip.mimeType, base64Data) + Quickshell.execDetached(["notify-send", "Clipboard", "Image copied: " + clip.mimeType]) + } } - } + } else { + const textContent = clip.content || clip + let displayContent = textContent + let previewContent = "" - function selectPrev() { - if (filteredEntries.length > 0) { - selectedIndex = Math.max(selectedIndex - 1, 0); + displayContent = displayContent.replace(/\s+/g, ' ').trim() + + if (displayContent.length > 50) { + previewContent = displayContent + displayContent = displayContent.split('\n')[0].substring(0, 50) + "..." } + + entry = { + "isClipboard": true, + "name": displayContent, + "content": previewContent || textContent, + "icon": "content_paste", + "execute": function () { + Quickshell.clipboardText = String(textContent) + copyText(String(textContent)) + var preview = (textContent.length > 50) ? textContent.slice(0, 50) + "…" : textContent + Quickshell.execDetached(["notify-send", "Clipboard", "Text copied: " + preview]) + } + } + } + results.push(entry) } + }) - function activateSelected() { - if (filteredEntries.length === 0) return; + if (results.length === 0) { + results.push({ + "isClipboard": true, + "name": "No clipboard history", + "content": "No matching clipboard entries found", + "icon": "content_paste_off" + }) + } - var modelData = filteredEntries[selectedIndex]; - if (modelData && modelData.execute) { - if (modelData.isCommand) { - modelData.execute(); - return; - } else { - modelData.execute(); + return results + } + + // Handle calculator + if (query.startsWith(">calc")) { + var expr = searchText.slice(5).trim() + if (expr && isMathExpression(expr)) { + var value = safeEval(expr) + if (value !== null && value !== undefined && value !== "") { + var formattedResult = MathHelper.MathHelper.formatResult(value) + results.push({ + "isCalculator": true, + "name": `Calculator: ${expr} = ${formattedResult}`, + "result": value, + "expr": expr, + "icon": "calculate", + "execute": function () { + Quickshell.clipboardText = String(formattedResult) + clipboardTextCopyProcess.copyText(String(formattedResult)) + Quickshell.execDetached( + ["notify-send", "Calculator Result", `${expr} = ${formattedResult} (copied to clipboard)`]) + } + }) + } + } + + return results + } + + // Regular app search + if (!query) { + results = results.concat(visibleEntries.sort(function (a, b) { + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()) + })) + } else { + var fuzzyResults = Fuzzysort.go(query, visibleEntries, { + "keys": ["name", "comment", "genericName"] + }) + results = results.concat(fuzzyResults.map(function (r) { + return r.obj + })) + } + + console.log("[AppLauncher] Filtered entries:", results.length) + return results + } + + Component.onCompleted: { + console.log("[AppLauncher] Component completed") + console.log("[AppLauncher] DesktopEntries available:", typeof DesktopEntries !== 'undefined') + if (typeof DesktopEntries !== 'undefined') { + console.log("[AppLauncher] DesktopEntries.entries:", + DesktopEntries.entries ? DesktopEntries.entries.length : 'undefined') + } + // Start clipboard refresh immediately on open + updateClipboardHistory() + } + + function isMathExpression(str) { + // Allow more characters for enhanced math functions + return /^[-+*/().0-9\s\w]+$/.test(str) + } + + function safeEval(expr) { + return MathHelper.MathHelper.evaluate(expr) + } + + // Main content container + Rectangle { + anchors.centerIn: parent + width: Math.min(700 * scaling, parent.width * 0.75) + height: Math.min(550 * scaling, parent.height * 0.8) + radius: 32 * scaling + color: Colors.backgroundPrimary + border.color: Colors.outline + border.width: Style.borderThin * scaling + + // Subtle gradient background + gradient: Gradient { + GradientStop { + position: 0.0 + color: Qt.lighter(Colors.backgroundPrimary, 1.02) + } + GradientStop { + position: 1.0 + color: Qt.darker(Colors.backgroundPrimary, 1.1) + } } - appLauncherPanel.hide(); - } - } - - property var desktopEntries: DesktopEntries.applications.values - property string searchText: "" - property int selectedIndex: 0 - property var filteredEntries: { - console.log("[AppLauncher] Total desktop entries:", desktopEntries ? desktopEntries.length : 0) - if (!desktopEntries || desktopEntries.length === 0) { - console.log("[AppLauncher] No desktop entries available") - return [] - } - - // Filter out entries that shouldn't be displayed - var visibleEntries = desktopEntries.filter(entry => { - if (!entry || entry.noDisplay) { - return false - } - return true - }) - - console.log("[AppLauncher] Visible entries:", visibleEntries.length) - - var query = searchText ? searchText.toLowerCase() : ""; - var results = []; - - // Handle special commands - if (query === ">") { - results.push({ - isCommand: true, - name: ">calc", - content: "Calculator - evaluate mathematical expressions", - icon: "calculate", - execute: function() { - searchText = ">calc "; - searchInput.cursorPosition = searchText.length; - } - }); - - results.push({ - isCommand: true, - name: ">clip", - content: "Clipboard history - browse and restore clipboard items", - icon: "content_paste", - execute: function() { - searchText = ">clip "; - searchInput.cursorPosition = searchText.length; - } - }); - - return results; - } - - // Handle clipboard history - if (query.startsWith(">clip")) { - if (!Clipboard.initialized) { - Clipboard.refresh(); - } - const searchTerm = query.slice(5).trim(); - - Clipboard.history.forEach(function(clip, index) { - let searchContent = clip.type === 'image' ? - clip.mimeType : - clip.content || clip; - - if (!searchTerm || searchContent.toLowerCase().includes(searchTerm)) { - let entry; - if (clip.type === 'image') { - entry = { - isClipboard: true, - name: "Image from " + new Date(clip.timestamp).toLocaleTimeString(), - content: "Image: " + clip.mimeType, - icon: "image", - type: 'image', - data: clip.data, - execute: function() { - const base64Data = clip.data.split(',')[1]; - copyImageBase64(clip.mimeType, base64Data); - Quickshell.execDetached(["notify-send", "Clipboard", "Image copied: " + clip.mimeType]); - } - }; - } else { - const textContent = clip.content || clip; - let displayContent = textContent; - let previewContent = ""; - - displayContent = displayContent.replace(/\s+/g, ' ').trim(); - - if (displayContent.length > 50) { - previewContent = displayContent; - displayContent = displayContent.split('\n')[0].substring(0, 50) + "..."; - } - - entry = { - isClipboard: true, - name: displayContent, - content: previewContent || textContent, - icon: "content_paste", - execute: function() { - Quickshell.clipboardText = String(textContent); - copyText(String(textContent)); - var preview = (textContent.length > 50) ? textContent.slice(0,50) + "…" : textContent; - Quickshell.execDetached(["notify-send", "Clipboard", "Text copied: " + preview]); - } - }; - } - results.push(entry); - } - }); - - if (results.length === 0) { - results.push({ - isClipboard: true, - name: "No clipboard history", - content: "No matching clipboard entries found", - icon: "content_paste_off" - }); - } - - return results; - } - - // Handle calculator - if (query.startsWith(">calc")) { - var expr = searchText.slice(5).trim(); - if (expr && isMathExpression(expr)) { - var value = safeEval(expr); - if (value !== null && value !== undefined && value !== "") { - var formattedResult = MathHelper.MathHelper.formatResult(value); - results.push({ - isCalculator: true, - name: `Calculator: ${expr} = ${formattedResult}`, - result: value, - expr: expr, - icon: "calculate", - execute: function() { - Quickshell.clipboardText = String(formattedResult); - clipboardTextCopyProcess.copyText(String(formattedResult)); - Quickshell.execDetached(["notify-send", "Calculator Result", `${expr} = ${formattedResult} (copied to clipboard)`]); - } - }); - } - } - - return results; - } - - // Regular app search - if (!query) { - results = results.concat(visibleEntries.sort(function (a, b) { - return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); - })); - } else { - var fuzzyResults = Fuzzysort.go(query, visibleEntries, { - keys: ["name", "comment", "genericName"] - }); - results = results.concat(fuzzyResults.map(function (r) { - return r.obj; - })); - } - - console.log("[AppLauncher] Filtered entries:", results.length) - return results - } - Component.onCompleted: { - console.log("[AppLauncher] Component completed") - console.log("[AppLauncher] DesktopEntries available:", typeof DesktopEntries !== 'undefined') - if (typeof DesktopEntries !== 'undefined') { - console.log("[AppLauncher] DesktopEntries.entries:", DesktopEntries.entries ? DesktopEntries.entries.length : 'undefined') - } - // Start clipboard refresh immediately on open - updateClipboardHistory(); - } + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginLarge * scaling + spacing: Style.marginMedium * scaling - function isMathExpression(str) { - // Allow more characters for enhanced math functions - return /^[-+*/().0-9\s\w]+$/.test(str); - } - - function safeEval(expr) { - return MathHelper.MathHelper.evaluate(expr); - } - - // Main content container - Rectangle { - anchors.centerIn: parent - width: Math.min(700 * scaling, parent.width * 0.75) - height: Math.min(550 * scaling, parent.height * 0.8) - radius: 32 * scaling - color: Colors.backgroundPrimary - border.color: Colors.outline - border.width: Style.borderThin * scaling - - // Subtle gradient background - gradient: Gradient { - GradientStop { position: 0.0; color: Qt.lighter(Colors.backgroundPrimary, 1.02) } - GradientStop { position: 1.0; color: Qt.darker(Colors.backgroundPrimary, 1.1) } - } - - - - ColumnLayout { - anchors.fill: parent - anchors.margins: Style.marginLarge * scaling - spacing: Style.marginMedium * scaling - - - - // Search bar - Rectangle { + // Search bar + Rectangle { Layout.fillWidth: true Layout.preferredHeight: 40 * scaling Layout.bottomMargin: Style.marginMedium * scaling @@ -289,67 +286,71 @@ NLoader { border.width: searchInput.activeFocus ? 2 : 1 Row { - anchors.fill: parent - anchors.margins: 12 * scaling - spacing: 10 * scaling + anchors.fill: parent + anchors.margins: 12 * scaling + spacing: 10 * scaling - Text { - text: "search" - font.family: "Material Symbols Outlined" - font.pointSize: 16 * scaling - color: searchInput.activeFocus ? Colors.accentPrimary : Colors.textSecondary + Text { + text: "search" + font.family: "Material Symbols Outlined" + font.pointSize: 16 * scaling + color: searchInput.activeFocus ? Colors.accentPrimary : Colors.textSecondary + } + + TextField { + id: searchInput + placeholderText: "Search applications..." + color: Colors.textPrimary + placeholderTextColor: Colors.textSecondary + background: null + font.pointSize: 13 * scaling + Layout.fillWidth: true + onTextChanged: { + searchText = text + selectedIndex = 0 // Reset selection when search changes } - - TextField { - id: searchInput - placeholderText: "Search applications..." - color: Colors.textPrimary - placeholderTextColor: Colors.textSecondary - background: null - font.pointSize: 13 * scaling - Layout.fillWidth: true - onTextChanged: { - searchText = text; - selectedIndex = 0; // Reset selection when search changes - } - selectedTextColor: Colors.textPrimary - selectionColor: Colors.accentPrimary - padding: 0 - verticalAlignment: TextInput.AlignVCenter - leftPadding: 0 - rightPadding: 0 - topPadding: 0 - bottomPadding: 0 - font.bold: true - Component.onCompleted: { - contentItem.cursorColor = Colors.textPrimary - contentItem.verticalAlignment = TextInput.AlignVCenter - // Focus the search bar by default - Qt.callLater(() => { - searchInput.forceActiveFocus() - }) - } - onActiveFocusChanged: contentItem.cursorColor = Colors.textPrimary - - Keys.onDownPressed: selectNext() - Keys.onUpPressed: selectPrev() - Keys.onEnterPressed: activateSelected() - Keys.onReturnPressed: activateSelected() - Keys.onEscapePressed: appLauncherPanel.hide() + selectedTextColor: Colors.textPrimary + selectionColor: Colors.accentPrimary + padding: 0 + verticalAlignment: TextInput.AlignVCenter + leftPadding: 0 + rightPadding: 0 + topPadding: 0 + bottomPadding: 0 + font.bold: true + Component.onCompleted: { + contentItem.cursorColor = Colors.textPrimary + contentItem.verticalAlignment = TextInput.AlignVCenter + // Focus the search bar by default + Qt.callLater(() => { + searchInput.forceActiveFocus() + }) } + onActiveFocusChanged: contentItem.cursorColor = Colors.textPrimary + + Keys.onDownPressed: selectNext() + Keys.onUpPressed: selectPrev() + Keys.onEnterPressed: activateSelected() + Keys.onReturnPressed: activateSelected() + Keys.onEscapePressed: appLauncherPanel.hide() + } } Behavior on border.color { - ColorAnimation { duration: 120 } + ColorAnimation { + duration: 120 + } } Behavior on border.width { - NumberAnimation { duration: 120 } + NumberAnimation { + duration: 120 + } } - } + } - // Applications list - ScrollView { + // Applications list + ScrollView { Layout.fillWidth: true Layout.fillHeight: true clip: true @@ -357,163 +358,176 @@ NLoader { ScrollBar.vertical.policy: ScrollBar.AsNeeded ListView { - id: appsList - anchors.fill: parent - spacing: 4 * scaling - model: filteredEntries - currentIndex: selectedIndex + id: appsList + anchors.fill: parent + spacing: 4 * scaling + model: filteredEntries + currentIndex: selectedIndex - delegate: Rectangle { - width: appsList.width - Style.marginSmall * scaling - height: 56 * scaling - radius: 16 * scaling - property bool isSelected: index === selectedIndex - color: (appCardArea.containsMouse || isSelected) ? Qt.darker(Colors.accentPrimary, 1.1) : Colors.backgroundSecondary - border.color: (appCardArea.containsMouse || isSelected) ? Colors.accentPrimary : "transparent" - border.width: (appCardArea.containsMouse || isSelected) ? 2 : 0 - - Behavior on color { - ColorAnimation { duration: 150 } - } - - Behavior on border.color { - ColorAnimation { duration: 150 } - } - - Behavior on border.width { - NumberAnimation { duration: 150 } - } + delegate: Rectangle { + width: appsList.width - Style.marginSmall * scaling + height: 56 * scaling + radius: 16 * scaling + property bool isSelected: index === selectedIndex + color: (appCardArea.containsMouse || isSelected) ? Qt.darker( + Colors.accentPrimary, + 1.1) : Colors.backgroundSecondary + border.color: (appCardArea.containsMouse + || isSelected) ? Colors.accentPrimary : "transparent" + border.width: (appCardArea.containsMouse || isSelected) ? 2 : 0 - RowLayout { - anchors.fill: parent - anchors.margins: Style.marginMedium * scaling - spacing: Style.marginMedium * scaling - - // App icon with background - Rectangle { - Layout.preferredWidth: 40 * scaling - Layout.preferredHeight: 40 * scaling - radius: 14 * scaling - color: appCardArea.containsMouse ? Qt.darker(Colors.accentPrimary, 1.1) : Colors.backgroundTertiary - property bool iconLoaded: (modelData.isCalculator || modelData.isClipboard || modelData.isCommand) || (iconImg.status === Image.Ready && iconImg.source !== "" && iconImg.status !== Image.Error && iconImg.source !== "") - visible: !searchText.startsWith(">calc") // Hide icon when in calculator mode - - // Clipboard image display - Image { - id: clipboardImage - anchors.fill: parent - anchors.margins: 6 * scaling - visible: modelData.type === 'image' - source: modelData.data || "" - fillMode: Image.PreserveAspectCrop - asynchronous: true - cache: true - } - - IconImage { - id: iconImg - anchors.fill: parent - anchors.margins: 6 * scaling - asynchronous: true - source: modelData.isCalculator ? "calculate" : - modelData.isClipboard ? (modelData.type === 'image' ? "" : "content_paste") : - modelData.isCommand ? modelData.icon : - (modelData.icon ? Quickshell.iconPath(modelData.icon, "application-x-executable") : "") - visible: (modelData.isCalculator || modelData.isClipboard || modelData.isCommand || parent.iconLoaded) && modelData.type !== 'image' - } - - // Fallback icon container - Rectangle { - anchors.fill: parent - anchors.margins: 6 * scaling - radius: 10 * scaling - color: Colors.accentPrimary - opacity: 0.3 - visible: !parent.iconLoaded - } - - Text { - anchors.centerIn: parent - visible: !parent.iconLoaded && !(modelData.isCalculator || modelData.isClipboard || modelData.isCommand) - text: modelData.name ? modelData.name.charAt(0).toUpperCase() : "?" - font.pointSize: 18 * scaling - font.weight: Font.Bold - color: Colors.accentPrimary - } - - Behavior on color { - ColorAnimation { duration: 150 } - } - } - - // App info - ColumnLayout { - Layout.fillWidth: true - spacing: 2 * scaling - - NText { - text: modelData.name || "Unknown" - font.pointSize: 14 * scaling - font.weight: Font.Bold - color: Colors.textPrimary - elide: Text.ElideRight - Layout.fillWidth: true - } - - NText { - text: modelData.isCalculator ? (modelData.expr + " = " + modelData.result) : - modelData.isClipboard ? modelData.content : - modelData.isCommand ? modelData.content : - (modelData.genericName || modelData.comment || "") - font.pointSize: 11 * scaling - color: (appCardArea.containsMouse || isSelected) ? Colors.textPrimary : Colors.textSecondary - elide: Text.ElideRight - Layout.fillWidth: true - visible: text !== "" - } - } - - - } - - MouseArea { - id: appCardArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - - onClicked: { - selectedIndex = index; - activateSelected(); - } - } + Behavior on color { + ColorAnimation { + duration: 150 + } } - } - } - // No results message - NText { + Behavior on border.color { + ColorAnimation { + duration: 150 + } + } + + Behavior on border.width { + NumberAnimation { + duration: 150 + } + } + + RowLayout { + anchors.fill: parent + anchors.margins: Style.marginMedium * scaling + spacing: Style.marginMedium * scaling + + // App icon with background + Rectangle { + Layout.preferredWidth: 40 * scaling + Layout.preferredHeight: 40 * scaling + radius: 14 * scaling + color: appCardArea.containsMouse ? Qt.darker(Colors.accentPrimary, + 1.1) : Colors.backgroundTertiary + property bool iconLoaded: (modelData.isCalculator || modelData.isClipboard + || modelData.isCommand) || (iconImg.status === Image.Ready + && iconImg.source !== "" + && iconImg.status !== Image.Error + && iconImg.source !== "") + visible: !searchText.startsWith(">calc") // Hide icon when in calculator mode + + // Clipboard image display + Image { + id: clipboardImage + anchors.fill: parent + anchors.margins: 6 * scaling + visible: modelData.type === 'image' + source: modelData.data || "" + fillMode: Image.PreserveAspectCrop + asynchronous: true + cache: true + } + + IconImage { + id: iconImg + anchors.fill: parent + anchors.margins: 6 * scaling + asynchronous: true + source: modelData.isCalculator ? "calculate" : modelData.isClipboard ? (modelData.type === 'image' ? "" : "content_paste") : modelData.isCommand ? modelData.icon : (modelData.icon ? Quickshell.iconPath(modelData.icon, "application-x-executable") : "") + visible: (modelData.isCalculator || modelData.isClipboard || modelData.isCommand + || parent.iconLoaded) && modelData.type !== 'image' + } + + // Fallback icon container + Rectangle { + anchors.fill: parent + anchors.margins: 6 * scaling + radius: 10 * scaling + color: Colors.accentPrimary + opacity: 0.3 + visible: !parent.iconLoaded + } + + Text { + anchors.centerIn: parent + visible: !parent.iconLoaded && !(modelData.isCalculator || modelData.isClipboard + || modelData.isCommand) + text: modelData.name ? modelData.name.charAt(0).toUpperCase() : "?" + font.pointSize: 18 * scaling + font.weight: Font.Bold + color: Colors.accentPrimary + } + + Behavior on color { + ColorAnimation { + duration: 150 + } + } + } + + // App info + ColumnLayout { + Layout.fillWidth: true + spacing: 2 * scaling + + NText { + text: modelData.name || "Unknown" + font.pointSize: 14 * scaling + font.weight: Font.Bold + color: Colors.textPrimary + elide: Text.ElideRight + Layout.fillWidth: true + } + + NText { + text: modelData.isCalculator ? (modelData.expr + " = " + modelData.result) : modelData.isClipboard ? modelData.content : modelData.isCommand ? modelData.content : (modelData.genericName || modelData.comment || "") + font.pointSize: 11 * scaling + color: (appCardArea.containsMouse + || isSelected) ? Colors.textPrimary : Colors.textSecondary + elide: Text.ElideRight + Layout.fillWidth: true + visible: text !== "" + } + } + } + + MouseArea { + id: appCardArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + selectedIndex = index + activateSelected() + } + } + } + } + } + + // No results message + NText { text: searchText.trim() !== "" ? "No applications found" : "No applications available" font.pointSize: Style.fontSizeLarge * scaling color: Colors.textSecondary horizontalAlignment: Text.AlignHCenter Layout.fillWidth: true visible: filteredEntries.length === 0 - } + } - // Results count - NText { - text: searchText.startsWith(">clip") ? `${filteredEntries.length} clipboard item${filteredEntries.length !== 1 ? 's' : ''}` : - searchText.startsWith(">calc") ? `${filteredEntries.length} result${filteredEntries.length !== 1 ? 's' : ''}` : - `${filteredEntries.length} application${filteredEntries.length !== 1 ? 's' : ''}` + // Results count + NText { + text: searchText.startsWith( + ">clip") ? `${filteredEntries.length} clipboard item${filteredEntries.length + !== 1 ? 's' : ''}` : searchText.startsWith( + ">calc") ? `${filteredEntries.length} result${filteredEntries.length + !== 1 ? 's' : ''}` : `${filteredEntries.length} application${filteredEntries.length !== 1 ? 's' : ''}` font.pointSize: Style.fontSizeSmall * scaling color: Colors.textSecondary horizontalAlignment: Text.AlignHCenter Layout.fillWidth: true visible: searchText.trim() !== "" + } } - } - } - } - } -} \ No newline at end of file + } + } + } + } diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml index c40fb48..2e0c2a6 100644 --- a/Modules/Dock/Dock.qml +++ b/Modules/Dock/Dock.qml @@ -9,12 +9,12 @@ import qs.Services import qs.Widgets NLoader { - isLoaded: Settings.data.general.showDock - content: Component { - Variants { - model: Quickshell.screens + isLoaded: Settings.data.general.showDock + content: Component { + Variants { + model: Quickshell.screens - Item { + Item { property var modelData readonly property real scaling: Scaling.scale(modelData) @@ -38,308 +38,321 @@ NLoader { property var contextMenuToplevel: null PanelWindow { - id: dockWindow - visible: true - screen: modelData + id: dockWindow + visible: true + screen: modelData + exclusionMode: ExclusionMode.Ignore + anchors.bottom: true + anchors.left: true + anchors.right: true + focusable: false + color: "transparent" + implicitHeight: 60 + + // Timer for auto-hide delay + Timer { + id: hideTimer + interval: hideDelay + onTriggered: if (autoHide && !dockHovered && !anyAppHovered) + hidden = true + } + + // Timer for show delay + Timer { + id: showTimer + interval: showDelay + onTriggered: hidden = false + } + + // Behavior for smooth hide/show animations + Behavior on margins.bottom { + NumberAnimation { + duration: hidden ? hideAnimationDuration : showAnimationDuration + easing.type: Easing.InOutQuad + } + } + + // Mouse area at screen bottom to detect entry and keep dock visible + MouseArea { + id: screenEdgeMouseArea + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: 10 + hoverEnabled: true + propagateComposedEvents: true + + onEntered: if (autoHide && hidden) + showTimer.start() + onExited: if (autoHide && !hidden && !dockHovered && !anyAppHovered) + hideTimer.start() + } + + margins.bottom: hidden ? -(fullHeight - peekHeight) : 0 + + MouseArea { + anchors.fill: parent + enabled: contextMenuVisible + onClicked: { + contextMenuVisible = false + contextMenuTarget = null + contextMenuToplevel = null + } + } + + Rectangle { + id: dockContainer + width: dock.width + 40 + height: 50 + color: Colors.backgroundSecondary + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + topLeftRadius: 20 + topRightRadius: 20 + + MouseArea { + id: dockMouseArea + anchors.fill: parent + hoverEnabled: true + propagateComposedEvents: true + + onEntered: { + dockHovered = true + if (autoHide) { + showTimer.stop() + hideTimer.stop() + hidden = false + } + } + onExited: { + dockHovered = false + if (autoHide && !anyAppHovered && !contextMenuVisible) + hideTimer.start() + } + } + + Item { + id: dock + width: runningAppsRow.width + height: parent.height - 10 + anchors.centerIn: parent + + NTooltip { + id: appTooltip + visible: false + positionAbove: true + } + + function getAppIcon(toplevel: Toplevel): string { + if (!toplevel) + return "" + let icon = Quickshell.iconPath(toplevel.appId?.toLowerCase(), true) + if (!icon) + icon = Quickshell.iconPath(toplevel.appId, true) + if (!icon) + icon = Quickshell.iconPath(toplevel.title?.toLowerCase(), true) + if (!icon) + icon = Quickshell.iconPath(toplevel.title, true) + return icon || Quickshell.iconPath("application-x-executable", true) + } + + Row { + id: runningAppsRow + spacing: 8 + height: parent.height + anchors.centerIn: parent + + Repeater { + model: ToplevelManager ? ToplevelManager.toplevels : null + + delegate: Rectangle { + id: appButton + width: 36 + height: 36 + radius: 18 + color: "transparent" + + property bool isActive: ToplevelManager.activeToplevel + && ToplevelManager.activeToplevel === modelData + property bool hovered: appMouseArea.containsMouse + property string appId: modelData ? modelData.appId : "" + property string appTitle: modelData ? modelData.title : "" + + Behavior on color { + ColorAnimation { + duration: 150 + } + } + + Image { + id: appIcon + width: 28 + height: 28 + anchors.centerIn: parent + source: dock.getAppIcon(modelData) + visible: source.toString() !== "" + smooth: false + mipmap: false + antialiasing: false + fillMode: Image.PreserveAspectFit + } + + Text { + anchors.centerIn: parent + visible: !appIcon.visible + text: appButton.appId ? appButton.appId.charAt(0).toUpperCase() : "?" + font.pixelSize: 14 + font.bold: true + color: appButton.isActive ? Colors.accentPrimary : Colors.textPrimary + } + + MouseArea { + id: appMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + + onEntered: { + anyAppHovered = true + const appName = appButton.appTitle || appButton.appId || "Unknown" + appTooltip.text = appName.length > 40 ? appName.substring(0, 37) + "..." : appName + appTooltip.target = appButton + appTooltip.isVisible = true + if (autoHide) { + showTimer.stop() + hideTimer.stop() + hidden = false + } + } + + onExited: { + anyAppHovered = false + appTooltip.hide() + if (autoHide && !dockHovered && !contextMenuVisible) + hideTimer.start() + } + + onClicked: function (mouse) { + if (mouse.button === Qt.MiddleButton && modelData?.close) { + modelData.close() + } + if (mouse.button === Qt.LeftButton && modelData?.activate) { + modelData.activate() + } + if (mouse.button === Qt.RightButton) { + appTooltip.hide() + contextMenuTarget = appButton + contextMenuToplevel = modelData + contextMenuVisible = true + } + } + } + + Rectangle { + visible: isActive + width: 20 + height: 3 + color: Colors.accentPrimary + radius: 1.5 + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottomMargin: 2 + } + } + } + } + } + } + + // Context Menu + PanelWindow { + id: contextMenuWindow + visible: contextMenuVisible + screen: dockWindow.screen exclusionMode: ExclusionMode.Ignore anchors.bottom: true anchors.left: true anchors.right: true - focusable: false color: "transparent" - implicitHeight: 60 - - // Timer for auto-hide delay - Timer { - id: hideTimer - interval: hideDelay - onTriggered: if (autoHide && !dockHovered && !anyAppHovered) hidden = true - } - - // Timer for show delay - Timer { - id: showTimer - interval: showDelay - onTriggered: hidden = false - } - - - - // Behavior for smooth hide/show animations - Behavior on margins.bottom { - NumberAnimation { - duration: hidden ? hideAnimationDuration : showAnimationDuration - easing.type: Easing.InOutQuad - } - } - - // Mouse area at screen bottom to detect entry and keep dock visible - MouseArea { - id: screenEdgeMouseArea - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - height: 10 - hoverEnabled: true - propagateComposedEvents: true - - onEntered: if (autoHide && hidden) showTimer.start() - onExited: if (autoHide && !hidden && !dockHovered && !anyAppHovered) hideTimer.start() - } - - margins.bottom: hidden ? -(fullHeight - peekHeight) : 0 + focusable: false MouseArea { - anchors.fill: parent - enabled: contextMenuVisible - onClicked: { - contextMenuVisible = false - contextMenuTarget = null - contextMenuToplevel = null - } + anchors.fill: parent + onClicked: { + contextMenuVisible = false + contextMenuTarget = null + contextMenuToplevel = null + hidden = true // Hide dock when context menu closes + } } Rectangle { - id: dockContainer - width: dock.width + 40 - height: 50 - color: Colors.backgroundSecondary - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: parent.bottom - topLeftRadius: 20 - topRightRadius: 20 + id: contextMenuContainer + width: 80 + height: 32 + radius: 8 + color: closeMouseArea.containsMouse ? Colors.hover : Colors.backgroundPrimary + border.color: Colors.outline + border.width: 1 - MouseArea { - id: dockMouseArea - anchors.fill: parent - hoverEnabled: true - propagateComposedEvents: true + x: { + if (!contextMenuTarget) + return 0 + const pos = contextMenuTarget.mapToItem(null, 0, 0) + let xPos = pos.x + (contextMenuTarget.width - width) / 2 + return Math.max(0, Math.min(xPos, dockWindow.width - width)) + } - onEntered: { - dockHovered = true - if (autoHide) { - showTimer.stop() - hideTimer.stop() - hidden = false - } - } - onExited: { - dockHovered = false - if (autoHide && !anyAppHovered && !contextMenuVisible) hideTimer.start() - } + y: { + if (!contextMenuTarget) + return 0 + const pos = contextMenuTarget.mapToItem(null, 0, 0) + return pos.y - height + 32 + } + + Text { + anchors.centerIn: parent + text: "Close" + font.pixelSize: 14 + color: Colors.textPrimary + } + + MouseArea { + id: closeMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + if (contextMenuToplevel?.close) + contextMenuToplevel.close() + contextMenuVisible = false + hidden = true } + } - Item { - id: dock - width: runningAppsRow.width - height: parent.height - 10 - anchors.centerIn: parent - - NTooltip { - id: appTooltip - visible: false - positionAbove: true - } - - function getAppIcon(toplevel: Toplevel): string { - if (!toplevel) return ""; - let icon = Quickshell.iconPath(toplevel.appId?.toLowerCase(), true); - if (!icon) icon = Quickshell.iconPath(toplevel.appId, true); - if (!icon) icon = Quickshell.iconPath(toplevel.title?.toLowerCase(), true); - if (!icon) icon = Quickshell.iconPath(toplevel.title, true); - return icon || Quickshell.iconPath("application-x-executable", true); - } - - Row { - id: runningAppsRow - spacing: 8 - height: parent.height - anchors.centerIn: parent - - Repeater { - model: ToplevelManager ? ToplevelManager.toplevels : null - - delegate: Rectangle { - id: appButton - width: 36 - height: 36 - radius: 18 - color:"transparent" - - property bool isActive: ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData - property bool hovered: appMouseArea.containsMouse - property string appId: modelData ? modelData.appId : "" - property string appTitle: modelData ? modelData.title : "" - - Behavior on color { ColorAnimation { duration: 150 } } - - Image { - id: appIcon - width: 28 - height: 28 - anchors.centerIn: parent - source: dock.getAppIcon(modelData) - visible: source.toString() !== "" - smooth: false - mipmap: false - antialiasing: false - fillMode: Image.PreserveAspectFit - } - - Text { - anchors.centerIn: parent - visible: !appIcon.visible - text: appButton.appId ? appButton.appId.charAt(0).toUpperCase() : "?" - font.pixelSize: 14 - font.bold: true - color: appButton.isActive ? Colors.accentPrimary : Colors.textPrimary - } - - MouseArea { - id: appMouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - - onEntered: { - anyAppHovered = true - const appName = appButton.appTitle || appButton.appId || "Unknown" - appTooltip.text = appName.length > 40 ? appName.substring(0, 37) + "..." : appName - appTooltip.target = appButton - appTooltip.isVisible = true - if (autoHide) { - showTimer.stop() - hideTimer.stop() - hidden = false - } - } - - onExited: { - anyAppHovered = false - appTooltip.hide() - if (autoHide && !dockHovered && !contextMenuVisible) hideTimer.start() - } - - onClicked: function(mouse) { - if (mouse.button === Qt.MiddleButton && modelData?.close) { - modelData.close() - } - if (mouse.button === Qt.LeftButton && modelData?.activate) { - modelData.activate() - } - if (mouse.button === Qt.RightButton) { - appTooltip.hide() - contextMenuTarget = appButton - contextMenuToplevel = modelData - contextMenuVisible = true - } - } - } - - Rectangle { - visible: isActive - width: 20 - height: 3 - color: Colors.accentPrimary - radius: 1.5 - anchors.bottom: parent.bottom - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottomMargin: 2 - } - } - } - } - + // Animation + scale: contextMenuVisible ? 1 : 0.9 + opacity: contextMenuVisible ? 1 : 0 + transformOrigin: Item.Bottom + Behavior on scale { + NumberAnimation { + duration: 150 + easing.type: Easing.OutBack } + } + + Behavior on opacity { + NumberAnimation { + duration: 100 + } + } } - - // Context Menu - PanelWindow { - id: contextMenuWindow - visible: contextMenuVisible - screen: dockWindow.screen - exclusionMode: ExclusionMode.Ignore - anchors.bottom: true - anchors.left: true - anchors.right: true - color: "transparent" - focusable: false - - MouseArea { - anchors.fill: parent - onClicked: { - contextMenuVisible = false - contextMenuTarget = null - contextMenuToplevel = null - hidden = true // Hide dock when context menu closes - } - } - - - - Rectangle { - id: contextMenuContainer - width: 80 - height: 32 - radius: 8 - color: closeMouseArea.containsMouse ? Colors.hover : Colors.backgroundPrimary - border.color: Colors.outline - border.width: 1 - - x: { - if (!contextMenuTarget) return 0 - const pos = contextMenuTarget.mapToItem(null, 0, 0) - let xPos = pos.x + (contextMenuTarget.width - width) / 2 - return Math.max(0, Math.min(xPos, dockWindow.width - width)) - } - - y: { - if (!contextMenuTarget) return 0 - const pos = contextMenuTarget.mapToItem(null, 0, 0) - return pos.y - height + 32 - } - - Text { - anchors.centerIn: parent - text: "Close" - font.pixelSize: 14 - color: Colors.textPrimary - } - - MouseArea { - id: closeMouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - - onClicked: { - if (contextMenuToplevel?.close) contextMenuToplevel.close() - contextMenuVisible = false - hidden = true - } - } - - // Animation - scale: contextMenuVisible ? 1 : 0.9 - opacity: contextMenuVisible ? 1 : 0 - transformOrigin: Item.Bottom - - Behavior on scale { - NumberAnimation { - duration: 150 - easing.type: Easing.OutBack - } - } - - Behavior on opacity { - NumberAnimation { duration: 100 } - } - } - } + } } + } } + } } -} -} diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index 31f2fc9..0adb9e0 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -540,7 +540,7 @@ WlSessionLock { Text { anchors.centerIn: parent - text: lock.authenticating ? "EXECUTING..." : "EXECUTE" + text: lock.authenticating ? "EXECUTING" : "EXECUTE" color: executeButtonArea.containsMouse ? Colors.onAccent : Colors.accentPrimary font.family: "DejaVu Sans Mono" font.pointSize: Style.fontSizeMedium diff --git a/Modules/Settings/Tabs/About.qml b/Modules/Settings/Tabs/About.qml index 258c7e2..bafd0d0 100644 --- a/Modules/Settings/Tabs/About.qml +++ b/Modules/Settings/Tabs/About.qml @@ -236,7 +236,8 @@ ColumnLayout { } NText { - text: (modelData.contributions || 0) + " " + ((modelData.contributions || 0) === 1 ? "commit" : "commits") + text: (modelData.contributions || 0) + " " + ((modelData.contributions + || 0) === 1 ? "commit" : "commits") font.pointSize: Style.fontSizeSmall * scaling color: contributorArea.containsMouse ? Colors.backgroundPrimary : Colors.textSecondary } diff --git a/Services/Clipboard.qml b/Services/Clipboard.qml index 0d82e37..c66b3ce 100644 --- a/Services/Clipboard.qml +++ b/Services/Clipboard.qml @@ -6,134 +6,133 @@ import Quickshell.Io import qs.Services Singleton { - id: root + id: root - property var history: [] - property bool initialized: false + property var history: [] + property bool initialized: false - // Internal state - property bool _enabled: true + // Internal state + property bool _enabled: true - Timer { - interval: 1000 - repeat: true - running: root._enabled - onTriggered: root.refresh() - } + Timer { + interval: 1000 + repeat: true + running: root._enabled + onTriggered: root.refresh() + } - // Detect current clipboard types (text/image) - Process { - id: typeProcess - property bool isLoading: false - property var currentTypes: [] + // Detect current clipboard types (text/image) + Process { + id: typeProcess + property bool isLoading: false + property var currentTypes: [] - onExited: (exitCode, exitStatus) => { - if (exitCode === 0) { - currentTypes = String(stdout.text).trim().split('\n').filter(t => t) + onExited: (exitCode, exitStatus) => { + if (exitCode === 0) { + currentTypes = String(stdout.text).trim().split('\n').filter(t => t) - const imageType = currentTypes.find(t => t.startsWith('image/')) - if (imageType) { - imageProcess.mimeType = imageType - imageProcess.command = ["sh", "-c", `wl-paste -n -t "${imageType}" | base64 -w 0`] - imageProcess.running = true - } else { - textProcess.command = ["wl-paste", "-n", "--type", "text/plain"] - textProcess.running = true - } - } else { - typeProcess.isLoading = false - } + const imageType = currentTypes.find(t => t.startsWith('image/')) + if (imageType) { + imageProcess.mimeType = imageType + imageProcess.command = ["sh", "-c", `wl-paste -n -t "${imageType}" | base64 -w 0`] + imageProcess.running = true + } else { + textProcess.command = ["wl-paste", "-n", "--type", "text/plain"] + textProcess.running = true } - - stdout: StdioCollector {} + } else { + typeProcess.isLoading = false + } } - // Read image data - Process { - id: imageProcess - property string mimeType: "" + stdout: StdioCollector {} + } - onExited: (exitCode, exitStatus) => { - if (exitCode === 0) { - const base64 = stdout.text.trim() - if (base64) { - const entry = { - type: 'image', - mimeType: mimeType, - data: `data:${mimeType};base64,${base64}`, - timestamp: new Date().getTime() - } + // Read image data + Process { + id: imageProcess + property string mimeType: "" - const exists = root.history.find(item => item.type === 'image' && item.data === entry.data) - if (!exists) { - root.history = [entry, ...root.history].slice(0, 20) - } - } - } + onExited: (exitCode, exitStatus) => { + if (exitCode === 0) { + const base64 = stdout.text.trim() + if (base64) { + const entry = { + "type": 'image', + "mimeType": mimeType, + "data": `data:${mimeType};base64,${base64}`, + "timestamp": new Date().getTime() + } - if (!textProcess.isLoading) { - root.initialized = true - } - typeProcess.isLoading = false + const exists = root.history.find(item => item.type === 'image' && item.data === entry.data) + if (!exists) { + root.history = [entry, ...root.history].slice(0, 20) + } } + } - stdout: StdioCollector {} + if (!textProcess.isLoading) { + root.initialized = true + } + typeProcess.isLoading = false } - // Read text data - Process { - id: textProcess - property bool isLoading: false + stdout: StdioCollector {} + } - onExited: (exitCode, exitStatus) => { - if (exitCode === 0) { - const content = String(stdout.text).trim() - if (content) { - const entry = { - type: 'text', - content: content, - timestamp: new Date().getTime() - } + // Read text data + Process { + id: textProcess + property bool isLoading: false - const exists = root.history.find(item => { - if (item.type === 'text') { - return item.content === content - } - return item === content - }) + onExited: (exitCode, exitStatus) => { + if (exitCode === 0) { + const content = String(stdout.text).trim() + if (content) { + const entry = { + "type": 'text', + "content": content, + "timestamp": new Date().getTime() + } - if (!exists) { - const newHistory = root.history.map(item => { - if (typeof item === 'string') { - return { - type: 'text', - content: item, - timestamp: new Date().getTime() - } - } - return item - }) + const exists = root.history.find(item => { + if (item.type === 'text') { + return item.content === content + } + return item === content + }) - root.history = [entry, ...newHistory].slice(0, 20) - } - } - } else { - textProcess.isLoading = false - } + if (!exists) { + const newHistory = root.history.map(item => { + if (typeof item === 'string') { + return { + "type": 'text', + "content": item, + "timestamp": new Date().getTime() + } + } + return item + }) - root.initialized = true - typeProcess.isLoading = false + root.history = [entry, ...newHistory].slice(0, 20) + } } + } else { + textProcess.isLoading = false + } - stdout: StdioCollector {} + root.initialized = true + typeProcess.isLoading = false } - function refresh() { - if (!typeProcess.isLoading && !textProcess.isLoading) { - typeProcess.isLoading = true - typeProcess.command = ["wl-paste", "-l"] - typeProcess.running = true - } + stdout: StdioCollector {} + } + + function refresh() { + if (!typeProcess.isLoading && !textProcess.isLoading) { + typeProcess.isLoading = true + typeProcess.command = ["wl-paste", "-l"] + typeProcess.running = true } + } } - diff --git a/Services/GitHub.qml b/Services/GitHub.qml index 1b74f4d..6404578 100644 --- a/Services/GitHub.qml +++ b/Services/GitHub.qml @@ -82,10 +82,10 @@ Singleton { data.timestamp = Time.timestamp console.log("[GitHub] Saving data to cache file:", githubDataFile) console.log("[GitHub] Data to save - version:", data.version, "contributors:", data.contributors.length) - + // Ensure cache directory exists Quickshell.execDetached(["mkdir", "-p", Settings.cacheDir]) - + Qt.callLater(() => { // Use direct ID reference to the FileView githubDataFileView.writeAdapter() @@ -147,7 +147,8 @@ Singleton { console.log("[GitHub] Raw contributors response length:", response ? response.length : 0) if (response && response.trim()) { const data = JSON.parse(response) - console.log("[GitHub] Parsed contributors data type:", typeof data, "length:", Array.isArray(data) ? data.length : "not array") + console.log("[GitHub] Parsed contributors data type:", typeof data, "length:", + Array.isArray(data) ? data.length : "not array") root.data.contributors = data || [] root.contributors = root.data.contributors console.log("[GitHub] Contributors fetched from GitHub:", root.contributors.length) diff --git a/Services/Wallpapers.qml b/Services/Wallpapers.qml index 300db58..0b3201f 100644 --- a/Services/Wallpapers.qml +++ b/Services/Wallpapers.qml @@ -160,6 +160,7 @@ Singleton { running: false stdout: StdioCollector { onStreamFinished: { + // console.log(this.text) } } From 7fced5df95c8a5cbe47bd716d2c37995489761c6 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Thu, 14 Aug 2025 14:58:18 -0400 Subject: [PATCH 277/394] LockScreen: moved instance from IPCManager.qml to shell --- Services/IPCManager.qml | 6 ------ shell.qml | 17 ++++++++++------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/Services/IPCManager.qml b/Services/IPCManager.qml index 998635b..07f195b 100644 --- a/Services/IPCManager.qml +++ b/Services/IPCManager.qml @@ -1,6 +1,5 @@ import QtQuick import Quickshell.Io -import qs.Modules.LockScreen Item { id: root @@ -49,9 +48,4 @@ Item { lockScreen.locked = !lockScreen.locked } } - - // LockScreen instance - LockScreen { - id: lockScreen - } } diff --git a/shell.qml b/shell.qml index b68c05f..5b52447 100644 --- a/shell.qml +++ b/shell.qml @@ -2,17 +2,18 @@ import QtQuick import Quickshell import Quickshell.Io -import Quickshell.Widgets import Quickshell.Services.Pipewire +import Quickshell.Widgets +import qs.Modules.AppLauncher +import qs.Modules.Background import qs.Modules.Bar -import qs.Modules.Dock import qs.Modules.Calendar import qs.Modules.Demo -import qs.Modules.Background -import qs.Modules.SidePanel -import qs.Modules.AppLauncher +import qs.Modules.Dock +import qs.Modules.LockScreen import qs.Modules.Notification import qs.Modules.Settings +import qs.Modules.SidePanel import qs.Services import qs.Widgets @@ -29,8 +30,6 @@ ShellRoot { id: appLauncherPanel } - - DemoPanel { id: demoPanel } @@ -55,6 +54,10 @@ ShellRoot { id: notificationHistoryPanel } + LockScreen { + id: lockScreen + } + IPCManager {} Component.onCompleted: { From 73c7ba8cdcba7928fdd94ae0d28fb7f5a23ab81e Mon Sep 17 00:00:00 2001 From: quadbyte Date: Thu, 14 Aug 2025 18:19:02 -0400 Subject: [PATCH 278/394] Switched to Material3 colors principle - works with matugen only for now - need to restore rosepine --- Assets/Matugen/matugen.toml | 25 +- Assets/Matugen/templates/noctalia.json | 51 ++-- Bin/matugen-theme.sh | 62 ----- Modules/AppLauncher/AppLauncher.qml | 48 ++-- Modules/Audio/CircularSpectrum.qml | 4 +- Modules/Audio/LinearSpectrum.qml | 4 +- Modules/Background/Overview.qml | 2 +- Modules/Background/ScreenCorners.qml | 2 +- Modules/Bar/Bar.qml | 2 +- Modules/Bar/Battery.qml | 8 +- Modules/Bar/SystemMonitor.qml | 6 +- Modules/Bar/TrayMenu.qml | 18 +- Modules/Bar/Volume.qml | 4 +- Modules/Bar/WiFiMenu.qml | 40 +-- Modules/Bar/Workspace.qml | 24 +- Modules/Calendar/Calendar.qml | 12 +- Modules/Demo/DemoPanel.qml | 20 +- Modules/Dock/Dock.qml | 12 +- Modules/LockScreen/LockScreen.qml | 70 ++--- Modules/Notification/Notification.qml | 23 +- .../Notification/NotificationHistoryPanel.qml | 16 +- Modules/Settings/SettingsPanel.qml | 26 +- Modules/Settings/Tabs/About.qml | 30 +-- Modules/Settings/Tabs/Audio.qml | 20 +- Modules/Settings/Tabs/Bar.qml | 2 +- Modules/Settings/Tabs/Display.qml | 10 +- Modules/Settings/Tabs/General.qml | 6 +- Modules/Settings/Tabs/Misc.qml | 2 +- Modules/Settings/Tabs/Network.qml | 2 +- Modules/Settings/Tabs/ScreenRecorder.qml | 6 +- Modules/Settings/Tabs/TimeWeather.qml | 6 +- Modules/Settings/Tabs/Wallpaper.qml | 24 +- Modules/Settings/Tabs/WallpaperSelector.qml | 36 +-- Modules/SidePanel/Cards/MediaCard.qml | 46 ++-- Modules/SidePanel/Cards/ProfileCard.qml | 4 +- Modules/SidePanel/Cards/WeatherCard.qml | 8 +- Modules/SidePanel/PowerMenu.qml | 36 +-- Modules/SidePanel/SidePanel.qml | 15 +- Services/Colors.qml | 241 +++++++++++------- Services/Settings.qml | 1 - Services/Wallpapers.qml | 5 +- Widgets/NBox.qml | 4 +- Widgets/NBusyIndicator.qml | 2 +- Widgets/NCard.qml | 4 +- Widgets/NCircleStat.qml | 16 +- Widgets/NComboBox.qml | 16 +- Widgets/NDivider.qml | 2 +- Widgets/NIconButton.qml | 6 +- Widgets/NPill.qml | 10 +- Widgets/NRadioButton.qml | 4 +- Widgets/NSlider.qml | 14 +- Widgets/NText.qml | 2 +- Widgets/NTextInput.qml | 14 +- Widgets/NToggle.qml | 16 +- Widgets/NTooltip.qml | 13 +- 55 files changed, 519 insertions(+), 583 deletions(-) delete mode 100755 Bin/matugen-theme.sh diff --git a/Assets/Matugen/matugen.toml b/Assets/Matugen/matugen.toml index ad2bf57..bd165cd 100644 --- a/Assets/Matugen/matugen.toml +++ b/Assets/Matugen/matugen.toml @@ -1,28 +1,7 @@ -# matugen configuration for Noctalia -# This file configures how matugen generates colors from wallpapers - +# This file configures how matugen generates colors from wallpapers for Noctalia [config] -# Color scheme type for generation -scheme = "dark" -# Color space for color extraction -color_space = "oklch" - -# Algorithm for color extraction -algorithm = "kmeans" - -# Number of colors to extract -color_count = 16 - -# Use source image colors -use_source_colors = true - -# Generate dark theme variant -generate_dark = true - -# Generate light theme variant -generate_light = false [templates.noctalia] input_path = "templates/noctalia.json" -output_path = "~/.config/noctalia/theme.json" \ No newline at end of file +output_path = "~/.config/noctalia/colors.json" \ No newline at end of file diff --git a/Assets/Matugen/templates/noctalia.json b/Assets/Matugen/templates/noctalia.json index 35e857f..9044c0d 100644 --- a/Assets/Matugen/templates/noctalia.json +++ b/Assets/Matugen/templates/noctalia.json @@ -1,26 +1,31 @@ { - "backgroundPrimary": "{{colors.surface_dim.default.hex}}", - "backgroundSecondary": "{{colors.surface.default.hex}}", - "backgroundTertiary": "{{colors.surface_bright.default.hex}}", + "colorPrimary": "{{colors.primary.default.hex}}", + "colorOnPrimary": "{{colors.on_primary.default.hex}}", + "colorPrimaryContainer": "{{colors.primary_container.default.hex}}", + "colorOnPrimaryContainer": "{{colors.on_primary_container.default.hex}}", + + "colorSecondary": "{{colors.secondary.default.hex}}", + "colorOnSecondary": "{{colors.on_secondary.default.hex}}", + "colorSecondaryContainer": "{{colors.secondary_container.default.hex}}", + "colorOnSecondaryContainer": "{{colors.on_secondary_container.default.hex}}", + + "colorTertiary": "{{colors.tertiary.default.hex}}", + "colorOnTertiary": "{{colors.on_tertiary.default.hex}}", + "colorTertiaryContainer": "{{colors.tertiary_container.default.hex}}", + "colorOnTertiaryContainer": "{{colors.on_tertiary_container.default.hex}}", + + "colorError": "{{colors.error.default.hex}}", + "colorOnError": "{{colors.on_error.default.hex}}", + "colorErrorContainer": "{{colors.error_container.default.hex}}", + "colorOnErrorContainer": "{{colors.on_error_container.default.hex}}", + + "colorSurface": "{{colors.surface.default.hex}}", + "colorOnSurface": "{{colors.on_surface.default.hex}}", + "colorSurfaceVariant": "{{colors.surface_variant.default.hex}}", + "colorOnSurfaceVariant": "{{colors.on_surface_variant.default.hex}}", - "surface": "{{colors.surface.default.hex}}", - "surfaceVariant": "{{colors.surface_variant.default.hex}}", - - "textPrimary": "{{colors.on_surface.default.hex}}", - "textSecondary": "{{colors.on_surface_variant.default.hex}}", - "textDisabled": "{{colors.on_surface_variant.default.hex}}", - - "accentPrimary": "{{colors.primary.default.hex}}", - "accentSecondary": "{{colors.secondary.default.hex}}", - "accentTertiary": "{{colors.tertiary.default.hex}}", - - "error": "{{colors.error.default.hex}}", - "warning": "{{colors.error_container.default.hex}}", - - "hover": "{{colors.primary_container.default.hex}}", - "onAccent": "{{colors.on_primary.default.hex}}", - "outline": "{{colors.outline.default.hex}}", - - "shadow": "{{colors.shadow.default.hex}}", - "overlay": "{{colors.scrim.default.hex}}" + "colorInversePrimary": "{{colors.inverse_primary.default.hex}}", + "colorOutline": "{{colors.outline.default.hex}}", + "colorOutlineVariant": "{{colors.outline_variant.default.hex}}", + "colorShadow": "{{colors.shadow.default.hex}}" } \ No newline at end of file diff --git a/Bin/matugen-theme.sh b/Bin/matugen-theme.sh deleted file mode 100755 index 73c18ca..0000000 --- a/Bin/matugen-theme.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/bin/bash - -# matugen-theme.sh - Generate theme colors from wallpaper using matugen -# Usage: ./matugen-theme.sh - -set -e - -# Configuration -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_DIR="$(dirname "$SCRIPT_DIR")" -CONFIG_FILE="$PROJECT_DIR/Assets/Matugen/matugen.toml" -TEMPLATE_FILE="$PROJECT_DIR/Assets/Matugen/templates/noctalia.json" -OUTPUT_DIR="$HOME/.config/noctalia" -OUTPUT_FILE="$OUTPUT_DIR/theme.json" - -# Check if wallpaper path is provided -if [ $# -eq 0 ]; then - echo "Error: No wallpaper path provided" - echo "Usage: $0 " - exit 1 -fi - -WALLPAPER_PATH="$1" - -# Check if wallpaper exists -if [ ! -f "$WALLPAPER_PATH" ]; then - echo "Error: Wallpaper file not found: $WALLPAPER_PATH" - exit 1 -fi - -# Create output directory if it doesn't exist -mkdir -p "$OUTPUT_DIR" - -# Generate theme using matugen -echo "Generating theme from wallpaper: $WALLPAPER_PATH" - -# Use matugen to generate colors and transform to our format -matugen image "$WALLPAPER_PATH" \ - --config "$CONFIG_FILE" \ - --json hex | jq -c ' -{ - "backgroundPrimary": .colors.dark.surface_dim, - "backgroundSecondary": .colors.dark.surface, - "backgroundTertiary": .colors.dark.surface_bright, - "surface": .colors.dark.surface, - "surfaceVariant": .colors.dark.surface_variant, - "textPrimary": .colors.dark.on_surface, - "textSecondary": .colors.dark.on_surface_variant, - "textDisabled": .colors.dark.on_surface_variant, - "accentPrimary": .colors.dark.primary, - "accentSecondary": .colors.dark.secondary, - "accentTertiary": .colors.dark.tertiary, - "error": .colors.dark.error, - "warning": .colors.dark.error_container, - "hover": .colors.dark.primary_container, - "onAccent": .colors.dark.on_primary, - "outline": .colors.dark.outline, - "shadow": .colors.dark.shadow, - "overlay": .colors.dark.scrim -}' > "$OUTPUT_FILE.tmp" && mv "$OUTPUT_FILE.tmp" "$OUTPUT_FILE" - -echo "Theme generated successfully: $OUTPUT_FILE" \ No newline at end of file diff --git a/Modules/AppLauncher/AppLauncher.qml b/Modules/AppLauncher/AppLauncher.qml index 3dc07d5..9120e4d 100644 --- a/Modules/AppLauncher/AppLauncher.qml +++ b/Modules/AppLauncher/AppLauncher.qml @@ -254,19 +254,19 @@ NLoader { width: Math.min(700 * scaling, parent.width * 0.75) height: Math.min(550 * scaling, parent.height * 0.8) radius: 32 * scaling - color: Colors.backgroundPrimary - border.color: Colors.outline + color: Colors.colorSurface + border.color: Colors.colorOutline border.width: Style.borderThin * scaling // Subtle gradient background gradient: Gradient { GradientStop { position: 0.0 - color: Qt.lighter(Colors.backgroundPrimary, 1.02) + color: Qt.lighter(Colors.colorSurface, 1.02) } GradientStop { position: 1.0 - color: Qt.darker(Colors.backgroundPrimary, 1.1) + color: Qt.darker(Colors.colorSurface, 1.1) } } @@ -281,8 +281,8 @@ NLoader { Layout.preferredHeight: 40 * scaling Layout.bottomMargin: Style.marginMedium * scaling radius: 20 * scaling - color: Colors.backgroundSecondary - border.color: searchInput.activeFocus ? Colors.accentPrimary : Colors.outline + color: Colors.colorSurface + border.color: searchInput.activeFocus ? Colors.colorPrimary : Colors.colorOutline border.width: searchInput.activeFocus ? 2 : 1 Row { @@ -294,14 +294,14 @@ NLoader { text: "search" font.family: "Material Symbols Outlined" font.pointSize: 16 * scaling - color: searchInput.activeFocus ? Colors.accentPrimary : Colors.textSecondary + color: searchInput.activeFocus ? Colors.colorPrimary : Colors.colorOnSurface } TextField { id: searchInput placeholderText: "Search applications..." - color: Colors.textPrimary - placeholderTextColor: Colors.textSecondary + color: Colors.colorOnSurface + placeholderTextColor: Colors.colorOnSurface background: null font.pointSize: 13 * scaling Layout.fillWidth: true @@ -309,8 +309,8 @@ NLoader { searchText = text selectedIndex = 0 // Reset selection when search changes } - selectedTextColor: Colors.textPrimary - selectionColor: Colors.accentPrimary + selectedTextColor: Colors.colorOnSurface + selectionColor: Colors.colorPrimary padding: 0 verticalAlignment: TextInput.AlignVCenter leftPadding: 0 @@ -319,14 +319,14 @@ NLoader { bottomPadding: 0 font.bold: true Component.onCompleted: { - contentItem.cursorColor = Colors.textPrimary + contentItem.cursorColor = Colors.colorOnSurface contentItem.verticalAlignment = TextInput.AlignVCenter // Focus the search bar by default Qt.callLater(() => { searchInput.forceActiveFocus() }) } - onActiveFocusChanged: contentItem.cursorColor = Colors.textPrimary + onActiveFocusChanged: contentItem.cursorColor = Colors.colorOnSurface Keys.onDownPressed: selectNext() Keys.onUpPressed: selectPrev() @@ -370,10 +370,10 @@ NLoader { radius: 16 * scaling property bool isSelected: index === selectedIndex color: (appCardArea.containsMouse || isSelected) ? Qt.darker( - Colors.accentPrimary, - 1.1) : Colors.backgroundSecondary + Colors.colorPrimary, + 1.1) : Colors.colorSurface border.color: (appCardArea.containsMouse - || isSelected) ? Colors.accentPrimary : "transparent" + || isSelected) ? Colors.colorPrimary : "transparent" border.width: (appCardArea.containsMouse || isSelected) ? 2 : 0 Behavior on color { @@ -404,8 +404,8 @@ NLoader { Layout.preferredWidth: 40 * scaling Layout.preferredHeight: 40 * scaling radius: 14 * scaling - color: appCardArea.containsMouse ? Qt.darker(Colors.accentPrimary, - 1.1) : Colors.backgroundTertiary + color: appCardArea.containsMouse ? Qt.darker(Colors.colorPrimary, + 1.1) : Colors.colorSurfaceVariant property bool iconLoaded: (modelData.isCalculator || modelData.isClipboard || modelData.isCommand) || (iconImg.status === Image.Ready && iconImg.source !== "" @@ -440,7 +440,7 @@ NLoader { anchors.fill: parent anchors.margins: 6 * scaling radius: 10 * scaling - color: Colors.accentPrimary + color: Colors.colorPrimary opacity: 0.3 visible: !parent.iconLoaded } @@ -452,7 +452,7 @@ NLoader { text: modelData.name ? modelData.name.charAt(0).toUpperCase() : "?" font.pointSize: 18 * scaling font.weight: Font.Bold - color: Colors.accentPrimary + color: Colors.colorPrimary } Behavior on color { @@ -471,7 +471,7 @@ NLoader { text: modelData.name || "Unknown" font.pointSize: 14 * scaling font.weight: Font.Bold - color: Colors.textPrimary + color: Colors.colorOnSurface elide: Text.ElideRight Layout.fillWidth: true } @@ -480,7 +480,7 @@ NLoader { text: modelData.isCalculator ? (modelData.expr + " = " + modelData.result) : modelData.isClipboard ? modelData.content : modelData.isCommand ? modelData.content : (modelData.genericName || modelData.comment || "") font.pointSize: 11 * scaling color: (appCardArea.containsMouse - || isSelected) ? Colors.textPrimary : Colors.textSecondary + || isSelected) ? Colors.colorOnSurface : Colors.colorOnSurface elide: Text.ElideRight Layout.fillWidth: true visible: text !== "" @@ -507,7 +507,7 @@ NLoader { NText { text: searchText.trim() !== "" ? "No applications found" : "No applications available" font.pointSize: Style.fontSizeLarge * scaling - color: Colors.textSecondary + color: Colors.colorOnSurface horizontalAlignment: Text.AlignHCenter Layout.fillWidth: true visible: filteredEntries.length === 0 @@ -521,7 +521,7 @@ NLoader { ">calc") ? `${filteredEntries.length} result${filteredEntries.length !== 1 ? 's' : ''}` : `${filteredEntries.length} application${filteredEntries.length !== 1 ? 's' : ''}` font.pointSize: Style.fontSizeSmall * scaling - color: Colors.textSecondary + color: Colors.colorOnSurface horizontalAlignment: Text.AlignHCenter Layout.fillWidth: true visible: searchText.trim() !== "" diff --git a/Modules/Audio/CircularSpectrum.qml b/Modules/Audio/CircularSpectrum.qml index e4e0850..7d0de41 100644 --- a/Modules/Audio/CircularSpectrum.qml +++ b/Modules/Audio/CircularSpectrum.qml @@ -5,8 +5,8 @@ Item { id: root property int innerRadius: 32 * scaling property int outerRadius: 64 * scaling - property color fillColor: Colors.accentPrimary - property color strokeColor: Colors.textPrimary + property color fillColor: Colors.colorPrimary + property color strokeColor: Colors.colorOnSurface property int strokeWidth: 0 * scaling property var values: [] property int usableOuter: 64 diff --git a/Modules/Audio/LinearSpectrum.qml b/Modules/Audio/LinearSpectrum.qml index bb1600d..b5e1b08 100644 --- a/Modules/Audio/LinearSpectrum.qml +++ b/Modules/Audio/LinearSpectrum.qml @@ -3,8 +3,8 @@ import qs.Services Item { id: root - property color fillColor: Colors.accentPrimary - property color strokeColor: Colors.textPrimary + property color fillColor: Colors.colorPrimary + property color strokeColor: Colors.colorOnSurface property int strokeWidth: 0 property var values: [] diff --git a/Modules/Background/Overview.qml b/Modules/Background/Overview.qml index b44f05e..399c25e 100644 --- a/Modules/Background/Overview.qml +++ b/Modules/Background/Overview.qml @@ -62,7 +62,7 @@ NLoader { Rectangle { anchors.fill: parent - color: Qt.rgba(Colors.backgroundPrimary.r, Colors.backgroundPrimary.g, Colors.backgroundPrimary.b, 0.5) + color: Qt.rgba(Colors.colorSurface.r, Colors.colorSurface.g, Colors.colorSurface.b, 0.5) } } } diff --git a/Modules/Background/ScreenCorners.qml b/Modules/Background/ScreenCorners.qml index 6bd65cd..12acee1 100644 --- a/Modules/Background/ScreenCorners.qml +++ b/Modules/Background/ScreenCorners.qml @@ -17,7 +17,7 @@ NLoader { required property ShellScreen modelData // Visible ring color - property color ringColor: Colors.backgroundPrimary + property color ringColor: Colors.colorSurface // The amount subtracted from full size for the inner cutout // Inner size = full size - borderWidth (per axis) property int borderWidth: Style.borderMedium diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index afba6fa..dadadb0 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -36,7 +36,7 @@ Variants { id: bar anchors.fill: parent - color: Colors.backgroundPrimary + color: Colors.colorSurface layer.enabled: true } diff --git a/Modules/Bar/Battery.qml b/Modules/Bar/Battery.qml index 97bc807..3c64c96 100644 --- a/Modules/Bar/Battery.qml +++ b/Modules/Bar/Battery.qml @@ -51,10 +51,10 @@ NPill { icon: root.batteryIcon() text: Math.round(root.percent) + "%" - pillColor: Colors.surfaceVariant - iconCircleColor: Colors.accentPrimary - iconTextColor: Colors.backgroundPrimary - textColor: charging ? Colors.accentPrimary : Colors.textPrimary + pillColor: Colors.colorSurfaceVariant + iconCircleColor: Colors.colorPrimary + iconTextColor: Colors.colorSurface + textColor: charging ? Colors.colorPrimary : Colors.colorOnSurface tooltipText: { let lines = [] diff --git a/Modules/Bar/SystemMonitor.qml b/Modules/Bar/SystemMonitor.qml index ccac84b..d8efec0 100644 --- a/Modules/Bar/SystemMonitor.qml +++ b/Modules/Bar/SystemMonitor.qml @@ -23,7 +23,7 @@ Row { font.pointSize: Style.fontSizeLarge * scaling verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter - color: Colors.accentPrimary + color: Colors.colorPrimary } NText { @@ -45,7 +45,7 @@ Row { text: "thermometer" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeLarge * scaling - color: Colors.accentPrimary + color: Colors.colorPrimary verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter } @@ -68,7 +68,7 @@ Row { text: "memory" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeLarge * scaling - color: Colors.accentPrimary + color: Colors.colorPrimary verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter } diff --git a/Modules/Bar/TrayMenu.qml b/Modules/Bar/TrayMenu.qml index 81a0778..153da44 100644 --- a/Modules/Bar/TrayMenu.qml +++ b/Modules/Bar/TrayMenu.qml @@ -73,8 +73,8 @@ PopupWindow { Rectangle { id: bg anchors.fill: parent - color: Colors.backgroundSecondary - border.color: Colors.outline + color: Colors.colorSurface + border.color: Colors.colorOutline border.width: Math.max(1, Style.borderThin * scaling) radius: Style.radiusMedium * scaling z: 0 @@ -112,7 +112,7 @@ PopupWindow { Rectangle { id: bg anchors.fill: parent - color: mouseArea.containsMouse ? Colors.hover : "transparent" + color: mouseArea.containsMouse ? Colors.colorTertiary : "transparent" radius: Style.radiusSmall * scaling visible: !(modelData?.isSeparator ?? false) @@ -126,7 +126,7 @@ PopupWindow { id: text Layout.fillWidth: true color: (modelData?.enabled - ?? true) ? (mouseArea.containsMouse ? Colors.textPrimary : Colors.textPrimary) : Colors.textDisabled + ?? true) ? (mouseArea.containsMouse ? Colors.colorOnSurface : Colors.colorOnSurface) : Colors.textDisabled text: modelData?.text ?? "" font.pointSize: Style.fontSizeSmall * scaling verticalAlignment: Text.AlignVCenter @@ -148,7 +148,7 @@ PopupWindow { font.pointSize: Style.fontSizeSmall * scaling verticalAlignment: Text.AlignVCenter visible: modelData?.hasChildren ?? false - color: Colors.textPrimary + color: Colors.colorOnSurface } } @@ -308,8 +308,8 @@ PopupWindow { Rectangle { id: bg anchors.fill: parent - color: Colors.backgroundSecondary - border.color: Colors.outline + color: Colors.colorSurface + border.color: Colors.colorOutline border.width: Math.max(1, Style.borderThin * scaling) radius: Style.radiusMedium * scaling z: 0 @@ -347,10 +347,10 @@ PopupWindow { Rectangle { id: bg anchors.fill: parent - color: mouseArea.containsMouse ? Colors.hover : "transparent" + color: mouseArea.containsMouse ? Colors.colorTertiary : "transparent" radius: Style.radiusSmall * scaling visible: !(modelData?.isSeparator ?? false) - property color hoverTextColor: mouseArea.containsMouse ? Colors.textPrimary : Colors.textPrimary + property color hoverTextColor: mouseArea.containsMouse ? Colors.colorOnSurface : Colors.colorOnSurface RowLayout { anchors.fill: parent diff --git a/Modules/Bar/Volume.qml b/Modules/Bar/Volume.qml index ddceda1..ee75c32 100644 --- a/Modules/Bar/Volume.qml +++ b/Modules/Bar/Volume.qml @@ -38,8 +38,8 @@ Item { NPill { id: pill icon: getIcon() - iconCircleColor: Colors.accentPrimary - collapsedIconColor: Colors.textPrimary + iconCircleColor: Colors.colorPrimary + collapsedIconColor: Colors.colorOnSurface autoHide: true text: Math.floor(Audio.volume * 100) + "%" tooltipText: "Volume: " + Math.round( diff --git a/Modules/Bar/WiFiMenu.qml b/Modules/Bar/WiFiMenu.qml index 7d13663..ba320fc 100644 --- a/Modules/Bar/WiFiMenu.qml +++ b/Modules/Bar/WiFiMenu.qml @@ -67,9 +67,9 @@ NLoader { Rectangle { id: wifiMenuRect - color: Colors.backgroundSecondary + color: Colors.colorSurface radius: Style.radiusMedium * scaling - border.color: Colors.backgroundTertiary + border.color: Colors.colorSurfaceVariant border.width: Math.max(1, Style.borderMedium * scaling) width: 340 * scaling height: 320 * scaling @@ -119,14 +119,14 @@ NLoader { text: "wifi" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXL * scaling - color: Colors.accentPrimary + color: Colors.colorPrimary } NText { text: "WiFi" font.pointSize: Style.fontSizeLarge * scaling font.bold: true - color: Colors.textPrimary + color: Colors.colorOnSurface Layout.fillWidth: true } @@ -180,7 +180,7 @@ NLoader { Layout.fillWidth: true Layout.preferredHeight: Style.baseWidgetSize * 1.5 * scaling radius: Style.radiusMedium * scaling - color: modelData.connected ? Colors.accentPrimary : (networkMouseArea.containsMouse ? Colors.hover : "transparent") + color: modelData.connected ? Colors.colorPrimary : (networkMouseArea.containsMouse ? Colors.colorTertiary : "transparent") RowLayout { anchors.fill: parent @@ -191,7 +191,7 @@ NLoader { text: network.signalIcon(modelData.signal) font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXL * scaling - color: modelData.connected ? Colors.backgroundPrimary : (networkMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary) + color: modelData.connected ? Colors.colorSurface : (networkMouseArea.containsMouse ? Colors.colorSurface : Colors.colorOnSurface) } ColumnLayout { @@ -204,7 +204,7 @@ NLoader { font.pointSize: Style.fontSizeNormal * scaling elide: Text.ElideRight Layout.fillWidth: true - color: modelData.connected ? Colors.backgroundPrimary : (networkMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary) + color: modelData.connected ? Colors.colorSurface : (networkMouseArea.containsMouse ? Colors.colorSurface : Colors.colorOnSurface) } // Security Protocol @@ -213,14 +213,14 @@ NLoader { font.pointSize: Style.fontSizeTiny * scaling elide: Text.ElideRight Layout.fillWidth: true - color: modelData.connected ? Colors.backgroundPrimary : (networkMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary) + color: modelData.connected ? Colors.colorSurface : (networkMouseArea.containsMouse ? Colors.colorSurface : Colors.colorOnSurface) } NText { visible: network.connectStatusSsid === modelData.ssid && network.connectStatus === "error" && network.connectError.length > 0 text: network.connectError - color: Colors.error + color: Colors.colorError font.pointSize: Style.fontSizeSmall * scaling elide: Text.ElideRight Layout.fillWidth: true @@ -236,7 +236,7 @@ NLoader { NBusyIndicator { visible: network.connectingSsid === modelData.ssid running: network.connectingSsid === modelData.ssid - color: Colors.accentPrimary + color: Colors.colorPrimary anchors.centerIn: parent size: Style.baseWidgetSize * 0.7 * scaling } @@ -257,7 +257,7 @@ NLoader { text: "error" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeSmall * scaling - color: Colors.error + color: Colors.colorError anchors.centerIn: parent } } @@ -266,7 +266,7 @@ NLoader { visible: modelData.connected text: "connected" font.pointSize: Style.fontSizeSmall * scaling - color: modelData.connected ? Colors.backgroundPrimary : (networkMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary) + color: modelData.connected ? Colors.colorSurface : (networkMouseArea.containsMouse ? Colors.colorSurface : Colors.colorOnSurface) } } @@ -298,7 +298,7 @@ NLoader { Layout.preferredHeight: modelData.ssid === passwordPromptSsid && showPasswordPrompt ? 60 : 0 Layout.margins: 8 visible: modelData.ssid === passwordPromptSsid && showPasswordPrompt - color: Colors.surfaceVariant + color: Colors.colorSurfaceVariant radius: Style.radiusSmall * scaling RowLayout { @@ -314,7 +314,7 @@ NLoader { anchors.fill: parent radius: 8 color: "transparent" - border.color: passwordInputField.activeFocus ? Colors.accentPrimary : Colors.outline + border.color: passwordInputField.activeFocus ? Colors.colorPrimary : Colors.colorOutline border.width: 1 TextInput { @@ -323,7 +323,7 @@ NLoader { anchors.margins: Style.marginMedium * scaling text: passwordInput font.pointSize: Style.fontSizeMedium * scaling - color: Colors.textPrimary + color: Colors.colorOnSurface verticalAlignment: TextInput.AlignVCenter clip: true focus: true @@ -350,8 +350,8 @@ NLoader { Layout.preferredWidth: 80 Layout.preferredHeight: 36 radius: Style.radiusMedium * scaling - color: Colors.accentPrimary - border.color: Colors.accentPrimary + color: Colors.colorPrimary + border.color: Colors.colorPrimary border.width: 0 Behavior on color { @@ -363,7 +363,7 @@ NLoader { NText { anchors.centerIn: parent text: "Connect" - color: Colors.backgroundPrimary + color: Colors.colorSurface font.pointSize: Style.fontSizeSmall * scaling } @@ -375,8 +375,8 @@ NLoader { } cursorShape: Qt.PointingHandCursor hoverEnabled: true - onEntered: parent.color = Qt.darker(Colors.accentPrimary, 1.1) - onExited: parent.color = Colors.accentPrimary + onEntered: parent.color = Qt.darker(Colors.colorPrimary, 1.1) + onExited: parent.color = Colors.colorPrimary } } } diff --git a/Modules/Bar/Workspace.qml b/Modules/Bar/Workspace.qml index 36797d1..ac6263e 100644 --- a/Modules/Bar/Workspace.qml +++ b/Modules/Bar/Workspace.qml @@ -18,7 +18,7 @@ Item { property ListModel localWorkspaces: ListModel {} property real masterProgress: 0.0 property bool effectsActive: false - property color effectColor: Colors.accentPrimary + property color effectColor: Colors.colorPrimary property int horizontalPadding: Math.round(16 * s) property int spacingBetweenPills: Math.round(8 * s) @@ -72,7 +72,7 @@ Item { } function triggerUnifiedWave() { - effectColor = Colors.accentPrimary + effectColor = Colors.colorPrimary masterAnimation.restart() } @@ -108,7 +108,7 @@ Item { const ws = localWorkspaces.get(i) if (ws.isFocused === true) { root.triggerUnifiedWave() - root.workspaceChanged(ws.id, Colors.accentPrimary) + root.workspaceChanged(ws.id, Colors.colorPrimary) break } } @@ -121,14 +121,12 @@ Item { anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter radius: Math.round(12 * s) - color: Colors.surfaceVariant - border.color: Qt.rgba(Colors.textPrimary.r, Colors.textPrimary.g, Colors.textPrimary.b, 0.1) + color: Colors.colorSurfaceVariant + border.color: Colors.colorOutlineVariant border.width: Math.max(1, Math.round(1 * s)) layer.enabled: true layer.effect: MultiEffect { - shadowColor: "black" - - // radius: 12 + shadowColor: Colors.colorShadow shadowVerticalOffset: 0 shadowHorizontalOffset: 0 shadowOpacity: 0.10 @@ -168,15 +166,15 @@ Item { } color: { if (model.isFocused) - return Colors.accentPrimary + return Colors.colorPrimary if (model.isUrgent) - return Colors.error + return Colors.colorError if (model.isActive || model.isOccupied) - return Colors.accentTertiary + return Colors.colorSecondary if (model.isUrgent) - return Colors.error + return Colors.colorError - return Colors.outline + return Colors.colorOutline } scale: model.isFocused ? 1.0 : 0.9 z: 0 diff --git a/Modules/Calendar/Calendar.qml b/Modules/Calendar/Calendar.qml index 3feaee8..a2a67d0 100644 --- a/Modules/Calendar/Calendar.qml +++ b/Modules/Calendar/Calendar.qml @@ -63,9 +63,9 @@ NLoader { Rectangle { id: calendarRect - color: Colors.backgroundSecondary + color: Colors.colorSurface radius: Style.radiusMedium * scaling - border.color: Colors.backgroundTertiary + border.color: Colors.colorSurfaceVariant border.width: Math.max(1, Style.borderMedium * scaling) width: 340 * scaling height: 320 * scaling // Reduced height to eliminate bottom space @@ -135,7 +135,7 @@ NLoader { horizontalAlignment: Text.AlignHCenter font.pointSize: Style.fontSizeMedium * scaling font.weight: Style.fontWeightBold - color: Colors.accentPrimary + color: Colors.colorPrimary } NIconButton { @@ -172,7 +172,7 @@ NLoader { let dayIndex = (firstDay + index) % 7 return Qt.locale().dayName(dayIndex, Locale.ShortFormat) } - color: Colors.accentSecondary + color: Colors.colorSecondary font.pointSize: Style.fontSizeMedium * scaling font.weight: Style.fontWeightBold horizontalAlignment: Text.AlignHCenter @@ -210,12 +210,12 @@ NLoader { width: (Style.baseWidgetSize * scaling) height: (Style.baseWidgetSize * scaling) radius: Style.radiusSmall * scaling - color: model.today ? Colors.accentPrimary : "transparent" + color: model.today ? Colors.colorPrimary : "transparent" NText { anchors.centerIn: parent text: model.day - color: model.today ? Colors.onAccent : Colors.textPrimary + color: model.today ? Colors.onAccent : Colors.colorOnSurface opacity: model.month === grid.month ? Style.opacityHeavy : Style.opacityLight font.pointSize: (Style.fontSizeMedium * scaling) font.weight: model.today ? Style.fontWeightBold : Style.fontWeightRegular diff --git a/Modules/Demo/DemoPanel.qml b/Modules/Demo/DemoPanel.qml index 799fbc6..2025ace 100644 --- a/Modules/Demo/DemoPanel.qml +++ b/Modules/Demo/DemoPanel.qml @@ -68,10 +68,10 @@ NLoader { Rectangle { id: bgRect - color: Colors.backgroundPrimary + color: Colors.colorSurface radius: Style.radiusMedium * scaling - border.color: Colors.accentPrimary - border.width: 2 + border.color: Colors.colorOutlineVariant + border.width: Math.max(1, Style.borderThin * scaling) width: 500 * scaling height: 700 * scaling anchors.centerIn: parent @@ -115,7 +115,7 @@ NLoader { NText { text: "DemoPanel" - color: Colors.accentPrimary + color: Colors.colorPrimary font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold Layout.alignment: Qt.AlignHCenter @@ -130,7 +130,7 @@ NLoader { spacing: Style.marginLarge * scaling NText { text: "Scaling" - color: Colors.accentSecondary + color: Colors.colorSecondary font.weight: Style.fontWeightBold } NText { @@ -173,7 +173,7 @@ NLoader { spacing: Style.marginLarge * scaling NText { text: "NIconButton" - color: Colors.accentSecondary + color: Colors.colorSecondary font.weight: Style.fontWeightBold } @@ -193,7 +193,7 @@ NLoader { spacing: Style.marginMedium * scaling NText { text: "NToggle" - color: Colors.accentSecondary + color: Colors.colorSecondary font.weight: Style.fontWeightBold } @@ -215,7 +215,7 @@ NLoader { spacing: Style.marginMedium * scaling NText { text: "NComboBox" - color: Colors.accentSecondary + color: Colors.colorSecondary font.weight: Style.fontWeightBold } @@ -240,7 +240,7 @@ NLoader { spacing: Style.marginMedium * scaling NText { text: "NTextInput" - color: Colors.accentSecondary + color: Colors.colorSecondary font.weight: Style.fontWeightBold } @@ -263,7 +263,7 @@ NLoader { spacing: Style.marginMedium * scaling NText { text: "NBusyIndicator" - color: Colors.accentSecondary + color: Colors.colorSecondary font.weight: Style.fontWeightBold } diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml index 2e0c2a6..6fc8e82 100644 --- a/Modules/Dock/Dock.qml +++ b/Modules/Dock/Dock.qml @@ -104,7 +104,7 @@ NLoader { id: dockContainer width: dock.width + 40 height: 50 - color: Colors.backgroundSecondary + color: Colors.colorSurface anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom topLeftRadius: 20 @@ -203,7 +203,7 @@ NLoader { text: appButton.appId ? appButton.appId.charAt(0).toUpperCase() : "?" font.pixelSize: 14 font.bold: true - color: appButton.isActive ? Colors.accentPrimary : Colors.textPrimary + color: appButton.isActive ? Colors.colorPrimary : Colors.colorOnSurface } MouseArea { @@ -253,7 +253,7 @@ NLoader { visible: isActive width: 20 height: 3 - color: Colors.accentPrimary + color: Colors.colorPrimary radius: 1.5 anchors.bottom: parent.bottom anchors.horizontalCenter: parent.horizontalCenter @@ -292,8 +292,8 @@ NLoader { width: 80 height: 32 radius: 8 - color: closeMouseArea.containsMouse ? Colors.hover : Colors.backgroundPrimary - border.color: Colors.outline + color: closeMouseArea.containsMouse ? Colors.colorTertiary : Colors.colorSurface + border.color: Colors.colorOutline border.width: 1 x: { @@ -315,7 +315,7 @@ NLoader { anchors.centerIn: parent text: "Close" font.pixelSize: 14 - color: Colors.textPrimary + color: Colors.colorOnSurface } MouseArea { diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index 0adb9e0..07c7077 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -138,7 +138,7 @@ WlSessionLock { width: Math.random() * 4 + 2 height: width radius: width * 0.5 - color: Qt.rgba(Colors.accentPrimary.r, Colors.accentPrimary.g, Colors.accentPrimary.b, 0.3) + color: Qt.rgba(Colors.colorPrimary.r, Colors.colorPrimary.g, Colors.colorPrimary.b, 0.3) x: Math.random() * parent.width y: Math.random() * parent.height @@ -181,7 +181,7 @@ WlSessionLock { font.pointSize: Style.fontSizeXXL * 6 font.weight: Font.Bold font.letterSpacing: -2 - color: Colors.textPrimary + color: Colors.colorOnSurface horizontalAlignment: Text.AlignHCenter SequentialAnimation on scale { @@ -205,7 +205,7 @@ WlSessionLock { font.family: "Inter" font.pointSize: Style.fontSizeXL font.weight: Font.Light - color: Colors.textSecondary + color: Colors.colorOnSurface horizontalAlignment: Text.AlignHCenter width: timeText.width } @@ -222,7 +222,7 @@ WlSessionLock { height: 120 * Scaling.scale(screen) radius: width * 0.5 color: "transparent" - border.color: Colors.accentPrimary + border.color: Colors.colorPrimary border.width: 3 * Scaling.scale(screen) anchors.horizontalCenter: parent.horizontalCenter @@ -233,7 +233,7 @@ WlSessionLock { height: parent.height + 24 * Scaling.scale(screen) radius: width * 0.5 color: "transparent" - border.color: Qt.rgba(Colors.accentPrimary.r, Colors.accentPrimary.g, Colors.accentPrimary.b, 0.3) + border.color: Qt.rgba(Colors.colorPrimary.r, Colors.colorPrimary.g, Colors.colorPrimary.b, 0.3) border.width: 2 * Scaling.scale(screen) z: -1 @@ -301,8 +301,8 @@ WlSessionLock { id: terminalBackground anchors.fill: parent radius: 16 - color: Colors.applyOpacity(Colors.backgroundPrimary, "E6") - border.color: Colors.accentPrimary + color: Colors.applyOpacity(Colors.colorSurface, "E6") + border.color: Colors.colorPrimary border.width: 2 * Scaling.scale(screen) // Scanline effect @@ -311,7 +311,7 @@ WlSessionLock { Rectangle { width: parent.width height: 1 - color: Colors.applyOpacity(Colors.accentPrimary, "1A") + color: Colors.applyOpacity(Colors.colorPrimary, "1A") y: index * 10 opacity: 0.3 @@ -333,7 +333,7 @@ WlSessionLock { Rectangle { width: parent.width height: 40 * Scaling.scale(screen) - color: Colors.applyOpacity(Colors.accentPrimary, "33") + color: Colors.applyOpacity(Colors.colorPrimary, "33") topLeftRadius: 14 topRightRadius: 14 @@ -344,7 +344,7 @@ WlSessionLock { Text { text: "SECURE TERMINAL" - color: Colors.textPrimary + color: Colors.colorOnSurface font.family: "DejaVu Sans Mono" font.pointSize: Style.fontSizeLarge font.weight: Font.Bold @@ -370,7 +370,7 @@ WlSessionLock { Text { text: "root@noctalia:~$" - color: Colors.accentPrimary + color: Colors.colorPrimary font.family: "DejaVu Sans Mono" font.pointSize: Style.fontSizeLarge font.weight: Font.Bold @@ -379,7 +379,7 @@ WlSessionLock { Text { id: welcomeText text: "" - color: Colors.textPrimary + color: Colors.colorOnSurface font.family: "DejaVu Sans Mono" font.pointSize: Style.fontSizeLarge property int currentIndex: 0 @@ -408,7 +408,7 @@ WlSessionLock { Text { text: "root@noctalia:~$" - color: Colors.accentPrimary + color: Colors.colorPrimary font.family: "DejaVu Sans Mono" font.pointSize: Style.fontSizeLarge font.weight: Font.Bold @@ -416,7 +416,7 @@ WlSessionLock { Text { text: "sudo unlock_session" - color: Colors.textPrimary + color: Colors.colorOnSurface font.family: "DejaVu Sans Mono" font.pointSize: Style.fontSizeLarge } @@ -429,7 +429,7 @@ WlSessionLock { visible: false font.family: "DejaVu Sans Mono" font.pointSize: Style.fontSizeLarge - color: Colors.textPrimary + color: Colors.colorOnSurface echoMode: TextInput.Password passwordCharacter: "*" passwordMaskDelay: 0 @@ -456,7 +456,7 @@ WlSessionLock { Text { id: asterisksText text: "*".repeat(passwordInput.text.length) - color: Colors.textPrimary + color: Colors.colorOnSurface font.family: "DejaVu Sans Mono" font.pointSize: Style.fontSizeLarge visible: passwordInput.activeFocus @@ -483,7 +483,7 @@ WlSessionLock { Rectangle { width: 8 * Scaling.scale(screen) height: 20 * Scaling.scale(screen) - color: Colors.accentPrimary + color: Colors.colorPrimary visible: passwordInput.activeFocus anchors.left: asterisksText.right anchors.leftMargin: 2 * Scaling.scale(screen) @@ -506,7 +506,7 @@ WlSessionLock { // Status messages Text { text: lock.authenticating ? "Authenticating..." : (lock.errorMessage !== "" ? "Authentication failed." : "") - color: lock.authenticating ? Colors.accentPrimary : (lock.errorMessage !== "" ? Colors.error : "transparent") + color: lock.authenticating ? Colors.colorPrimary : (lock.errorMessage !== "" ? Colors.colorError : "transparent") font.family: "DejaVu Sans Mono" font.pointSize: Style.fontSizeLarge Layout.fillWidth: true @@ -530,9 +530,9 @@ WlSessionLock { width: 120 * Scaling.scale(screen) height: 40 * Scaling.scale(screen) radius: 12 - color: executeButtonArea.containsMouse ? Colors.accentPrimary : Colors.applyOpacity( - Colors.accentPrimary, "33") - border.color: Colors.accentPrimary + color: executeButtonArea.containsMouse ? Colors.colorPrimary : Colors.applyOpacity( + Colors.colorPrimary, "33") + border.color: Colors.colorPrimary border.width: 1 enabled: !lock.authenticating Layout.alignment: Qt.AlignRight @@ -541,7 +541,7 @@ WlSessionLock { Text { anchors.centerIn: parent text: lock.authenticating ? "EXECUTING" : "EXECUTE" - color: executeButtonArea.containsMouse ? Colors.onAccent : Colors.accentPrimary + color: executeButtonArea.containsMouse ? Colors.onAccent : Colors.colorPrimary font.family: "DejaVu Sans Mono" font.pointSize: Style.fontSizeMedium font.weight: Font.Bold @@ -595,7 +595,7 @@ WlSessionLock { anchors.fill: parent radius: parent.radius color: "transparent" - border.color: Colors.applyOpacity(Colors.accentPrimary, "4D") + border.color: Colors.applyOpacity(Colors.colorPrimary, "4D") border.width: 1 z: -1 @@ -631,8 +631,8 @@ WlSessionLock { width: 64 * Scaling.scale(screen) height: 64 * Scaling.scale(screen) radius: 32 - color: Qt.rgba(Colors.error.r, Colors.error.g, Colors.error.b, shutdownArea.containsMouse ? 0.9 : 0.2) - border.color: Colors.error + color: Qt.rgba(Colors.colorError.r, Colors.colorError.g, Colors.colorError.b, shutdownArea.containsMouse ? 0.9 : 0.2) + border.color: Colors.colorError border.width: 2 * Scaling.scale(screen) // Glow effect @@ -642,7 +642,7 @@ WlSessionLock { height: parent.height + 10 * Scaling.scale(screen) radius: width * 0.5 color: "transparent" - border.color: Qt.rgba(Colors.error.r, Colors.error.g, Colors.error.b, 0.3) + border.color: Qt.rgba(Colors.colorError.r, Colors.colorError.g, Colors.colorError.b, 0.3) border.width: 2 * Scaling.scale(screen) opacity: shutdownArea.containsMouse ? 1 : 0 z: -1 @@ -671,7 +671,7 @@ WlSessionLock { text: "power_settings_new" font.family: "Material Symbols Outlined" font.pixelSize: 28 * Scaling.scale(screen) - color: shutdownArea.containsMouse ? Colors.onAccent : Colors.error + color: shutdownArea.containsMouse ? Colors.onAccent : Colors.colorError } Behavior on color { @@ -688,9 +688,9 @@ WlSessionLock { width: 64 * Scaling.scale(screen) height: 64 * Scaling.scale(screen) radius: 32 - color: Qt.rgba(Colors.accentPrimary.r, Colors.accentPrimary.g, Colors.accentPrimary.b, + color: Qt.rgba(Colors.colorPrimary.r, Colors.colorPrimary.g, Colors.colorPrimary.b, rebootArea.containsMouse ? 0.9 : 0.2) - border.color: Colors.accentPrimary + border.color: Colors.colorPrimary border.width: 2 * Scaling.scale(screen) // Glow effect @@ -700,7 +700,7 @@ WlSessionLock { height: parent.height + 10 * Scaling.scale(screen) radius: width * 0.5 color: "transparent" - border.color: Qt.rgba(Colors.accentPrimary.r, Colors.accentPrimary.g, Colors.accentPrimary.b, 0.3) + border.color: Qt.rgba(Colors.colorPrimary.r, Colors.colorPrimary.g, Colors.colorPrimary.b, 0.3) border.width: 2 * Scaling.scale(screen) opacity: rebootArea.containsMouse ? 1 : 0 z: -1 @@ -728,7 +728,7 @@ WlSessionLock { text: "refresh" font.family: "Material Symbols Outlined" font.pixelSize: 28 * Scaling.scale(screen) - color: rebootArea.containsMouse ? Colors.onAccent : Colors.accentPrimary + color: rebootArea.containsMouse ? Colors.onAccent : Colors.colorPrimary } Behavior on color { @@ -745,9 +745,9 @@ WlSessionLock { width: 64 * Scaling.scale(screen) height: 64 * Scaling.scale(screen) radius: 32 - color: Qt.rgba(Colors.accentSecondary.r, Colors.accentSecondary.g, Colors.accentSecondary.b, + color: Qt.rgba(Colors.colorSecondary.r, Colors.colorSecondary.g, Colors.colorSecondary.b, logoutArea.containsMouse ? 0.9 : 0.2) - border.color: Colors.accentSecondary + border.color: Colors.colorSecondary border.width: 2 * Scaling.scale(screen) // Glow effect @@ -757,7 +757,7 @@ WlSessionLock { height: parent.height + 10 * Scaling.scale(screen) radius: width * 0.5 color: "transparent" - border.color: Qt.rgba(Colors.accentSecondary.r, Colors.accentSecondary.g, Colors.accentSecondary.b, 0.3) + border.color: Qt.rgba(Colors.colorSecondary.r, Colors.colorSecondary.g, Colors.colorSecondary.b, 0.3) border.width: 2 * Scaling.scale(screen) opacity: logoutArea.containsMouse ? 1 : 0 z: -1 @@ -787,7 +787,7 @@ WlSessionLock { text: "exit_to_app" font.family: "Material Symbols Outlined" font.pixelSize: 28 * Scaling.scale(screen) - color: logoutArea.containsMouse ? Colors.onAccent : Colors.accentSecondary + color: logoutArea.containsMouse ? Colors.onAccent : Colors.colorSecondary } Behavior on color { diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index d7c3e94..7f71ecb 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -63,18 +63,9 @@ PanelWindow { height: Math.max(80 * scaling, contentColumn.implicitHeight + (Style.marginMedium * 2 * scaling)) clip: true radius: Style.radiusMedium * scaling - border.color: Colors.accentPrimary + border.color: Colors.colorPrimary border.width: Math.max(1, Style.borderThin * scaling) - gradient: Gradient { - GradientStop { - position: 0.0 - color: Colors.backgroundSecondary - } - GradientStop { - position: 1.0 - color: Colors.backgroundTertiary - } - } + color: Colors.colorSurface // Animation properties property real scaleValue: 0.8 @@ -142,14 +133,14 @@ PanelWindow { spacing: Style.marginSmall * scaling NText { text: (model.appName || model.desktopEntry) || "Unknown App" - color: Colors.accentSecondary + color: Colors.colorSecondary font.pointSize: Style.fontSizeSmall * scaling } Rectangle { width: 6 * scaling height: 6 * scaling radius: 3 * scaling - color: (model.urgency === NotificationUrgency.Critical) ? Colors.error : (model.urgency === NotificationUrgency.Low) ? Colors.textSecondary : Colors.accentPrimary + color: (model.urgency === NotificationUrgency.Critical) ? Colors.colorError : (model.urgency === NotificationUrgency.Low) ? Colors.colorOnSurface : Colors.colorPrimary Layout.alignment: Qt.AlignVCenter } Item { @@ -157,7 +148,7 @@ PanelWindow { } NText { text: notificationService.formatTimestamp(model.timestamp) - color: Colors.textSecondary + color: Colors.colorOnSurface font.pointSize: Style.fontSizeSmall * scaling } } @@ -166,7 +157,7 @@ PanelWindow { text: model.summary || "No summary" font.pointSize: Style.fontSizeLarge * scaling font.weight: Style.fontWeightBold - color: Colors.textPrimary + color: Colors.colorOnSurface wrapMode: Text.Wrap width: 300 * scaling maximumLineCount: 3 @@ -176,7 +167,7 @@ PanelWindow { NText { text: model.body || "" font.pointSize: Style.fontSizeSmall * scaling - color: Colors.textSecondary + color: Colors.colorOnSurface wrapMode: Text.Wrap width: 300 * scaling maximumLineCount: 5 diff --git a/Modules/Notification/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml index 0fc0aac..3dce3b3 100644 --- a/Modules/Notification/NotificationHistoryPanel.qml +++ b/Modules/Notification/NotificationHistoryPanel.qml @@ -65,9 +65,9 @@ NLoader { Rectangle { id: notificationRect - color: Colors.backgroundSecondary + color: Colors.colorSurface radius: Style.radiusMedium * scaling - border.color: Colors.backgroundTertiary + border.color: Colors.colorSurfaceVariant border.width: Math.max(1, Style.borderMedium * scaling) width: 400 * scaling height: 500 * scaling @@ -117,14 +117,14 @@ NLoader { text: "notifications" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXL * scaling - color: Colors.accentPrimary + color: Colors.colorPrimary } NText { text: "Notification History" font.pointSize: Style.fontSizeLarge * scaling font.bold: true - color: Colors.textPrimary + color: Colors.colorOnSurface Layout.fillWidth: true } @@ -159,7 +159,7 @@ NLoader { width: notificationList ? (notificationList.width - 20) : 380 * scaling height: Math.max(80, notificationContent.height + 30) radius: Style.radiusMedium * scaling - color: notificationMouseArea.containsMouse ? Colors.accentPrimary : "transparent" + color: notificationMouseArea.containsMouse ? Colors.colorPrimary : "transparent" RowLayout { anchors { @@ -179,7 +179,7 @@ NLoader { text: (summary || "No summary").substring(0, 100) font.pointSize: Style.fontSizeMedium * scaling font.weight: Font.Medium - color: notificationMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary + color: notificationMouseArea.containsMouse ? Colors.colorSurface : Colors.colorOnSurface wrapMode: Text.Wrap width: parent.width - 30 maximumLineCount: 2 @@ -189,7 +189,7 @@ NLoader { NText { text: (body || "").substring(0, 150) font.pointSize: Style.fontSizeSmall * scaling - color: notificationMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.textSecondary + color: notificationMouseArea.containsMouse ? Colors.colorSurface : Colors.colorOnSurface wrapMode: Text.Wrap width: parent.width - 30 maximumLineCount: 3 @@ -199,7 +199,7 @@ NLoader { NText { text: NotificationService.formatTimestamp(timestamp) font.pointSize: Style.fontSizeSmall * scaling - color: notificationMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.textSecondary + color: notificationMouseArea.containsMouse ? Colors.colorSurface : Colors.colorOnSurface } } } diff --git a/Modules/Settings/SettingsPanel.qml b/Modules/Settings/SettingsPanel.qml index 51948ab..e2ae004 100644 --- a/Modules/Settings/SettingsPanel.qml +++ b/Modules/Settings/SettingsPanel.qml @@ -158,9 +158,9 @@ NLoader { Rectangle { id: bgRect - color: Colors.backgroundPrimary + color: Colors.colorSurface radius: Style.radiusLarge * scaling - border.color: Colors.backgroundTertiary + border.color: Colors.colorOutlineVariant border.width: Math.max(1, Style.borderMedium * scaling) layer.enabled: true width: (screen.width * 0.5) * scaling @@ -209,10 +209,10 @@ NLoader { id: sidebar Layout.preferredWidth: 260 * scaling Layout.fillHeight: true - radius: Style.radiusMedium * scaling - color: Colors.backgroundSecondary - border.color: Colors.outline + color: Colors.colorSurfaceVariant + border.color: Colors.colorOutlineVariant border.width: Math.max(1, Style.borderThin * scaling) + radius: Style.radiusMedium * scaling Column { anchors.fill: parent @@ -229,7 +229,7 @@ NLoader { width: parent.width height: 32 * scaling // Back to original height radius: Style.radiusSmall * scaling - color: selected ? Colors.accentPrimary : (tabItem.hovering ? Colors.hover : "transparent") + color: selected ? Colors.colorPrimary : (tabItem.hovering ? Colors.colorTertiary : "transparent") border.color: "transparent" border.width: 0 @@ -238,24 +238,28 @@ NLoader { // Subtle hover effect: only icon/text color tint on hover property bool hovering: false + property color tabTextColor: selected ? Colors.colorOnPrimary : (tabItem.hovering ? Colors.colorOnTertiary : Colors.colorOnSurface) + RowLayout { anchors.fill: parent anchors.leftMargin: Style.marginSmall * scaling anchors.rightMargin: Style.marginSmall * scaling spacing: Style.marginSmall * scaling + // Tab icon on the left side NText { text: modelData.icon + color: tabTextColor font.family: "Material Symbols Outlined" font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 } font.pointSize: Style.fontSizeLarge * scaling - color: selected ? Colors.onAccent : (tabItem.hovering ? Colors.onAccent : Colors.textSecondary) + } // Tab label on the left side NText { text: modelData.label - color: selected ? Colors.onAccent : (tabItem.hovering ? Colors.onAccent : Colors.textPrimary) + color: tabTextColor font.pointSize: Style.fontSizeMedium * scaling font.weight: Style.fontWeightBold Layout.fillWidth: true @@ -281,8 +285,8 @@ NLoader { Layout.fillWidth: true Layout.fillHeight: true radius: Style.radiusMedium * scaling - color: Colors.surfaceVariant - border.color: Colors.outline + color: Colors.colorSurfaceVariant + border.color: Colors.colorOutlineVariant border.width: Math.max(1, Style.borderThin * scaling) clip: true @@ -302,7 +306,7 @@ NLoader { text: panel.tabsModel[currentTabIndex].label font.pointSize: Style.fontSizeLarge * scaling font.weight: Style.fontWeightBold - color: Colors.accentPrimary + color: Colors.colorPrimary Layout.fillWidth: true } NIconButton { diff --git a/Modules/Settings/Tabs/About.qml b/Modules/Settings/Tabs/About.qml index bafd0d0..82b259f 100644 --- a/Modules/Settings/Tabs/About.qml +++ b/Modules/Settings/Tabs/About.qml @@ -59,7 +59,7 @@ ColumnLayout { text: "Noctalia: quiet by design" font.pointSize: Style.fontSizeXXL * scaling font.weight: Style.fontWeightBold - color: Colors.textPrimary + color: Colors.colorOnSurface Layout.alignment: Qt.AlignCenter Layout.bottomMargin: Style.marginSmall * scaling } @@ -67,7 +67,7 @@ ColumnLayout { NText { text: "It may just be another quickshell setup but it won't get in your way." font.pointSize: Style.fontSizeMedium * scaling - color: Colors.textSecondary + color: Colors.colorOnSurface Layout.alignment: Qt.AlignCenter Layout.bottomMargin: Style.marginLarge * scaling } @@ -80,25 +80,25 @@ ColumnLayout { NText { text: "Latest Version:" - color: Colors.textSecondary + color: Colors.colorOnSurface Layout.alignment: Qt.AlignRight } NText { text: root.latestVersion - color: Colors.textPrimary + color: Colors.colorOnSurface font.weight: Style.fontWeightBold } NText { text: "Installed Version:" - color: Colors.textSecondary + color: Colors.colorOnSurface Layout.alignment: Qt.AlignRight } NText { text: root.currentVersion - color: Colors.textPrimary + color: Colors.colorOnSurface font.weight: Style.fontWeightBold } } @@ -109,8 +109,8 @@ ColumnLayout { Layout.preferredWidth: updateText.implicitWidth + 46 * scaling Layout.preferredHeight: 32 * scaling radius: Style.radiusLarge * scaling - color: updateArea.containsMouse ? Colors.accentPrimary : "transparent" - border.color: Colors.accentPrimary + color: updateArea.containsMouse ? Colors.colorPrimary : "transparent" + border.color: Colors.colorPrimary border.width: 1 visible: { if (root.currentVersion === "Unknown" || root.latestVersion === "Unknown") @@ -138,14 +138,14 @@ ColumnLayout { text: "system_update" font.family: "Material Symbols Outlined" font.pointSize: 18 * scaling - color: updateArea.containsMouse ? Colors.backgroundPrimary : Colors.accentPrimary + color: updateArea.containsMouse ? Colors.colorSurface : Colors.colorPrimary } NText { id: updateText text: "Download latest release" font.pointSize: 14 * scaling - color: updateArea.containsMouse ? Colors.backgroundPrimary : Colors.accentPrimary + color: updateArea.containsMouse ? Colors.colorSurface : Colors.colorPrimary } } @@ -171,7 +171,7 @@ ColumnLayout { text: `Shout-out to our ${root.contributors.length} awesome contributors!` font.pointSize: Style.fontSizeLarge * scaling font.weight: Style.fontWeightBold - color: Colors.textSecondary + color: Colors.colorOnSurface Layout.alignment: Qt.AlignCenter Layout.topMargin: Style.marginLarge * 2 } @@ -199,7 +199,7 @@ ColumnLayout { width: contributorsGrid.cellWidth - Style.marginLarge * scaling height: contributorsGrid.cellHeight - Style.marginTiny * scaling radius: Style.radiusLarge * scaling - color: contributorArea.containsMouse ? Colors.hover : "transparent" + color: contributorArea.containsMouse ? Colors.colorTertiary : "transparent" RowLayout { anchors.fill: parent @@ -216,7 +216,7 @@ ColumnLayout { anchors.fill: parent anchors.margins: Style.marginTiny * scaling fallbackIcon: "person" - borderColor: Colors.accentPrimary + borderColor: Colors.colorPrimary borderWidth: Math.max(1, Style.borderMedium * scaling) imageRadius: width * 0.5 } @@ -230,7 +230,7 @@ ColumnLayout { NText { text: modelData.login || "Unknown" font.weight: Style.fontWeightBold - color: contributorArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary + color: contributorArea.containsMouse ? Colors.colorSurface : Colors.colorOnSurface elide: Text.ElideRight Layout.fillWidth: true } @@ -239,7 +239,7 @@ ColumnLayout { text: (modelData.contributions || 0) + " " + ((modelData.contributions || 0) === 1 ? "commit" : "commits") font.pointSize: Style.fontSizeSmall * scaling - color: contributorArea.containsMouse ? Colors.backgroundPrimary : Colors.textSecondary + color: contributorArea.containsMouse ? Colors.colorSurface : Colors.colorOnSurface } } } diff --git a/Modules/Settings/Tabs/Audio.qml b/Modules/Settings/Tabs/Audio.qml index dc6a052..b174304 100644 --- a/Modules/Settings/Tabs/Audio.qml +++ b/Modules/Settings/Tabs/Audio.qml @@ -49,7 +49,7 @@ ColumnLayout { text: "Audio" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.textPrimary + color: Colors.colorOnSurface Layout.bottomMargin: Style.marginSmall * scaling } @@ -70,13 +70,13 @@ ColumnLayout { NText { text: "Master Volume" font.weight: Style.fontWeightBold - color: Colors.textPrimary + color: Colors.colorOnSurface } NText { text: "System-wide volume level" font.pointSize: Style.fontSizeSmall * scaling - color: Colors.textSecondary + color: Colors.colorOnSurface wrapMode: Text.WordWrap Layout.fillWidth: true } @@ -110,7 +110,7 @@ ColumnLayout { NText { text: Math.floor(Audio.volume * 100) + "%" Layout.alignment: Qt.AlignVCenter - color: Colors.textSecondary + color: Colors.colorOnSurface } } } @@ -149,7 +149,7 @@ ColumnLayout { text: "Audio Devices" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.textPrimary + color: Colors.colorOnSurface Layout.bottomMargin: Style.marginSmall * scaling } @@ -168,13 +168,13 @@ ColumnLayout { text: "Output Device" font.pointSize: Style.fontSizeMedium * scaling font.weight: Style.fontWeightBold - color: Colors.textPrimary + color: Colors.colorOnSurface } NText { text: "Select the desired audio output device" font.pointSize: Style.fontSizeSmall * scaling - color: Colors.textSecondary + color: Colors.colorOnSurface wrapMode: Text.WordWrap Layout.fillWidth: true } @@ -207,13 +207,13 @@ ColumnLayout { text: "Input Device" font.pointSize: Style.fontSizeMedium * scaling font.weight: Style.fontWeightBold - color: Colors.textPrimary + color: Colors.colorOnSurface } NText { text: "Select desired audio input device" font.pointSize: Style.fontSizeSmall * scaling - color: Colors.textSecondary + color: Colors.colorOnSurface wrapMode: Text.WordWrap Layout.fillWidth: true } @@ -247,7 +247,7 @@ ColumnLayout { text: "Audio Visualizer" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.textPrimary + color: Colors.colorOnSurface Layout.bottomMargin: Style.marginSmall * scaling } diff --git a/Modules/Settings/Tabs/Bar.qml b/Modules/Settings/Tabs/Bar.qml index 39f609a..f54a0a8 100644 --- a/Modules/Settings/Tabs/Bar.qml +++ b/Modules/Settings/Tabs/Bar.qml @@ -36,7 +36,7 @@ ColumnLayout { text: "Components" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.textPrimary + color: Colors.colorOnSurface } NToggle { diff --git a/Modules/Settings/Tabs/Display.qml b/Modules/Settings/Tabs/Display.qml index 66f4a72..f93215e 100644 --- a/Modules/Settings/Tabs/Display.qml +++ b/Modules/Settings/Tabs/Display.qml @@ -44,7 +44,7 @@ Item { text: "Per‑monitor configuration" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.textPrimary + color: Colors.colorOnSurface } Repeater { @@ -52,8 +52,8 @@ Item { delegate: Rectangle { Layout.fillWidth: true radius: Style.radiusMedium * scaling - color: Colors.surface - border.color: Colors.outline + color: Colors.colorSurface + border.color: Colors.colorOutline border.width: Math.max(1, Style.borderThin * scaling) implicitHeight: contentCol.implicitHeight + Style.marginXL * 2 * scaling @@ -67,13 +67,13 @@ Item { text: (modelData.name || "Unknown") font.pointSize: Style.fontSizeLarge * scaling font.weight: Style.fontWeightBold - color: Colors.accentSecondary + color: Colors.colorSecondary } NText { text: `Resolution: ${modelData.width}x${modelData.height} - Position: (${modelData.x}, ${modelData.y})` font.pointSize: Style.fontSizeSmall * scaling - color: Colors.textSecondary + color: Colors.colorOnSurface } ColumnLayout { diff --git a/Modules/Settings/Tabs/General.qml b/Modules/Settings/Tabs/General.qml index 53791ac..2ed620e 100644 --- a/Modules/Settings/Tabs/General.qml +++ b/Modules/Settings/Tabs/General.qml @@ -36,7 +36,7 @@ ColumnLayout { text: "General Settings" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.textPrimary + color: Colors.colorOnSurface } // Profile section @@ -55,7 +55,7 @@ ColumnLayout { height: 64 * scaling imagePath: Settings.data.general.avatarImage fallbackIcon: "person" - borderColor: Colors.accentPrimary + borderColor: Colors.colorPrimary borderWidth: Math.max(1, Style.borderMedium) } @@ -87,7 +87,7 @@ ColumnLayout { text: "User Interface" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.textPrimary + color: Colors.colorOnSurface Layout.bottomMargin: Style.marginSmall * scaling } diff --git a/Modules/Settings/Tabs/Misc.qml b/Modules/Settings/Tabs/Misc.qml index 22958a8..64ace7c 100644 --- a/Modules/Settings/Tabs/Misc.qml +++ b/Modules/Settings/Tabs/Misc.qml @@ -36,7 +36,7 @@ ColumnLayout { text: "Miscellaneous Settings" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.textPrimary + color: Colors.colorOnSurface Layout.bottomMargin: Style.marginSmall * scaling } } diff --git a/Modules/Settings/Tabs/Network.qml b/Modules/Settings/Tabs/Network.qml index e3d66d5..5b96c91 100644 --- a/Modules/Settings/Tabs/Network.qml +++ b/Modules/Settings/Tabs/Network.qml @@ -38,7 +38,7 @@ ColumnLayout { text: "Interfaces" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.textPrimary + color: Colors.colorOnSurface } NToggle { diff --git a/Modules/Settings/Tabs/ScreenRecorder.qml b/Modules/Settings/Tabs/ScreenRecorder.qml index b18d2ed..2bc9b2b 100644 --- a/Modules/Settings/Tabs/ScreenRecorder.qml +++ b/Modules/Settings/Tabs/ScreenRecorder.qml @@ -36,7 +36,7 @@ ColumnLayout { text: "Recording" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.textPrimary + color: Colors.colorOnSurface Layout.bottomMargin: Style.marginSmall * scaling } @@ -87,7 +87,7 @@ ColumnLayout { text: "Video Settings" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.textPrimary + color: Colors.colorOnSurface Layout.bottomMargin: Style.marginSmall * scaling } @@ -155,7 +155,7 @@ ColumnLayout { text: "Audio Settings" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.textPrimary + color: Colors.colorOnSurface Layout.bottomMargin: Style.marginSmall * scaling } diff --git a/Modules/Settings/Tabs/TimeWeather.qml b/Modules/Settings/Tabs/TimeWeather.qml index 5f511a0..74d17f5 100644 --- a/Modules/Settings/Tabs/TimeWeather.qml +++ b/Modules/Settings/Tabs/TimeWeather.qml @@ -36,7 +36,7 @@ ColumnLayout { text: "Location" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.textPrimary + color: Colors.colorOnSurface Layout.bottomMargin: Style.marginSmall * scaling } @@ -73,7 +73,7 @@ ColumnLayout { text: "Time Format" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.textPrimary + color: Colors.colorOnSurface Layout.bottomMargin: 8 } @@ -111,7 +111,7 @@ ColumnLayout { text: "Weather" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.textPrimary + color: Colors.colorOnSurface Layout.bottomMargin: Style.marginSmall * scaling } diff --git a/Modules/Settings/Tabs/Wallpaper.qml b/Modules/Settings/Tabs/Wallpaper.qml index 4520be1..c0cd99e 100644 --- a/Modules/Settings/Tabs/Wallpaper.qml +++ b/Modules/Settings/Tabs/Wallpaper.qml @@ -36,7 +36,7 @@ ColumnLayout { text: "Directory" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.textPrimary + color: Colors.colorOnSurface Layout.bottomMargin: Style.marginSmall * scaling } @@ -78,7 +78,7 @@ ColumnLayout { text: "Automation" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.textPrimary + color: Colors.colorOnSurface Layout.bottomMargin: Style.marginSmall * scaling } @@ -111,13 +111,13 @@ ColumnLayout { NText { text: "Wallpaper Interval" font.weight: Style.fontWeightBold - color: Colors.textPrimary + color: Colors.colorOnSurface } NText { text: "How often to change wallpapers automatically (in seconds)" font.pointSize: Style.fontSizeSmall * scaling - color: Colors.textSecondary + color: Colors.colorOnSurface wrapMode: Text.WordWrap Layout.fillWidth: true } @@ -137,7 +137,7 @@ ColumnLayout { stepSize: 10 value: Settings.data.wallpaper.randomInterval onPressedChanged: Settings.data.wallpaper.randomInterval = Math.round(value) - cutoutColor: Colors.backgroundPrimary + cutoutColor: Colors.colorSurface } } } @@ -158,7 +158,7 @@ ColumnLayout { text: "SWWW" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.textPrimary + color: Colors.colorOnSurface Layout.bottomMargin: 8 } @@ -212,13 +212,13 @@ ColumnLayout { NText { text: "Transition FPS" font.weight: Style.fontWeightBold - color: Colors.textPrimary + color: Colors.colorOnSurface } NText { text: "Frames per second for transition animations" font.pointSize: Style.fontSizeSmall * scaling - color: Colors.textSecondary + color: Colors.colorOnSurface wrapMode: Text.WordWrap Layout.fillWidth: true } @@ -238,7 +238,7 @@ ColumnLayout { stepSize: 5 value: Settings.data.wallpaper.swww.transitionFps onPressedChanged: Settings.data.wallpaper.swww.transitionFps = Math.round(value) - cutoutColor: Colors.backgroundPrimary + cutoutColor: Colors.colorSurface } } @@ -251,13 +251,13 @@ ColumnLayout { NText { text: "Transition Duration" font.weight: Style.fontWeightBold - color: Colors.textPrimary + color: Colors.colorOnSurface } NText { text: "Duration of transition animations in seconds" font.pointSize: Style.fontSizeSmall * scaling - color: Colors.textSecondary + color: Colors.colorOnSurface wrapMode: Text.WordWrap Layout.fillWidth: true } @@ -277,7 +277,7 @@ ColumnLayout { stepSize: 0.05 value: Settings.data.wallpaper.swww.transitionDuration onPressedChanged: Settings.data.wallpaper.swww.transitionDuration = value - cutoutColor: Colors.backgroundPrimary + cutoutColor: Colors.colorSurface } } } diff --git a/Modules/Settings/Tabs/WallpaperSelector.qml b/Modules/Settings/Tabs/WallpaperSelector.qml index 3be5c0f..3caa2b7 100644 --- a/Modules/Settings/Tabs/WallpaperSelector.qml +++ b/Modules/Settings/Tabs/WallpaperSelector.qml @@ -32,15 +32,15 @@ Item { text: "Current Wallpaper" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.textPrimary + color: Colors.colorOnSurface } Rectangle { Layout.fillWidth: true Layout.preferredHeight: 120 * scaling radius: Style.radiusMedium * scaling - color: Colors.backgroundSecondary - border.color: Colors.outline + color: Colors.colorSurface + border.color: Colors.colorOutline border.width: Math.max(1, Style.borderThin * scaling) clip: true @@ -50,7 +50,7 @@ Item { anchors.margins: Style.marginSmall * scaling imagePath: Wallpapers.currentWallpaper fallbackIcon: "image" - borderColor: Colors.outline + borderColor: Colors.colorOutline borderWidth: Math.max(1, Style.borderThin * scaling) imageRadius: Style.radiusMedium * scaling } @@ -73,12 +73,12 @@ Item { text: "Wallpaper Selector" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.textPrimary + color: Colors.colorOnSurface } NText { text: "Click on a wallpaper to set it as your current wallpaper" - color: Colors.textSecondary + color: Colors.colorOnSurface wrapMode: Text.WordWrap Layout.fillWidth: true } @@ -86,7 +86,7 @@ Item { NText { text: Settings.data.wallpaper.swww.enabled ? "Wallpapers will change with " + Settings.data.wallpaper.swww.transitionType + " transition" : "Wallpapers will change instantly" - color: Colors.textSecondary + color: Colors.colorOnSurface font.pointSize: Style.fontSizeSmall * scaling visible: Settings.data.wallpaper.swww.enabled } @@ -147,8 +147,8 @@ Item { width: wallpaperGridView.itemSize height: Math.floor(wallpaperGridView.itemSize * 0.67) radius: Style.radiusMedium * scaling - color: isSelected ? Colors.accentPrimary : Colors.backgroundSecondary - border.color: isSelected ? Colors.accentSecondary : Colors.outline + color: isSelected ? Colors.colorPrimary : Colors.colorSurface + border.color: isSelected ? Colors.colorSecondary : Colors.colorOutline border.width: Math.max(1, Style.borderThin * scaling) clip: true @@ -170,8 +170,8 @@ Item { width: 20 * scaling height: 20 * scaling radius: width / 2 - color: Colors.accentPrimary - border.color: Colors.onAccent + color: Colors.colorPrimary + border.color: Colors.colorOutline border.width: Math.max(1, Style.borderThin * scaling) visible: isSelected @@ -180,14 +180,14 @@ Item { text: "check" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeSmall * scaling - color: Colors.onAccent + color: Colors.colorOnPrimary } } // Hover effect Rectangle { anchors.fill: parent - color: Colors.textPrimary + color: Colors.colorOnSurface opacity: mouseArea.containsMouse ? 0.1 : 0 radius: parent.radius @@ -213,9 +213,9 @@ Item { // Empty state Rectangle { anchors.fill: parent - color: Colors.backgroundSecondary + color: Colors.colorSurface radius: Style.radiusMedium * scaling - border.color: Colors.outline + border.color: Colors.colorOutline border.width: Math.max(1, Style.borderThin * scaling) visible: folderModel.count === 0 && !Wallpapers.scanning @@ -227,20 +227,20 @@ Item { text: "folder_open" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeLarge * scaling - color: Colors.textSecondary + color: Colors.colorOnSurface Layout.alignment: Qt.AlignHCenter } NText { text: "No wallpapers found" - color: Colors.textSecondary + color: Colors.colorOnSurface font.weight: Style.fontWeightBold Layout.alignment: Qt.AlignHCenter } NText { text: "Make sure your wallpaper directory is configured and contains image files" - color: Colors.textSecondary + color: Colors.colorOnSurface wrapMode: Text.WordWrap horizontalAlignment: Text.AlignHCenter Layout.preferredWidth: 300 * scaling diff --git a/Modules/SidePanel/Cards/MediaCard.qml b/Modules/SidePanel/Cards/MediaCard.qml index 6a59f24..26aa766 100644 --- a/Modules/SidePanel/Cards/MediaCard.qml +++ b/Modules/SidePanel/Cards/MediaCard.qml @@ -39,13 +39,13 @@ NBox { NText { text: "album" font.family: "Material Symbols Outlined" - font.pointSize: 28 * scaling - color: Colors.textSecondary + font.pointSize: Style.fontSizeXXL * 2.5 * scaling + color: Colors.colorOnSurfaceVariant Layout.alignment: Qt.AlignHCenter } NText { text: "No media player detected" - color: Colors.textDisabled + color: Colors.colorOnSurfaceVariant Layout.alignment: Qt.AlignHCenter } @@ -76,7 +76,7 @@ NBox { // implicitWidth: 120 * scaling // implicitHeight: 30 * scaling color: "transparent" - border.color: playerSelector.activeFocus ? Colors.hover : Colors.outline + border.color: playerSelector.activeFocus ? Colors.colorTertiary : Colors.colorOutline border.width: Math.max(1, Style.borderThin * scaling) radius: Style.radiusMedium * scaling } @@ -87,7 +87,7 @@ NBox { rightPadding: playerSelector.indicator.width + playerSelector.spacing text: playerSelector.displayText font.pointSize: Style.fontSizeSmall * scaling - color: Colors.textPrimary + color: Colors.colorOnSurface verticalAlignment: Text.AlignVCenter elide: Text.ElideRight } @@ -98,7 +98,7 @@ NBox { text: "arrow_drop_down" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXL * scaling - color: Colors.textPrimary + color: Colors.colorOnSurface horizontalAlignment: Text.AlignRight } @@ -122,14 +122,14 @@ NBox { gradient: Gradient { GradientStop { position: 0.0 - color: Colors.backgroundSecondary + color: Colors.colorSurface } GradientStop { position: 1.0 - color: Colors.backgroundTertiary + color: Colors.colorSurfaceVariant } } - border.color: Colors.outline + border.color: Colors.colorOutline border.width: Math.max(1, Style.borderThin * scaling) radius: Style.radiusTiny * scaling } @@ -140,7 +140,7 @@ NBox { contentItem: NText { text: modelData.identity font.pointSize: Style.fontSizeSmall * scaling - color: highlighted ? Colors.backgroundPrimary : Colors.textPrimary + color: highlighted ? Colors.colorSurface : Colors.colorOnSurface verticalAlignment: Text.AlignVCenter elide: Text.ElideRight } @@ -148,7 +148,7 @@ NBox { background: Rectangle { width: popup.width - Style.marginSmall * scaling * 2 - color: highlighted ? Colors.hover : "transparent" + color: highlighted ? Colors.colorTertiary : "transparent" radius: Style.radiusTiny * scaling } } @@ -169,8 +169,8 @@ NBox { width: 90 * scaling height: 90 * scaling radius: width * 0.5 - color: trackArt.visible ? Colors.accentPrimary : "transparent" - border.color: trackArt.visible ? Colors.outline : "transparent" + color: trackArt.visible ? Colors.colorPrimary : "transparent" + border.color: trackArt.visible ? Colors.colorOutline : "transparent" border.width: Math.max(1, Style.borderThin * scaling) clip: true @@ -182,7 +182,7 @@ NBox { anchors.margins: Style.marginTiny * scaling imagePath: MediaPlayer.trackArtUrl fallbackIcon: "image" - borderColor: Colors.outline + borderColor: Colors.colorOutline borderWidth: Math.max(1, Style.borderThin * scaling) imageRadius: width * 0.5 } @@ -217,7 +217,7 @@ NBox { NText { visible: MediaPlayer.trackArtist !== "" text: MediaPlayer.trackArtist - color: Colors.textSecondary + color: Colors.colorOnSurface font.pointSize: Style.fontSizeSmall * scaling elide: Text.ElideRight Layout.fillWidth: true @@ -226,7 +226,7 @@ NBox { NText { visible: MediaPlayer.trackAlbum !== "" text: MediaPlayer.trackAlbum - color: Colors.textSecondary + color: Colors.colorOnSurface font.pointSize: Style.fontSizeSmall * scaling elide: Text.ElideRight Layout.fillWidth: true @@ -241,7 +241,7 @@ NBox { width: parent.width height: 4 * scaling radius: Style.radiusSmall * scaling - color: Colors.backgroundTertiary + color: Colors.colorSurfaceVariant Layout.fillWidth: true property real progressRatio: { @@ -256,7 +256,7 @@ NBox { width: progressBarBackground.progressRatio * parent.width height: parent.height radius: parent.radius - color: Colors.accentPrimary + color: Colors.colorPrimary Behavior on width { NumberAnimation { @@ -271,8 +271,8 @@ NBox { width: 16 * scaling height: 16 * scaling radius: width * 0.5 - color: Colors.accentPrimary - border.color: Colors.backgroundPrimary + color: Colors.colorPrimary + border.color: Colors.colorSurface border.width: Math.max(1 * Style.borderMedium * scaling) x: Math.max(0, Math.min(parent.width - width, progressFill.width - width / 2)) @@ -345,7 +345,7 @@ NBox { width: 300 * scaling height: 80 * scaling values: Cava.values - fillColor: Colors.textPrimary + fillColor: Colors.colorOnSurface Layout.alignment: Qt.AlignHCenter } } @@ -355,8 +355,8 @@ NBox { // values: Cava.values // innerRadius: 30 * scaling // Position just outside 60x60 album art // outerRadius: 48 * scaling // Extend bars outward from album art - // fillColor: Colors.accentPrimary - // strokeColor: Colors.accentPrimary + // fillColor: Colors.colorPrimary + // strokeColor: Colors.colorPrimary // strokeWidth: 0 * scaling // } } diff --git a/Modules/SidePanel/Cards/ProfileCard.qml b/Modules/SidePanel/Cards/ProfileCard.qml index 39d5b6f..ec74814 100644 --- a/Modules/SidePanel/Cards/ProfileCard.qml +++ b/Modules/SidePanel/Cards/ProfileCard.qml @@ -33,7 +33,7 @@ NBox { height: Style.baseWidgetSize * 1.25 * scaling imagePath: Settings.data.general.avatarImage fallbackIcon: "person" - borderColor: Colors.accentPrimary + borderColor: Colors.colorPrimary borderWidth: Math.max(1, Style.borderMedium * scaling) } @@ -46,7 +46,7 @@ NBox { } NText { text: `System Uptime: ${uptimeText}` - color: Colors.textSecondary + color: Colors.colorOnSurface } } diff --git a/Modules/SidePanel/Cards/WeatherCard.qml b/Modules/SidePanel/Cards/WeatherCard.qml index 040b046..3eab860 100644 --- a/Modules/SidePanel/Cards/WeatherCard.qml +++ b/Modules/SidePanel/Cards/WeatherCard.qml @@ -30,7 +30,7 @@ NBox { text: weatherReady ? Location.weatherSymbolFromCode(Location.data.weather.current_weather.weathercode) : "" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXXL * 1.5 * scaling - color: Colors.accentPrimary + color: Colors.colorPrimary } ColumnLayout { @@ -91,13 +91,13 @@ NBox { spacing: Style.marginSmall * scaling NText { text: Qt.formatDateTime(new Date(Location.data.weather.daily.time[index]), "ddd") - color: Colors.textPrimary + color: Colors.colorOnSurface } NText { text: Location.weatherSymbolFromCode(Location.data.weather.daily.weathercode[index]) font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXL * scaling - color: Colors.accentPrimary + color: Colors.colorPrimary } NText { text: { @@ -112,7 +112,7 @@ NBox { return `${max}°/${min}°` } font.pointSize: Style.fontSizeSmall * scaling - color: Colors.textSecondary + color: Colors.colorOnSurfaceVariant } } } diff --git a/Modules/SidePanel/PowerMenu.qml b/Modules/SidePanel/PowerMenu.qml index 2815fc5..1d9afe0 100644 --- a/Modules/SidePanel/PowerMenu.qml +++ b/Modules/SidePanel/PowerMenu.qml @@ -25,16 +25,16 @@ NPanel { width: 160 * scaling height: 220 * scaling radius: Style.radiusMedium * scaling - border.color: Colors.outline + border.color: Colors.colorOutline border.width: Math.max(1, Style.borderThin * scaling) gradient: Gradient { GradientStop { position: 0.0 - color: Colors.backgroundSecondary + color: Colors.colorSurface } GradientStop { position: 1.0 - color: Colors.backgroundTertiary + color: Colors.colorSurfaceVariant } } @@ -65,7 +65,7 @@ NPanel { Layout.fillWidth: true Layout.preferredHeight: 36 * scaling radius: Style.radiusSmall * scaling - color: lockButtonArea.containsMouse ? Colors.hover : "transparent" + color: lockButtonArea.containsMouse ? Colors.colorTertiary : "transparent" Item { anchors.left: parent.left @@ -88,7 +88,7 @@ NPanel { font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 } - color: lockButtonArea.containsMouse ? Colors.textPrimary : Colors.textPrimary + color: lockButtonArea.containsMouse ? Colors.colorOnSurface : Colors.colorOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -96,7 +96,7 @@ NPanel { Text { text: "Lock Screen" - color: lockButtonArea.containsMouse ? Colors.textPrimary : Colors.textPrimary + color: lockButtonArea.containsMouse ? Colors.colorOnSurface : Colors.colorOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -125,7 +125,7 @@ NPanel { Layout.fillWidth: true Layout.preferredHeight: 36 * scaling radius: Style.radiusSmall * scaling - color: suspendButtonArea.containsMouse ? Colors.hover : "transparent" + color: suspendButtonArea.containsMouse ? Colors.colorTertiary : "transparent" Item { anchors.left: parent.left @@ -148,7 +148,7 @@ NPanel { font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 } - color: suspendButtonArea.containsMouse ? Colors.textPrimary : Colors.textPrimary + color: suspendButtonArea.containsMouse ? Colors.colorOnSurface : Colors.colorOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -156,7 +156,7 @@ NPanel { Text { text: "Suspend" - color: suspendButtonArea.containsMouse ? Colors.textPrimary : Colors.textPrimary + color: suspendButtonArea.containsMouse ? Colors.colorOnSurface : Colors.colorOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -183,7 +183,7 @@ NPanel { Layout.fillWidth: true Layout.preferredHeight: 36 * scaling radius: Style.radiusSmall * scaling - color: rebootButtonArea.containsMouse ? Colors.hover : "transparent" + color: rebootButtonArea.containsMouse ? Colors.colorTertiary : "transparent" Item { anchors.left: parent.left @@ -206,7 +206,7 @@ NPanel { font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 } - color: rebootButtonArea.containsMouse ? Colors.textPrimary : Colors.textPrimary + color: rebootButtonArea.containsMouse ? Colors.colorOnSurface : Colors.colorOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -214,7 +214,7 @@ NPanel { Text { text: "Reboot" - color: rebootButtonArea.containsMouse ? Colors.textPrimary : Colors.textPrimary + color: rebootButtonArea.containsMouse ? Colors.colorOnSurface : Colors.colorOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -241,7 +241,7 @@ NPanel { Layout.fillWidth: true Layout.preferredHeight: 36 * scaling radius: Style.radiusSmall * scaling - color: logoutButtonArea.containsMouse ? Colors.hover : "transparent" + color: logoutButtonArea.containsMouse ? Colors.colorTertiary : "transparent" Item { anchors.left: parent.left @@ -264,7 +264,7 @@ NPanel { font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 } - color: logoutButtonArea.containsMouse ? Colors.textPrimary : Colors.textPrimary + color: logoutButtonArea.containsMouse ? Colors.colorOnSurface : Colors.colorOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -272,7 +272,7 @@ NPanel { Text { text: "Logout" - color: logoutButtonArea.containsMouse ? Colors.textPrimary : Colors.textPrimary + color: logoutButtonArea.containsMouse ? Colors.colorOnSurface : Colors.colorOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -299,7 +299,7 @@ NPanel { Layout.fillWidth: true Layout.preferredHeight: 36 * scaling radius: Style.radiusSmall * scaling - color: shutdownButtonArea.containsMouse ? Colors.hover : "transparent" + color: shutdownButtonArea.containsMouse ? Colors.colorTertiary : "transparent" Item { anchors.left: parent.left @@ -322,7 +322,7 @@ NPanel { font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 } - color: shutdownButtonArea.containsMouse ? Colors.textPrimary : Colors.textPrimary + color: shutdownButtonArea.containsMouse ? Colors.colorOnSurface : Colors.colorOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -330,7 +330,7 @@ NPanel { Text { text: "Shutdown" - color: shutdownButtonArea.containsMouse ? Colors.textPrimary : Colors.textPrimary + color: shutdownButtonArea.containsMouse ? Colors.colorOnSurface : Colors.colorOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml index 97c267a..7c3aec2 100644 --- a/Modules/SidePanel/SidePanel.qml +++ b/Modules/SidePanel/SidePanel.qml @@ -81,20 +81,9 @@ NLoader { // Inline helpers moved to dedicated widgets: NCard and NCircleStat Rectangle { id: panelBackground - color: "transparent" - gradient: Gradient { - GradientStop { - position: 0.0 - color: Colors.backgroundSecondary - } - GradientStop { - position: 1.0 - color: Colors.backgroundTertiary - } - } - + color: Colors.colorSurface radius: Style.radiusLarge * scaling - border.color: Colors.backgroundTertiary + border.color: Colors.colorOutline border.width: Math.max(1, Style.borderMedium * scaling) layer.enabled: true width: 460 * scaling diff --git a/Services/Colors.qml b/Services/Colors.qml index 45afb9b..fa4230d 100644 --- a/Services/Colors.qml +++ b/Services/Colors.qml @@ -5,117 +5,162 @@ import Quickshell import Quickshell.Io import qs.Services +// -------------------------------- +// Material3 Colors +// We only use a subset of all materials colors to avoid complexity Singleton { id: root - // Backgrounds - property color backgroundPrimary: useMatugen ? matugenTheme.backgroundPrimary : defaultTheme.backgroundPrimary - property color backgroundSecondary: useMatugen ? matugenTheme.backgroundSecondary : defaultTheme.backgroundSecondary - property color backgroundTertiary: useMatugen ? matugenTheme.backgroundTertiary : defaultTheme.backgroundTertiary + // --- Key Colors + property color colorPrimary: useMatugen ? matugenTheme.colorPrimary : defaultTheme.colorPrimary + property color colorOnPrimary: useMatugen ? matugenTheme.colorOnPrimary : defaultTheme.colorOnPrimary + property color colorPrimaryContainer: useMatugen ? matugenTheme.colorPrimaryContainer : defaultTheme.colorPrimaryContainer + property color colorOnPrimaryContainer: useMatugen ? matugenTheme.colorOnPrimaryContainer : defaultTheme.colorOnPrimaryContainer - // Surfaces & Elevation - property color surface: useMatugen ? matugenTheme.surface : defaultTheme.surface - property color surfaceVariant: useMatugen ? matugenTheme.surfaceVariant : defaultTheme.surfaceVariant + property color colorSecondary: useMatugen ? matugenTheme.colorSecondary : defaultTheme.colorSecondary + property color colorOnSecondary: useMatugen ? matugenTheme.colorOnSecondary : defaultTheme.colorOnSecondary + property color colorSecondaryContainer: useMatugen ? matugenTheme.colorSecondaryContainer : defaultTheme.colorSecondaryContainer + property color colorOnSecondaryContainer: useMatugen ? matugenTheme.colorOnSecondaryContainer : defaultTheme.colorOnSecondaryContainer - // Text Colors - property color textPrimary: useMatugen ? matugenTheme.textPrimary : defaultTheme.textPrimary - property color textSecondary: useMatugen ? matugenTheme.textSecondary : defaultTheme.textSecondary - property color textDisabled: useMatugen ? matugenTheme.textDisabled : defaultTheme.textDisabled + property color colorTertiary: useMatugen ? matugenTheme.colorTertiary : defaultTheme.colorTertiary + property color colorOnTertiary: useMatugen ? matugenTheme.colorOnTertiary : defaultTheme.colorOnTertiary + property color colorTertiaryContainer: useMatugen ? matugenTheme.colorTertiaryContainer : defaultTheme.colorTertiaryContainer + property color colorOnTertiaryContainer: useMatugen ? matugenTheme.colorOnTertiaryContainer : defaultTheme.colorOnTertiaryContainer - // Accent Colors - property color accentPrimary: useMatugen ? matugenTheme.accentPrimary : defaultTheme.accentPrimary - property color accentSecondary: useMatugen ? matugenTheme.accentSecondary : defaultTheme.accentSecondary - property color accentTertiary: useMatugen ? matugenTheme.accentTertiary : defaultTheme.accentTertiary + // --- Utility Colors + property color colorError: useMatugen ? matugenTheme.colorError : defaultTheme.colorError + property color colorOnError: useMatugen ? matugenTheme.colorOnError : defaultTheme.colorOnError + property color colorErrorContainer: useMatugen ? matugenTheme.colorErrorContainer : defaultTheme.colorErrorContainer + property color colorOnErrorContainer: useMatugen ? matugenTheme.colorOnErrorContainer : defaultTheme.colorOnErrorContainer - // Error/Warning - property color error: useMatugen ? matugenTheme.error : defaultTheme.error - property color warning: useMatugen ? matugenTheme.warning : defaultTheme.warning + // --- Surface and Variant Colors + property color colorSurface: useMatugen ? matugenTheme.colorSurface : defaultTheme.colorSurface + property color colorOnSurface: useMatugen ? matugenTheme.colorOnSurface : defaultTheme.colorOnSurface + property color colorSurfaceVariant: useMatugen ? matugenTheme.colorSurfaceVariant : defaultTheme.colorSurfaceVariant + property color colorOnSurfaceVariant: useMatugen ? matugenTheme.colorOnSurfaceVariant : defaultTheme.colorOnSurfaceVariant + property color colorInversePrimary: useMatugen ? matugenTheme.colorInversePrimary : defaultTheme.colorInversePrimary + property color colorOutline: useMatugen ? matugenTheme.colorOutline : defaultTheme.colorOutline + property color colorOutlineVariant: useMatugen ? matugenTheme.colorOutlineVariant : defaultTheme.colorOutlineVariant + property color colorShadow: useMatugen ? matugenTheme.colorShadow : defaultTheme.colorShadow - // Hover - property color hover: useMatugen ? matugenTheme.hover : defaultTheme.hover - - // Additional Theme Properties - property color onAccent: useMatugen ? matugenTheme.onAccent : defaultTheme.onAccent - property color outline: useMatugen ? matugenTheme.outline : defaultTheme.outline - - // Shadows & Overlays - property color shadow: applyOpacity(useMatugen ? matugenTheme.shadow : defaultTheme.shadow, "B3") - property color overlay: applyOpacity(useMatugen ? matugenTheme.overlay : defaultTheme.overlay, "66") + // ----------- // Check if we should use Matugen theme property bool useMatugen: Settings.data.wallpaper.generateTheme && matugenFile.loaded + // ----------- function applyOpacity(color, opacity) { // Convert color to string and apply opacity return color.toString().replace("#", "#" + opacity) } - // Default theme colors + // -------------------------------- + // Default theme colors - RosePine QtObject { id: defaultTheme - property color backgroundPrimary: "#191724" - property color backgroundSecondary: "#1f1d2e" - property color backgroundTertiary: "#26233a" + // // --- Key Colors: These are the main accent colors that define your app's theme. + property color colorPrimary: "#000000" // The main brand color, used most frequently. + property color colorOnPrimary: "#000000" // Color for text/icons on a Primary background. + property color colorPrimaryContainer: "#000000" // A lighter/subtler tone of Primary, used for component backgrounds. + property color colorOnPrimaryContainer: "#000000" // Color for text/icons on a Primary Container background. - property color surface: "#1b1927" - property color surfaceVariant: "#262337" + property color colorSecondary: "#000000" // An accent color for less prominent components. + property color colorOnSecondary: "#000000" // Color for text/icons on a Secondary background. + property color colorSecondaryContainer: "#000000" // A lighter/subtler tone of Secondary. + property color colorOnSecondaryContainer: "#000000" // olor for text/icons on a Secondary Container background. - property color textPrimary: "#e0def4" - property color textSecondary: "#908caa" - property color textDisabled: "#6e6a86" + property color colorTertiary: "#000000" // A contrasting accent color used for things like highlights or special actions. + property color colorOnTertiary: "#000000" // Color for text/icons on a Tertiary background. + property color colorTertiaryContainer: "#000000" // A lighter/subtler tone of Tertiary. + property color colorOnTertiaryContainer: "#000000" // Color for text/icons on a Tertiary Container background. - property color accentPrimary: "#ebbcba" - property color accentSecondary: "#31748f" - property color accentTertiary: "#9ccfd8" + // --- Utility colorColors: These colors serve specific, universal purposes like indicating errors or providing neutral backgrounds. + property color colorError: "#000000" // Indicates an error state. + property color colorOnError: "#000000" // Color for text/icons on an Error background. + property color colorErrorContainer: "#000000" // A lighter/subtler tone of Error. + property color colorOnErrorContainer: "#000000" // Color for text/icons on an Error Container background. - property color error: "#eb6f92" - property color warning: "#f6c177" + // --- Surface colorand Variant Colors: These provide additional options for surfaces and their contents, creating visual hierarchy. + property color colorSurface: "#000000" // The color for component surfaces like cards, sheets, and menus. + property color colorOnSurface: "#000000" // The primary color for text/icons on a Surface background. + property color colorSurfaceVariant: "#000000" // A surface color with a slightly different tint for differentiation. + property color colorOnSurfaceVariant: "#000000" // The color for less prominent text/icons on a Surface. + property color colorInversePrimary: "#000000" // A primary color legible on an Inverse Surface, often used for call-to-action buttons. + property color colorOutline: "#000000" // The color for component outlines, like text fields or buttons. + property color colorOutlineVariant: "#000000" // A subtler outline color for decorative elements or dividers. + property color colorShadow: "#000000" // The color used for shadows to create elevation. - property color hover: "#c4a7e7" - property color onAccent: "#191724" - property color outline: "#44415a" + // // property color colorBackground: "#191724" + // // property color colorSurface: "#1f1d2e" + // // property color colorSurfaceVariant: "#26233a" - property color shadow: "#191724" - property color overlay: "#191724" + // // property color surface: "#1b1927" + // // property color surfaceVariant: "#262337" + + // // property color colorOnBackground: "#e0def4" + // // property color textSecondary: "#908caa" + // // property color textDisabled: "#6e6a86" + + // // property color colorPrimary: "#ebbcba" + // // property color accentSecondary: "#31748f" + // // property color accentTertiary: "#9ccfd8" + + // // property color error: "#eb6f92" + // // property color warning: "#f6c177" + + // // property color hover: "#c4a7e7" + + // // property color onAccent: "#191724" + // // property color outline: "#44415a" + + // // property color shadow: "#191724" + // // property color overlay: "#191724" } + // ---------------------------------------------------------------- // Matugen theme colors (loaded from theme.json) QtObject { id: matugenTheme - property color backgroundPrimary: matugenData.backgroundPrimary - property color backgroundSecondary: matugenData.backgroundSecondary - property color backgroundTertiary: matugenData.backgroundTertiary + // --- Key Colors + property color colorPrimary: matugenData.colorPrimary + property color colorOnPrimary: matugenData.colorOnPrimary + property color colorPrimaryContainer: matugenData.colorPrimaryContainer + property color colorOnPrimaryContainer: matugenData.colorOnPrimaryContainer - property color surface: matugenData.surface - property color surfaceVariant: matugenData.surfaceVariant + property color colorSecondary: matugenData.colorSecondary + property color colorOnSecondary: matugenData.colorOnSecondary + property color colorSecondaryContainer: matugenData.colorSecondaryContainer + property color colorOnSecondaryContainer: matugenData.colorOnSecondaryContainer - property color textPrimary: matugenData.textPrimary - property color textSecondary: matugenData.textSecondary - property color textDisabled: matugenData.textDisabled + property color colorTertiary: matugenData.colorTertiary + property color colorOnTertiary: matugenData.colorOnTertiary + property color colorTertiaryContainer: matugenData.colorTertiaryContainer + property color colorOnTertiaryContainer: matugenData.colorOnTertiaryContainer - property color accentPrimary: matugenData.accentPrimary - property color accentSecondary: matugenData.accentSecondary - property color accentTertiary: matugenData.accentTertiary + // --- Utility Colors + property color colorError: matugenData.colorError + property color colorOnError: matugenData.colorOnError + property color colorErrorContainer: matugenData.colorErrorContainer + property color colorOnErrorContainer: matugenData.colorOnErrorContainer - property color error: matugenData.error - property color warning: matugenData.warning - - property color hover: matugenData.hover - - property color onAccent: matugenData.onAccent - property color outline: matugenData.outline - - property color shadow: matugenData.shadow - property color overlay: matugenData.overlay + // --- Surface and Variant Colors + property color colorSurface: matugenData.colorSurface + property color colorOnSurface: matugenData.colorOnSurface + property color colorSurfaceVariant: matugenData.colorSurfaceVariant + property color colorOnSurfaceVariant: matugenData.colorOnSurfaceVariant + property color colorInversePrimary: matugenData.colorInversePrimary + property color colorOutline: matugenData.colorOutline + property color colorOutlineVariant: matugenData.colorOutlineVariant + property color colorShadow: matugenData.colorShadow } - // FileView to load Matugen theme data from Theme.json + // FileView to load Matugen theme data from colors.json FileView { id: matugenFile - path: Settings.configDir + "theme.json" + path: Settings.configDir + "colors.json" watchChanges: true onFileChanged: reload() onAdapterUpdated: writeAdapter() @@ -128,39 +173,37 @@ Singleton { JsonAdapter { id: matugenData - // Backgrounds - property string backgroundPrimary: "#191724" - property string backgroundSecondary: "#1f1d2e" - property string backgroundTertiary: "#26233a" + // --- Key Colors + property color colorPrimary: defaultTheme.colorPrimary + property color colorOnPrimary: defaultTheme.colorOnPrimary + property color colorPrimaryContainer: defaultTheme.colorPrimaryContainer + property color colorOnPrimaryContainer: defaultTheme.colorOnPrimaryContainer - // Surfaces & Elevation - property string surface: "#1b1927" - property string surfaceVariant: "#262337" + property color colorSecondary: defaultTheme.colorSecondary + property color colorOnSecondary: defaultTheme.colorOnSecondary + property color colorSecondaryContainer: defaultTheme.colorSecondaryContainer + property color colorOnSecondaryContainer: defaultTheme.colorOnSecondaryContainer - // Text Colors - property string textPrimary: "#e0def4" - property string textSecondary: "#908caa" - property string textDisabled: "#6e6a86" + property color colorTertiary: defaultTheme.colorTertiary + property color colorOnTertiary: defaultTheme.colorOnTertiary + property color colorTertiaryContainer: defaultTheme.colorTertiaryContainer + property color colorOnTertiaryContainer: defaultTheme.colorOnTertiaryContainer - // Accent Colors - property string accentPrimary: "#ebbcba" - property string accentSecondary: "#31748f" - property string accentTertiary: "#9ccfd8" + // --- Utility Colors + property color colorError: defaultTheme.colorError + property color colorOnError: defaultTheme.colorOnError + property color colorErrorContainer: defaultTheme.colorErrorContainer + property color colorOnErrorContainer: defaultTheme.colorOnErrorContainer - // Error/Warning - property string error: "#eb6f92" - property string warning: "#f6c177" - - // Hover - property string hover: "#c4a7e7" - - // Additional Theme Properties - property string onAccent: "#191724" - property string outline: "#44415a" - - // Shadows & Overlays - property string shadow: "#191724" - property string overlay: "#191724" + // --- Surface and Variant Colors + property color colorSurface: defaultTheme.colorSurface + property color colorOnSurface: defaultTheme.colorOnSurface + property color colorSurfaceVariant: defaultTheme.colorSurfaceVariant + property color colorOnSurfaceVariant: defaultTheme.colorOnSurfaceVariant + property color colorInversePrimary: defaultTheme.colorInversePrimary + property color colorOutline: defaultTheme.colorOutline + property color colorOutlineVariant: defaultTheme.colorOutlineVariant + property color colorShadow: defaultTheme.colorShadow } } } diff --git a/Services/Settings.qml b/Services/Settings.qml index ff78fc1..7bab6c0 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -18,7 +18,6 @@ Singleton { "HOME") + "/.cache") + "/" + shellName + "/" property string settingsFile: Quickshell.env("NOCTALIA_SETTINGS_FILE") || (configDir + "settings.json") - property string colorsFile: Quickshell.env("NOCTALIA_COLORS_FILE") || (configDir + "colors.json") property string defaultWallpaper: Qt.resolvedUrl("../Assets/Tests/wallpaper.png") property string defaultAvatar: Quickshell.env("HOME") + "/.face" diff --git a/Services/Wallpapers.qml b/Services/Wallpapers.qml index 0b3201f..74dc578 100644 --- a/Services/Wallpapers.qml +++ b/Services/Wallpapers.qml @@ -155,13 +155,12 @@ Singleton { Process { id: generateThemeProcess - command: [Quickshell.shellDir + "/Bin/matugen-theme.sh", currentWallpaper] + command: ["matugen", "image", currentWallpaper, "--config", Quickshell.shellDir + "/Assets/Matugen/matugen.toml"] workingDirectory: Quickshell.shellDir running: false stdout: StdioCollector { onStreamFinished: { - - // console.log(this.text) + //console.log(this.text) } } } diff --git a/Widgets/NBox.qml b/Widgets/NBox.qml index b75d79c..310f1fe 100644 --- a/Widgets/NBox.qml +++ b/Widgets/NBox.qml @@ -11,9 +11,9 @@ Rectangle { implicitWidth: childrenRect.width implicitHeight: childrenRect.height - color: Colors.surface + color: Colors.colorSurfaceVariant radius: Style.radiusMedium * scaling - border.color: Colors.backgroundTertiary + border.color: Colors.colorOutlineVariant border.width: Math.max(1, Style.borderThin * scaling) clip: true } diff --git a/Widgets/NBusyIndicator.qml b/Widgets/NBusyIndicator.qml index d405449..3164fe9 100644 --- a/Widgets/NBusyIndicator.qml +++ b/Widgets/NBusyIndicator.qml @@ -7,7 +7,7 @@ Item { readonly property real scaling: Scaling.scale(screen) property bool running: true - property color color: Colors.accentPrimary + property color color: Colors.colorPrimary property int size: Style.baseWidgetSize * scaling property int strokeWidth: Style.borderThick * scaling property int duration: 1000 diff --git a/Widgets/NCard.qml b/Widgets/NCard.qml index bc2bb40..3361305 100644 --- a/Widgets/NCard.qml +++ b/Widgets/NCard.qml @@ -10,8 +10,8 @@ Rectangle { implicitWidth: childrenRect.width implicitHeight: childrenRect.height - color: Colors.backgroundSecondary + color: Colors.colorSurface radius: Style.radiusMedium * scaling - border.color: Colors.backgroundTertiary + border.color: Colors.colorSurfaceVariant border.width: Math.max(1, Style.borderThin * scaling) } diff --git a/Widgets/NCircleStat.qml b/Widgets/NCircleStat.qml index 80bec42..4909d62 100644 --- a/Widgets/NCircleStat.qml +++ b/Widgets/NCircleStat.qml @@ -18,9 +18,9 @@ Rectangle { width: 68 * scaling height: 92 * scaling - color: flat ? "transparent" : Colors.backgroundSecondary + color: flat ? "transparent" : Colors.colorSurface radius: Style.radiusSmall * scaling - border.color: flat ? "transparent" : Colors.backgroundTertiary + border.color: flat ? "transparent" : Colors.colorSurfaceVariant border.width: flat ? 0 : Math.max(1, Style.borderThin * scaling) clip: true @@ -58,14 +58,14 @@ Rectangle { ctx.reset() ctx.lineWidth = 6 * root.scaling * contentScale // Track uses surfaceVariant for stronger contrast - ctx.strokeStyle = Colors.surfaceVariant + ctx.strokeStyle = Colors.colorSurface ctx.beginPath() ctx.arc(cx, cy, r, start, endBg) ctx.stroke() // Value arc const ratio = Math.max(0, Math.min(1, root.value / 100)) const end = start + (endBg - start) * ratio - ctx.strokeStyle = Colors.accentPrimary + ctx.strokeStyle = Colors.colorPrimary ctx.beginPath() ctx.arc(cx, cy, r, start, end) ctx.stroke() @@ -79,7 +79,7 @@ Rectangle { text: `${root.value}${root.suffix}` font.pointSize: Style.fontSizeMedium * scaling * contentScale font.weight: Style.fontWeightBold - color: Colors.textPrimary + color: Colors.colorOnSurface horizontalAlignment: Text.AlignHCenter } @@ -89,8 +89,8 @@ Rectangle { width: 28 * scaling * contentScale height: width radius: width / 2 - color: Colors.surfaceVariant - // border.color: Colors.accentPrimary + color: Colors.colorSurface + // border.color: Colors.colorPrimary // border.width: Math.max(1, Style.borderThin * scaling) anchors.right: parent.right anchors.top: parent.top @@ -102,7 +102,7 @@ Rectangle { text: root.icon font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeLargeXL * scaling * contentScale - color: Colors.textSecondary + color: Colors.colorOnSurface horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } diff --git a/Widgets/NComboBox.qml b/Widgets/NComboBox.qml index e34e9f0..ddc8d85 100644 --- a/Widgets/NComboBox.qml +++ b/Widgets/NComboBox.qml @@ -29,13 +29,13 @@ ColumnLayout { text: label font.pointSize: Style.fontSizeMedium * scaling font.weight: Style.fontWeightBold - color: Colors.textPrimary + color: Colors.colorOnSurface } NText { text: description font.pointSize: Style.fontSizeSmall * scaling - color: Colors.textSecondary + color: Colors.colorOnSurface wrapMode: Text.WordWrap } } @@ -56,8 +56,8 @@ ColumnLayout { background: Rectangle { implicitWidth: 120 * scaling implicitHeight: preferredHeight - color: Colors.surfaceVariant - border.color: combo.activeFocus ? Colors.hover : Colors.outline + color: Colors.colorSurfaceVariant + border.color: combo.activeFocus ? Colors.colorTertiary : Colors.colorOutline border.width: Math.max(1, Style.borderThin * scaling) radius: Style.radiusMedium * scaling } @@ -97,8 +97,8 @@ ColumnLayout { } background: Rectangle { - color: Colors.surfaceVariant - border.color: Colors.outline + color: Colors.colorSurfaceVariant + border.color: Colors.colorOutline border.width: Math.max(1, Style.borderThin * scaling) radius: Style.radiusMedium * scaling } @@ -112,14 +112,14 @@ ColumnLayout { text: (combo.model.indexOf(modelData) >= 0 && combo.model.indexOf( modelData) < root.optionsLabels.length) ? root.optionsLabels[combo.model.indexOf(modelData)] : "" font.pointSize: Style.fontSizeMedium * scaling - color: highlighted ? Colors.backgroundPrimary : Colors.textPrimary + color: highlighted ? Colors.colorSurface : Colors.colorOnSurface verticalAlignment: Text.AlignVCenter elide: Text.ElideRight } background: Rectangle { width: combo.width - Style.marginMedium * scaling * 3 - color: highlighted ? Colors.hover : "transparent" + color: highlighted ? Colors.colorTertiary : "transparent" radius: Style.radiusSmall * scaling } } diff --git a/Widgets/NDivider.qml b/Widgets/NDivider.qml index bd47353..5cb2ea0 100644 --- a/Widgets/NDivider.qml +++ b/Widgets/NDivider.qml @@ -6,5 +6,5 @@ import qs.Services Rectangle { width: parent.width height: Math.max(1, Style.borderThin * scaling) - color: Colors.outline + color: Colors.colorOutline } diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml index 28e0b6f..19580e3 100644 --- a/Widgets/NIconButton.qml +++ b/Widgets/NIconButton.qml @@ -26,9 +26,9 @@ Rectangle { implicitWidth: size implicitHeight: size - color: (root.hovering || showFilled) ? Colors.accentPrimary : "transparent" + color: (root.hovering || showFilled) ? Colors.colorPrimary : "transparent" radius: width * 0.5 - border.color: showBorder ? Colors.accentPrimary : "transparent" + border.color: showBorder ? Colors.colorPrimary : "transparent" border.width: Math.max(1, Style.borderThin * scaling) NText { @@ -42,7 +42,7 @@ Rectangle { font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 } - color: (root.hovering || showFilled) ? Colors.onAccent : showBorder ? Colors.accentPrimary : Colors.textPrimary + color: (root.hovering || showFilled) ? Colors.colorOnPrimary : showBorder ? Colors.colorPrimary : Colors.colorOnSurface horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter opacity: root.enabled ? Style.opacityFull : Style.opacityMedium diff --git a/Widgets/NPill.qml b/Widgets/NPill.qml index 05c3a3e..65a0752 100644 --- a/Widgets/NPill.qml +++ b/Widgets/NPill.qml @@ -10,11 +10,11 @@ Item { property string icon: "" property string text: "" property string tooltipText: "" - property color pillColor: Colors.surfaceVariant - property color textColor: Colors.textPrimary - property color iconCircleColor: Colors.accentPrimary - property color iconTextColor: Colors.backgroundPrimary - property color collapsedIconColor: Colors.textPrimary + property color pillColor: Colors.colorSurfaceVariant + property color textColor: Colors.colorOnSurface + property color iconCircleColor: Colors.colorPrimary + property color iconTextColor: Colors.colorSurface + property color collapsedIconColor: Colors.colorOnSurface property real sizeMultiplier: 0.8 property bool autoHide: false diff --git a/Widgets/NRadioButton.qml b/Widgets/NRadioButton.qml index 06e9098..14e6f49 100644 --- a/Widgets/NRadioButton.qml +++ b/Widgets/NRadioButton.qml @@ -13,7 +13,7 @@ RadioButton { implicitHeight: 20 * scaling radius: width * 0.5 color: "transparent" - border.color: root.checked ? Colors.accentPrimary : Colors.textPrimary + border.color: root.checked ? Colors.colorPrimary : Colors.colorOnSurface border.width: 2 anchors.verticalCenter: parent.verticalCenter @@ -23,7 +23,7 @@ RadioButton { implicitHeight: Style.marginSmall * scaling radius: width * 0.5 - color: Qt.alpha(Colors.accentPrimary, root.checked ? 1 : 0) + color: Qt.alpha(Colors.colorPrimary, root.checked ? 1 : 0) } Behavior on border.color { diff --git a/Widgets/NSlider.qml b/Widgets/NSlider.qml index 2e9e6d1..2a2ed96 100644 --- a/Widgets/NSlider.qml +++ b/Widgets/NSlider.qml @@ -27,13 +27,13 @@ Slider { width: root.availableWidth height: implicitHeight radius: height / 2 - color: Colors.surfaceVariant + color: Colors.colorSurfaceVariant Rectangle { id: activeTrack width: root.visualPosition * parent.width height: parent.height - color: Colors.accentPrimary + color: Colors.colorPrimary radius: parent.radius } @@ -43,7 +43,7 @@ Slider { width: knobDiameter + cutoutExtra height: knobDiameter + cutoutExtra radius: width / 2 - color: root.cutoutColor !== undefined ? root.cutoutColor : Colors.backgroundPrimary + color: root.cutoutColor !== undefined ? root.cutoutColor : Colors.colorSurface x: Math.max(0, Math.min(parent.width - width, root.visualPosition * (parent.width - root.knobDiameter) - cutoutExtra / 2)) y: (parent.height - height) / 2 @@ -61,7 +61,7 @@ Slider { anchors.fill: knob source: knob shadowEnabled: true - shadowColor: Colors.shadow + shadowColor: Colors.colorShadow shadowOpacity: 0.25 shadowHorizontalOffset: 0 shadowVerticalOffset: 1 @@ -73,8 +73,8 @@ Slider { implicitWidth: knobDiameter implicitHeight: knobDiameter radius: width * 0.5 - color: root.pressed ? Colors.surfaceVariant : Colors.surface - border.color: Colors.accentPrimary + color: root.pressed ? Colors.colorSurfaceVariant : Colors.colorSurface + border.color: Colors.colorPrimary border.width: Math.max(1, Style.borderThick * scaling) // Press feedback halo (using accent color, low opacity) @@ -83,7 +83,7 @@ Slider { width: parent.width + 8 * scaling height: parent.height + 8 * scaling radius: width / 2 - color: Colors.accentPrimary + color: Colors.colorPrimary opacity: root.pressed ? 0.16 : 0.0 } } diff --git a/Widgets/NText.qml b/Widgets/NText.qml index 0544aac..d410537 100644 --- a/Widgets/NText.qml +++ b/Widgets/NText.qml @@ -10,5 +10,5 @@ Text { font.family: Settings.data.ui.fontFamily font.pointSize: Style.fontSizeMedium * scaling font.weight: Style.fontWeightRegular - color: Colors.textPrimary + color: Colors.colorOnSurface } diff --git a/Widgets/NTextInput.qml b/Widgets/NTextInput.qml index 8a7de1a..e96afc6 100644 --- a/Widgets/NTextInput.qml +++ b/Widgets/NTextInput.qml @@ -29,13 +29,13 @@ Item { text: label font.pointSize: Style.fontSizeMedium * scaling font.weight: Style.fontWeightBold - color: Colors.textPrimary + color: Colors.colorOnSurface } NText { text: description font.pointSize: Style.fontSizeSmall * scaling - color: Colors.textSecondary + color: Colors.colorOnSurface wrapMode: Text.WordWrap Layout.fillWidth: true } @@ -47,8 +47,8 @@ Item { implicitWidth: root.width implicitHeight: Style.baseWidgetSize * 1.35 * scaling radius: Style.radiusMedium * scaling - color: Colors.surfaceVariant - border.color: Colors.outline + color: Colors.colorSurfaceVariant + border.color: Colors.colorOutline border.width: Math.max(1, Style.borderThin * scaling) // Focus ring @@ -56,7 +56,7 @@ Item { anchors.fill: parent radius: frame.radius color: "transparent" - border.color: input.activeFocus ? Colors.hover : "transparent" + border.color: input.activeFocus ? Colors.colorTertiary : "transparent" border.width: input.activeFocus ? Math.max(1, Style.borderThin * scaling) : 0 } @@ -74,8 +74,8 @@ Item { echoMode: TextInput.Normal readOnly: root.readOnly enabled: root.enabled - color: Colors.textPrimary - placeholderTextColor: Colors.textSecondary + color: Colors.colorOnSurface + placeholderTextColor: Colors.colorOnSurface background: null font.pointSize: Style.fontSizeSmall * scaling onEditingFinished: root.editingFinished() diff --git a/Widgets/NToggle.qml b/Widgets/NToggle.qml index 28c766c..7994229 100644 --- a/Widgets/NToggle.qml +++ b/Widgets/NToggle.qml @@ -27,13 +27,13 @@ RowLayout { text: label font.pointSize: Style.fontSizeMedium * scaling font.weight: Style.fontWeightBold - color: Colors.textPrimary + color: Colors.colorOnSurface } NText { text: description font.pointSize: Style.fontSizeSmall * scaling - color: Colors.textSecondary + color: Colors.colorOnSurface wrapMode: Text.WordWrap Layout.fillWidth: true } @@ -45,16 +45,16 @@ RowLayout { implicitWidth: root.baseSize * 1.625 * scaling implicitHeight: root.baseSize * scaling radius: height * 0.5 - color: value ? Colors.accentPrimary : Colors.surfaceVariant - border.color: value ? Colors.accentPrimary : Colors.outline + color: value ? Colors.colorPrimary : Colors.colorSurface + border.color: value ? Colors.colorPrimary : Colors.colorOutline border.width: Math.max(1, Style.borderMedium * scaling) Rectangle { - implicitWidth: (root.baseSize - 4) * scaling - implicitHeight: (root.baseSize - 4) * scaling + implicitWidth: (root.baseSize - 5) * scaling + implicitHeight: (root.baseSize - 5) * scaling radius: height * 0.5 - color: Colors.surface - border.color: hovering ? Colors.textDisabled : Colors.outline + color: value ? Colors.colorOnSurface : Colors.colorPrimary //Colors.onBackground : Colors.colorSecondary + border.color: value ? Colors.colorSurface: Colors.colorSurface border.width: Math.max(1, Style.borderMedium * scaling) y: 2 * scaling x: value ? switcher.width - width - 2 * scaling : 2 * scaling diff --git a/Widgets/NTooltip.qml b/Widgets/NTooltip.qml index a450eb9..3319083 100644 --- a/Widgets/NTooltip.qml +++ b/Widgets/NTooltip.qml @@ -135,17 +135,8 @@ Window { id: tooltipRect anchors.fill: parent radius: Style.radiusMedium * scaling - gradient: Gradient { - GradientStop { - position: 0.0 - color: Colors.backgroundSecondary - } - GradientStop { - position: 1.0 - color: Colors.backgroundTertiary - } - } - border.color: Colors.outline + color: Colors.colorSurface + border.color: Colors.colorOutline border.width: Math.max(1, Style.borderThin * scaling) z: 1 From 4b161facca30b55ffe17da3f00a8ae2a7d36d766 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Thu, 14 Aug 2025 18:21:02 -0400 Subject: [PATCH 279/394] formatting --- Modules/AppLauncher/AppLauncher.qml | 5 ++--- Modules/LockScreen/LockScreen.qml | 3 ++- Modules/Settings/SettingsPanel.qml | 1 - Modules/SidePanel/Cards/WeatherCard.qml | 2 +- Services/Colors.qml | 2 -- Services/Wallpapers.qml | 1 + Widgets/NIconButton.qml | 3 ++- Widgets/NToggle.qml | 4 ++-- 8 files changed, 10 insertions(+), 11 deletions(-) diff --git a/Modules/AppLauncher/AppLauncher.qml b/Modules/AppLauncher/AppLauncher.qml index 9120e4d..2e3f870 100644 --- a/Modules/AppLauncher/AppLauncher.qml +++ b/Modules/AppLauncher/AppLauncher.qml @@ -369,9 +369,8 @@ NLoader { height: 56 * scaling radius: 16 * scaling property bool isSelected: index === selectedIndex - color: (appCardArea.containsMouse || isSelected) ? Qt.darker( - Colors.colorPrimary, - 1.1) : Colors.colorSurface + color: (appCardArea.containsMouse || isSelected) ? Qt.darker(Colors.colorPrimary, + 1.1) : Colors.colorSurface border.color: (appCardArea.containsMouse || isSelected) ? Colors.colorPrimary : "transparent" border.width: (appCardArea.containsMouse || isSelected) ? 2 : 0 diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index 07c7077..ea50bc2 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -631,7 +631,8 @@ WlSessionLock { width: 64 * Scaling.scale(screen) height: 64 * Scaling.scale(screen) radius: 32 - color: Qt.rgba(Colors.colorError.r, Colors.colorError.g, Colors.colorError.b, shutdownArea.containsMouse ? 0.9 : 0.2) + color: Qt.rgba(Colors.colorError.r, Colors.colorError.g, Colors.colorError.b, + shutdownArea.containsMouse ? 0.9 : 0.2) border.color: Colors.colorError border.width: 2 * Scaling.scale(screen) diff --git a/Modules/Settings/SettingsPanel.qml b/Modules/Settings/SettingsPanel.qml index e2ae004..926a816 100644 --- a/Modules/Settings/SettingsPanel.qml +++ b/Modules/Settings/SettingsPanel.qml @@ -254,7 +254,6 @@ NLoader { "wght": (Font.Normal + Font.Bold) / 2.0 } font.pointSize: Style.fontSizeLarge * scaling - } // Tab label on the left side NText { diff --git a/Modules/SidePanel/Cards/WeatherCard.qml b/Modules/SidePanel/Cards/WeatherCard.qml index 3eab860..48eb790 100644 --- a/Modules/SidePanel/Cards/WeatherCard.qml +++ b/Modules/SidePanel/Cards/WeatherCard.qml @@ -112,7 +112,7 @@ NBox { return `${max}°/${min}°` } font.pointSize: Style.fontSizeSmall * scaling - color: Colors.colorOnSurfaceVariant + color: Colors.colorOnSurfaceVariant } } } diff --git a/Services/Colors.qml b/Services/Colors.qml index fa4230d..972f483 100644 --- a/Services/Colors.qml +++ b/Services/Colors.qml @@ -43,7 +43,6 @@ Singleton { property color colorOutlineVariant: useMatugen ? matugenTheme.colorOutlineVariant : defaultTheme.colorOutlineVariant property color colorShadow: useMatugen ? matugenTheme.colorShadow : defaultTheme.colorShadow - // ----------- // Check if we should use Matugen theme property bool useMatugen: Settings.data.wallpaper.generateTheme && matugenFile.loaded @@ -91,7 +90,6 @@ Singleton { property color colorOutlineVariant: "#000000" // A subtler outline color for decorative elements or dividers. property color colorShadow: "#000000" // The color used for shadows to create elevation. - // // property color colorBackground: "#191724" // // property color colorSurface: "#1f1d2e" // // property color colorSurfaceVariant: "#26233a" diff --git a/Services/Wallpapers.qml b/Services/Wallpapers.qml index 74dc578..58adb56 100644 --- a/Services/Wallpapers.qml +++ b/Services/Wallpapers.qml @@ -160,6 +160,7 @@ Singleton { running: false stdout: StdioCollector { onStreamFinished: { + //console.log(this.text) } } diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml index 19580e3..9566fcf 100644 --- a/Widgets/NIconButton.qml +++ b/Widgets/NIconButton.qml @@ -42,7 +42,8 @@ Rectangle { font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 } - color: (root.hovering || showFilled) ? Colors.colorOnPrimary : showBorder ? Colors.colorPrimary : Colors.colorOnSurface + color: (root.hovering + || showFilled) ? Colors.colorOnPrimary : showBorder ? Colors.colorPrimary : Colors.colorOnSurface horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter opacity: root.enabled ? Style.opacityFull : Style.opacityMedium diff --git a/Widgets/NToggle.qml b/Widgets/NToggle.qml index 7994229..2c61e96 100644 --- a/Widgets/NToggle.qml +++ b/Widgets/NToggle.qml @@ -53,8 +53,8 @@ RowLayout { implicitWidth: (root.baseSize - 5) * scaling implicitHeight: (root.baseSize - 5) * scaling radius: height * 0.5 - color: value ? Colors.colorOnSurface : Colors.colorPrimary //Colors.onBackground : Colors.colorSecondary - border.color: value ? Colors.colorSurface: Colors.colorSurface + color: value ? Colors.colorOnSurface : Colors.colorPrimary //Colors.onBackground : Colors.colorSecondary + border.color: value ? Colors.colorSurface : Colors.colorSurface border.width: Math.max(1, Style.borderMedium * scaling) y: 2 * scaling x: value ? switcher.width - width - 2 * scaling : 2 * scaling From 796bb4acd0254fafef965364a1f96a5a49ac3312 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Thu, 14 Aug 2025 18:27:37 -0400 Subject: [PATCH 280/394] Removed gradients on MediaPlayer switcher and powermenu --- Modules/SidePanel/Cards/MediaCard.qml | 11 +--------- Modules/SidePanel/PowerMenu.qml | 31 ++++++++++----------------- 2 files changed, 12 insertions(+), 30 deletions(-) diff --git a/Modules/SidePanel/Cards/MediaCard.qml b/Modules/SidePanel/Cards/MediaCard.qml index 26aa766..1397c2d 100644 --- a/Modules/SidePanel/Cards/MediaCard.qml +++ b/Modules/SidePanel/Cards/MediaCard.qml @@ -119,16 +119,7 @@ NBox { } background: Rectangle { - gradient: Gradient { - GradientStop { - position: 0.0 - color: Colors.colorSurface - } - GradientStop { - position: 1.0 - color: Colors.colorSurfaceVariant - } - } + color: Colors.colorSurface border.color: Colors.colorOutline border.width: Math.max(1, Style.borderThin * scaling) radius: Style.radiusTiny * scaling diff --git a/Modules/SidePanel/PowerMenu.qml b/Modules/SidePanel/PowerMenu.qml index 1d9afe0..9cbe954 100644 --- a/Modules/SidePanel/PowerMenu.qml +++ b/Modules/SidePanel/PowerMenu.qml @@ -27,16 +27,7 @@ NPanel { radius: Style.radiusMedium * scaling border.color: Colors.colorOutline border.width: Math.max(1, Style.borderThin * scaling) - gradient: Gradient { - GradientStop { - position: 0.0 - color: Colors.colorSurface - } - GradientStop { - position: 1.0 - color: Colors.colorSurfaceVariant - } - } + color: Colors.colorSurface visible: true z: 9999 @@ -88,7 +79,7 @@ NPanel { font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 } - color: lockButtonArea.containsMouse ? Colors.colorOnSurface : Colors.colorOnSurface + color: lockButtonArea.containsMouse ? Colors.colorOnTertiary : Colors.colorOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -96,7 +87,7 @@ NPanel { Text { text: "Lock Screen" - color: lockButtonArea.containsMouse ? Colors.colorOnSurface : Colors.colorOnSurface + color: lockButtonArea.containsMouse ? Colors.colorOnTertiary : Colors.colorOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -148,7 +139,7 @@ NPanel { font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 } - color: suspendButtonArea.containsMouse ? Colors.colorOnSurface : Colors.colorOnSurface + color: suspendButtonArea.containsMouse ? Colors.colorOnTertiary : Colors.colorOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -156,7 +147,7 @@ NPanel { Text { text: "Suspend" - color: suspendButtonArea.containsMouse ? Colors.colorOnSurface : Colors.colorOnSurface + color: suspendButtonArea.containsMouse ? Colors.colorOnTertiary : Colors.colorOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -206,7 +197,7 @@ NPanel { font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 } - color: rebootButtonArea.containsMouse ? Colors.colorOnSurface : Colors.colorOnSurface + color: rebootButtonArea.containsMouse ? Colors.colorOnTertiary : Colors.colorOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -214,7 +205,7 @@ NPanel { Text { text: "Reboot" - color: rebootButtonArea.containsMouse ? Colors.colorOnSurface : Colors.colorOnSurface + color: rebootButtonArea.containsMouse ? Colors.colorOnTertiary : Colors.colorOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -264,7 +255,7 @@ NPanel { font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 } - color: logoutButtonArea.containsMouse ? Colors.colorOnSurface : Colors.colorOnSurface + color: logoutButtonArea.containsMouse ? Colors.colorOnTertiary : Colors.colorOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -272,7 +263,7 @@ NPanel { Text { text: "Logout" - color: logoutButtonArea.containsMouse ? Colors.colorOnSurface : Colors.colorOnSurface + color: logoutButtonArea.containsMouse ? Colors.colorOnTertiary : Colors.colorOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -322,7 +313,7 @@ NPanel { font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 } - color: shutdownButtonArea.containsMouse ? Colors.colorOnSurface : Colors.colorOnSurface + color: shutdownButtonArea.containsMouse ? Colors.colorOnTertiary : Colors.colorOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -330,7 +321,7 @@ NPanel { Text { text: "Shutdown" - color: shutdownButtonArea.containsMouse ? Colors.colorOnSurface : Colors.colorOnSurface + color: shutdownButtonArea.containsMouse ? Colors.colorOnTertiary : Colors.colorOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling From 7660a37b8ab5de149d5cc0fabfa24de7117ac0b6 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Thu, 14 Aug 2025 18:46:00 -0400 Subject: [PATCH 281/394] Partially restored the default theme (rosepine) --- Modules/Demo/DemoPanel.qml | 2 +- Modules/Settings/SettingsPanel.qml | 2 +- Modules/SidePanel/SidePanel.qml | 4 ++-- Services/Colors.qml | 30 +++++++++++++++--------------- Widgets/NComboBox.qml | 2 +- Widgets/NSlider.qml | 2 +- Widgets/NTextInput.qml | 2 +- Widgets/NToggle.qml | 2 +- 8 files changed, 23 insertions(+), 23 deletions(-) diff --git a/Modules/Demo/DemoPanel.qml b/Modules/Demo/DemoPanel.qml index 2025ace..c1aa89a 100644 --- a/Modules/Demo/DemoPanel.qml +++ b/Modules/Demo/DemoPanel.qml @@ -68,7 +68,7 @@ NLoader { Rectangle { id: bgRect - color: Colors.colorSurface + color: Colors.colorSurfaceVariant radius: Style.radiusMedium * scaling border.color: Colors.colorOutlineVariant border.width: Math.max(1, Style.borderThin * scaling) diff --git a/Modules/Settings/SettingsPanel.qml b/Modules/Settings/SettingsPanel.qml index 926a816..2e03531 100644 --- a/Modules/Settings/SettingsPanel.qml +++ b/Modules/Settings/SettingsPanel.qml @@ -161,7 +161,7 @@ NLoader { color: Colors.colorSurface radius: Style.radiusLarge * scaling border.color: Colors.colorOutlineVariant - border.width: Math.max(1, Style.borderMedium * scaling) + border.width: Math.max(1, Style.borderThin * scaling) layer.enabled: true width: (screen.width * 0.5) * scaling height: (screen.height * 0.5) * scaling diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml index 7c3aec2..0d6962c 100644 --- a/Modules/SidePanel/SidePanel.qml +++ b/Modules/SidePanel/SidePanel.qml @@ -83,8 +83,8 @@ NLoader { id: panelBackground color: Colors.colorSurface radius: Style.radiusLarge * scaling - border.color: Colors.colorOutline - border.width: Math.max(1, Style.borderMedium * scaling) + border.color: Colors.colorOutlineVariant + border.width: Math.max(1, Style.borderThin * scaling) layer.enabled: true width: 460 * scaling property real innerMargin: sidePanel.cardSpacing diff --git a/Services/Colors.qml b/Services/Colors.qml index 972f483..7cb0d57 100644 --- a/Services/Colors.qml +++ b/Services/Colors.qml @@ -59,36 +59,36 @@ Singleton { id: defaultTheme // // --- Key Colors: These are the main accent colors that define your app's theme. - property color colorPrimary: "#000000" // The main brand color, used most frequently. - property color colorOnPrimary: "#000000" // Color for text/icons on a Primary background. + property color colorPrimary: "#ebbcba" // The main brand color, used most frequently. + property color colorOnPrimary: "#191724" // Color for text/icons on a Primary background. property color colorPrimaryContainer: "#000000" // A lighter/subtler tone of Primary, used for component backgrounds. property color colorOnPrimaryContainer: "#000000" // Color for text/icons on a Primary Container background. - property color colorSecondary: "#000000" // An accent color for less prominent components. - property color colorOnSecondary: "#000000" // Color for text/icons on a Secondary background. + property color colorSecondary: "#31748f" // An accent color for less prominent components. + property color colorOnSecondary: "#e0def4" // Color for text/icons on a Secondary background. property color colorSecondaryContainer: "#000000" // A lighter/subtler tone of Secondary. property color colorOnSecondaryContainer: "#000000" // olor for text/icons on a Secondary Container background. - property color colorTertiary: "#000000" // A contrasting accent color used for things like highlights or special actions. - property color colorOnTertiary: "#000000" // Color for text/icons on a Tertiary background. + property color colorTertiary: "#9ccfd8" // A contrasting accent color used for things like highlights or special actions. + property color colorOnTertiary: "#191724" // Color for text/icons on a Tertiary background. property color colorTertiaryContainer: "#000000" // A lighter/subtler tone of Tertiary. property color colorOnTertiaryContainer: "#000000" // Color for text/icons on a Tertiary Container background. // --- Utility colorColors: These colors serve specific, universal purposes like indicating errors or providing neutral backgrounds. - property color colorError: "#000000" // Indicates an error state. - property color colorOnError: "#000000" // Color for text/icons on an Error background. + property color colorError: "#eb6f92" // Indicates an error state. + property color colorOnError: "#191724" // Color for text/icons on an Error background. property color colorErrorContainer: "#000000" // A lighter/subtler tone of Error. property color colorOnErrorContainer: "#000000" // Color for text/icons on an Error Container background. // --- Surface colorand Variant Colors: These provide additional options for surfaces and their contents, creating visual hierarchy. - property color colorSurface: "#000000" // The color for component surfaces like cards, sheets, and menus. - property color colorOnSurface: "#000000" // The primary color for text/icons on a Surface background. - property color colorSurfaceVariant: "#000000" // A surface color with a slightly different tint for differentiation. - property color colorOnSurfaceVariant: "#000000" // The color for less prominent text/icons on a Surface. + property color colorSurface: "#191724" // The color for component surfaces like cards, sheets, and menus. + property color colorOnSurface: "#e0def4" // The primary color for text/icons on a Surface background. + property color colorSurfaceVariant: "#26233a" // A surface color with a slightly different tint for differentiation. + property color colorOnSurfaceVariant: "#908caa" // The color for less prominent text/icons on a Surface. property color colorInversePrimary: "#000000" // A primary color legible on an Inverse Surface, often used for call-to-action buttons. - property color colorOutline: "#000000" // The color for component outlines, like text fields or buttons. - property color colorOutlineVariant: "#000000" // A subtler outline color for decorative elements or dividers. - property color colorShadow: "#000000" // The color used for shadows to create elevation. + property color colorOutline: "#44415a" // The color for component outlines, like text fields or buttons. + property color colorOutlineVariant: "#514e6c" // A subtler outline color for decorative elements or dividers. + property color colorShadow: "#191724" // The color used for shadows to create elevation. // // property color colorBackground: "#191724" // // property color colorSurface: "#1f1d2e" diff --git a/Widgets/NComboBox.qml b/Widgets/NComboBox.qml index ddc8d85..8b37d5b 100644 --- a/Widgets/NComboBox.qml +++ b/Widgets/NComboBox.qml @@ -56,7 +56,7 @@ ColumnLayout { background: Rectangle { implicitWidth: 120 * scaling implicitHeight: preferredHeight - color: Colors.colorSurfaceVariant + color: Colors.colorSurface border.color: combo.activeFocus ? Colors.colorTertiary : Colors.colorOutline border.width: Math.max(1, Style.borderThin * scaling) radius: Style.radiusMedium * scaling diff --git a/Widgets/NSlider.qml b/Widgets/NSlider.qml index 2a2ed96..962af98 100644 --- a/Widgets/NSlider.qml +++ b/Widgets/NSlider.qml @@ -27,7 +27,7 @@ Slider { width: root.availableWidth height: implicitHeight radius: height / 2 - color: Colors.colorSurfaceVariant + color: Colors.colorSurface Rectangle { id: activeTrack diff --git a/Widgets/NTextInput.qml b/Widgets/NTextInput.qml index e96afc6..5fc9faf 100644 --- a/Widgets/NTextInput.qml +++ b/Widgets/NTextInput.qml @@ -47,7 +47,7 @@ Item { implicitWidth: root.width implicitHeight: Style.baseWidgetSize * 1.35 * scaling radius: Style.radiusMedium * scaling - color: Colors.colorSurfaceVariant + color: Colors.colorSurface border.color: Colors.colorOutline border.width: Math.max(1, Style.borderThin * scaling) diff --git a/Widgets/NToggle.qml b/Widgets/NToggle.qml index 2c61e96..217fc9e 100644 --- a/Widgets/NToggle.qml +++ b/Widgets/NToggle.qml @@ -53,7 +53,7 @@ RowLayout { implicitWidth: (root.baseSize - 5) * scaling implicitHeight: (root.baseSize - 5) * scaling radius: height * 0.5 - color: value ? Colors.colorOnSurface : Colors.colorPrimary //Colors.onBackground : Colors.colorSecondary + color: value ? Colors.colorOnPrimary: Colors.colorPrimary border.color: value ? Colors.colorSurface : Colors.colorSurface border.width: Math.max(1, Style.borderMedium * scaling) y: 2 * scaling From 2415768443a27ee1e683e1ef0638516bfc5cfa0c Mon Sep 17 00:00:00 2001 From: quadbyte Date: Thu, 14 Aug 2025 19:19:54 -0400 Subject: [PATCH 282/394] Flushed README.md until we get a proper one --- README.md | 242 +----------------------------------------------------- 1 file changed, 1 insertion(+), 241 deletions(-) diff --git a/README.md b/README.md index be2389b..10b5086 100644 --- a/README.md +++ b/README.md @@ -1,241 +1 @@ -# Noctalia - -**_quiet by design_** - -

- -A sleek, minimal, and thoughtfully crafted setup for Wayland using **Quickshell**. This setup includes a status bar, notification system, control panel, wifi & bluetooth support, power profiles, lockscreen, tray, workspaces, and more — all styled with a warm lavender palette. - -## Preview - -
-Click to expand preview images - -![Main](https://i.imgur.com/5mOIGD2.jpeg) -
- -![Control Panel](https://i.imgur.com/fJmCV6m.jpeg) -
- -![Applauncher](https://i.imgur.com/9OPV30q.jpeg) - -
-
- ---- - -> ⚠️ **Note:** -> This setup currently only supports **Niri** and **Hyprland** (for the most part), mostly due to the workspace integration. For anything else you will have to add your own workspace logic. - ---- - -## Features - -- **Status Bar:** Modular and informative with smooth animations. -- **Notifications:** Non-intrusive alerts styled to blend naturally. -- **Control Panel:** Centralized system controls for quick adjustments. -- **Connectivity:** Easy management of WiFi and Bluetooth devices. -- **Power Profiles:** Quick toggles for CPU performance. -- **Lockscreen:** Secure and visually consistent lock experience. -- **Tray & Workspaces:** Efficient workspace switching and tray icons. -- **Applauncher:** Stylized Applauncher to fit into the setup. - ---- - -
-Theme Colors - -| Color Role | Color | Description | -| -------------------- | ----------- | -------------------------- | -| Background Primary | `#0C0D11` | Deep indigo-black | -| Background Secondary | `#151720` | Slightly lifted dark | -| Background Tertiary | `#1D202B` | Soft contrast surface | -| Surface | `#1A1C26` | Material-like base layer | -| Surface Variant | `#2A2D3A` | Lightly elevated | -| Text Primary | `#CACEE2` | Gentle off-white | -| Text Secondary | `#B7BBD0` | Muted lavender-blue | -| Text Disabled | `#6B718A` | Dimmed blue-gray | -| Accent Primary | `#A8AEFF` | Light enchanted lavender | -| Accent Secondary | `#9EA0FF` | Softer lavender hue | -| Accent Tertiary | `#8EABFF` | Warm golden glow | -| Error | `#FF6B81` | Soft rose red | -| Warning | `#FFBB66` | Candlelight amber-orange | -| Highlight | `#E3C2FF` | Bright magical lavender | -| Ripple Effect | `#F3DEFF` | Gentle soft splash | -| On Accent | `#1A1A1A` | Text on accent background | -| Outline | `#44485A` | Subtle bluish-gray line | -| Shadow | `#000000B3` | Standard soft black shadow | -| Overlay | `#11121ACC` | Deep bluish overlay | - -
- ---- - -## Installation & Usage - -
-Installation - -Install quickshell: - -``` -yay -S quickshell-git -``` - -or use any other way of installing quickshell-git (flake, paru etc). - -_Download and install the latest release:_ - -``` -mkdir -p ~/.config/quickshell && curl -sL https://github.com/Ly-sec/Noctalia/releases/latest/download/noctalia-latest.tar.gz | tar -xz --strip-components=1 -C ~/.config/quickshell/ -``` - -Or download manually from [releases](https://github.com/Ly-sec/Noctalia/releases) and extract: - -``` -mkdir -p ~/.config/quickshell && tar -xzf noctalia-*.tar.gz --strip-components=1 -C ~/.config/quickshell/ -``` - -### _niri only_ - -Add this to your `layout` section: - -`background-color "transparent"` - -That is to make swww work properly. - -
-
- -
-Usage - -### Start quickshell: - -``` -qs -``` - -(If you want to autostart it, just add it to your niri configuration.) - -It is recommended to set the following in your Niri configuration (hyprland equivalent): - -``` -window-rule { - geometry-corner-radius 20 - clip-to-geometry true -} -``` - -### Settings: - -To make the weather widget, wallpaper manager and record button work you will have to open up the settings menu in to right panel (top right button to open panel) and edit said things accordingly. - -### Launcher: - -The launcher supports special commands for math calculation and clipboard history. -Once the launcher open you can invoke those special command by typing ">" -* \>calc : lets you do simple math -* \>clip : shows clipboard history - -
- -
-
-Keybinds - -### Toggle Applauncher: - -``` - qs ipc call globalIPC toggleLauncher -``` - -### Toggle Lockscreen: - -``` - qs ipc call globalIPC toggleLock -``` - -### Toggle Notification Popup: - -``` -qs ipc call globalIPC toggleNotificationPopup -``` - -### Toggle Idle Inhibitor: - -``` -qs ipc call globalIPC toggleIdleInhibitor -``` -
- ---- - -## Dependencies - -You will need to install a few things to get everything working: - -- `cava` so the audio visualizer works -- `gpu-screen-recorder` so that the record button works -- `xdg-desktop-portal-gnome` or any other xdg-desktop-portal (for `gpu-screen-recorder`) -- `material-symbols-git` so the icons properly show up -- `swww` to add fancy wallpaper animations (optional) -- `matugen` to theme the setup based on wallpaper (optional) - -## zigstat and zigbrightness - -The zigstat and zigbrightness utilities are automatically built from source during release creation. Source code can be found [here](https://git.pika-os.com/wm-packages/pikabar/src/branch/main/src). - -## Known issues - -It is perfect now - ---- - -## 💜 Credits - -Huge thanks to [**@ferrreo**](https://github.com/ferrreo) and [**@quadbyte**](https://github.com/quadbyte) for all the changes they did and all the cool features they added! - ---- - -## Contributing - -Contributions are welcome! Feel free to open issues or submit pull requests. - ---- - -#### Donation - ---- -While I actually didn't want to accept donations, more and more people are asking to donate so... I don't know, if you really feel like donating then I obviously highly appreciate it but **PLEASE** never feel forced to donate or anything. It won't change how I work on Noctalia, it's a project that I work on for fun in the end. - -[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R01IX85B) - ---- - -#### Special Thanks - -Thank you to everyone who supports me and this project 💜! -* Gohma - ---- - -## License - -This project is licensed under the terms of the [MIT License](./LICENSE). +Work in progress... \ No newline at end of file From 9e51fdc932ab3a14aa96041355866a3f77001b16 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Thu, 14 Aug 2025 19:22:52 -0400 Subject: [PATCH 283/394] Avoid the word "Theme" in favor of "Colors" --- Assets/Matugen/templates/noctalia.json | 10 -- Modules/Settings/Tabs/Wallpaper.qml | 8 +- Services/Colors.qml | 139 +++++++------------------ Services/Settings.qml | 2 +- Services/Wallpapers.qml | 6 +- 5 files changed, 48 insertions(+), 117 deletions(-) diff --git a/Assets/Matugen/templates/noctalia.json b/Assets/Matugen/templates/noctalia.json index 9044c0d..84c0de3 100644 --- a/Assets/Matugen/templates/noctalia.json +++ b/Assets/Matugen/templates/noctalia.json @@ -1,30 +1,20 @@ { "colorPrimary": "{{colors.primary.default.hex}}", "colorOnPrimary": "{{colors.on_primary.default.hex}}", - "colorPrimaryContainer": "{{colors.primary_container.default.hex}}", - "colorOnPrimaryContainer": "{{colors.on_primary_container.default.hex}}", "colorSecondary": "{{colors.secondary.default.hex}}", "colorOnSecondary": "{{colors.on_secondary.default.hex}}", - "colorSecondaryContainer": "{{colors.secondary_container.default.hex}}", - "colorOnSecondaryContainer": "{{colors.on_secondary_container.default.hex}}", "colorTertiary": "{{colors.tertiary.default.hex}}", "colorOnTertiary": "{{colors.on_tertiary.default.hex}}", - "colorTertiaryContainer": "{{colors.tertiary_container.default.hex}}", - "colorOnTertiaryContainer": "{{colors.on_tertiary_container.default.hex}}", "colorError": "{{colors.error.default.hex}}", "colorOnError": "{{colors.on_error.default.hex}}", - "colorErrorContainer": "{{colors.error_container.default.hex}}", - "colorOnErrorContainer": "{{colors.on_error_container.default.hex}}", "colorSurface": "{{colors.surface.default.hex}}", "colorOnSurface": "{{colors.on_surface.default.hex}}", "colorSurfaceVariant": "{{colors.surface_variant.default.hex}}", "colorOnSurfaceVariant": "{{colors.on_surface_variant.default.hex}}", - - "colorInversePrimary": "{{colors.inverse_primary.default.hex}}", "colorOutline": "{{colors.outline.default.hex}}", "colorOutlineVariant": "{{colors.outline_variant.default.hex}}", "colorShadow": "{{colors.shadow.default.hex}}" diff --git a/Modules/Settings/Tabs/Wallpaper.qml b/Modules/Settings/Tabs/Wallpaper.qml index c0cd99e..668ff36 100644 --- a/Modules/Settings/Tabs/Wallpaper.qml +++ b/Modules/Settings/Tabs/Wallpaper.qml @@ -94,11 +94,11 @@ ColumnLayout { // Use Wallpaper Theme NToggle { - label: "Use Wallpaper Theme" - description: "Automatically adjust theme colors based on wallpaper using Matugen" - value: Settings.data.wallpaper.generateTheme + label: "Use Wallpaper Colors" + description: "Automatically adjust UI colors based on wallpaper using Matugen" + value: Settings.data.wallpaper.generateColors onToggled: function (newValue) { - Settings.data.wallpaper.generateTheme = newValue + Settings.data.wallpaper.generateColors = newValue } } diff --git a/Services/Colors.qml b/Services/Colors.qml index 7cb0d57..0a2ac85 100644 --- a/Services/Colors.qml +++ b/Services/Colors.qml @@ -11,41 +11,33 @@ import qs.Services Singleton { id: root + // ----------- + // Check if we should use custom colors + property bool useCustom: Settings.data.wallpaper.generateColors && customColorsFile.loaded + // --- Key Colors - property color colorPrimary: useMatugen ? matugenTheme.colorPrimary : defaultTheme.colorPrimary - property color colorOnPrimary: useMatugen ? matugenTheme.colorOnPrimary : defaultTheme.colorOnPrimary - property color colorPrimaryContainer: useMatugen ? matugenTheme.colorPrimaryContainer : defaultTheme.colorPrimaryContainer - property color colorOnPrimaryContainer: useMatugen ? matugenTheme.colorOnPrimaryContainer : defaultTheme.colorOnPrimaryContainer + property color colorPrimary: useCustom ? customColors.colorPrimary : defaultTheme.colorPrimary + property color colorOnPrimary: useCustom ? customColors.colorOnPrimary : defaultTheme.colorOnPrimary - property color colorSecondary: useMatugen ? matugenTheme.colorSecondary : defaultTheme.colorSecondary - property color colorOnSecondary: useMatugen ? matugenTheme.colorOnSecondary : defaultTheme.colorOnSecondary - property color colorSecondaryContainer: useMatugen ? matugenTheme.colorSecondaryContainer : defaultTheme.colorSecondaryContainer - property color colorOnSecondaryContainer: useMatugen ? matugenTheme.colorOnSecondaryContainer : defaultTheme.colorOnSecondaryContainer + property color colorSecondary: useCustom ? customColors.colorSecondary : defaultTheme.colorSecondary + property color colorOnSecondary: useCustom ? customColors.colorOnSecondary : defaultTheme.colorOnSecondary - property color colorTertiary: useMatugen ? matugenTheme.colorTertiary : defaultTheme.colorTertiary - property color colorOnTertiary: useMatugen ? matugenTheme.colorOnTertiary : defaultTheme.colorOnTertiary - property color colorTertiaryContainer: useMatugen ? matugenTheme.colorTertiaryContainer : defaultTheme.colorTertiaryContainer - property color colorOnTertiaryContainer: useMatugen ? matugenTheme.colorOnTertiaryContainer : defaultTheme.colorOnTertiaryContainer + property color colorTertiary: useCustom ? customColors.colorTertiary : defaultTheme.colorTertiary + property color colorOnTertiary: useCustom ? customColors.colorOnTertiary : defaultTheme.colorOnTertiary // --- Utility Colors - property color colorError: useMatugen ? matugenTheme.colorError : defaultTheme.colorError - property color colorOnError: useMatugen ? matugenTheme.colorOnError : defaultTheme.colorOnError - property color colorErrorContainer: useMatugen ? matugenTheme.colorErrorContainer : defaultTheme.colorErrorContainer - property color colorOnErrorContainer: useMatugen ? matugenTheme.colorOnErrorContainer : defaultTheme.colorOnErrorContainer + property color colorError: useCustom ? customColors.colorError : defaultTheme.colorError + property color colorOnError: useCustom ? customColors.colorOnError : defaultTheme.colorOnError // --- Surface and Variant Colors - property color colorSurface: useMatugen ? matugenTheme.colorSurface : defaultTheme.colorSurface - property color colorOnSurface: useMatugen ? matugenTheme.colorOnSurface : defaultTheme.colorOnSurface - property color colorSurfaceVariant: useMatugen ? matugenTheme.colorSurfaceVariant : defaultTheme.colorSurfaceVariant - property color colorOnSurfaceVariant: useMatugen ? matugenTheme.colorOnSurfaceVariant : defaultTheme.colorOnSurfaceVariant - property color colorInversePrimary: useMatugen ? matugenTheme.colorInversePrimary : defaultTheme.colorInversePrimary - property color colorOutline: useMatugen ? matugenTheme.colorOutline : defaultTheme.colorOutline - property color colorOutlineVariant: useMatugen ? matugenTheme.colorOutlineVariant : defaultTheme.colorOutlineVariant - property color colorShadow: useMatugen ? matugenTheme.colorShadow : defaultTheme.colorShadow + property color colorSurface: useCustom ? customColors.colorSurface : defaultTheme.colorSurface + property color colorOnSurface: useCustom ? customColors.colorOnSurface : defaultTheme.colorOnSurface + property color colorSurfaceVariant: useCustom ? customColors.colorSurfaceVariant : defaultTheme.colorSurfaceVariant + property color colorOnSurfaceVariant: useCustom ? customColors.colorOnSurfaceVariant : defaultTheme.colorOnSurfaceVariant + property color colorOutline: useCustom ? customColors.colorOutline : defaultTheme.colorOutline + property color colorOutlineVariant: useCustom ? customColors.colorOutlineVariant : defaultTheme.colorOutlineVariant + property color colorShadow: useCustom ? customColors.colorShadow : defaultTheme.colorShadow - // ----------- - // Check if we should use Matugen theme - property bool useMatugen: Settings.data.wallpaper.generateTheme && matugenFile.loaded // ----------- function applyOpacity(color, opacity) { @@ -61,103 +53,61 @@ Singleton { // // --- Key Colors: These are the main accent colors that define your app's theme. property color colorPrimary: "#ebbcba" // The main brand color, used most frequently. property color colorOnPrimary: "#191724" // Color for text/icons on a Primary background. - property color colorPrimaryContainer: "#000000" // A lighter/subtler tone of Primary, used for component backgrounds. - property color colorOnPrimaryContainer: "#000000" // Color for text/icons on a Primary Container background. property color colorSecondary: "#31748f" // An accent color for less prominent components. property color colorOnSecondary: "#e0def4" // Color for text/icons on a Secondary background. - property color colorSecondaryContainer: "#000000" // A lighter/subtler tone of Secondary. - property color colorOnSecondaryContainer: "#000000" // olor for text/icons on a Secondary Container background. property color colorTertiary: "#9ccfd8" // A contrasting accent color used for things like highlights or special actions. property color colorOnTertiary: "#191724" // Color for text/icons on a Tertiary background. - property color colorTertiaryContainer: "#000000" // A lighter/subtler tone of Tertiary. - property color colorOnTertiaryContainer: "#000000" // Color for text/icons on a Tertiary Container background. // --- Utility colorColors: These colors serve specific, universal purposes like indicating errors or providing neutral backgrounds. property color colorError: "#eb6f92" // Indicates an error state. property color colorOnError: "#191724" // Color for text/icons on an Error background. - property color colorErrorContainer: "#000000" // A lighter/subtler tone of Error. - property color colorOnErrorContainer: "#000000" // Color for text/icons on an Error Container background. // --- Surface colorand Variant Colors: These provide additional options for surfaces and their contents, creating visual hierarchy. property color colorSurface: "#191724" // The color for component surfaces like cards, sheets, and menus. property color colorOnSurface: "#e0def4" // The primary color for text/icons on a Surface background. property color colorSurfaceVariant: "#26233a" // A surface color with a slightly different tint for differentiation. property color colorOnSurfaceVariant: "#908caa" // The color for less prominent text/icons on a Surface. - property color colorInversePrimary: "#000000" // A primary color legible on an Inverse Surface, often used for call-to-action buttons. property color colorOutline: "#44415a" // The color for component outlines, like text fields or buttons. property color colorOutlineVariant: "#514e6c" // A subtler outline color for decorative elements or dividers. property color colorShadow: "#191724" // The color used for shadows to create elevation. - // // property color colorBackground: "#191724" - // // property color colorSurface: "#1f1d2e" - // // property color colorSurfaceVariant: "#26233a" - - // // property color surface: "#1b1927" - // // property color surfaceVariant: "#262337" - - // // property color colorOnBackground: "#e0def4" - // // property color textSecondary: "#908caa" - // // property color textDisabled: "#6e6a86" - - // // property color colorPrimary: "#ebbcba" - // // property color accentSecondary: "#31748f" - // // property color accentTertiary: "#9ccfd8" - - // // property color error: "#eb6f92" - // // property color warning: "#f6c177" - - // // property color hover: "#c4a7e7" - - // // property color onAccent: "#191724" - // // property color outline: "#44415a" - - // // property color shadow: "#191724" - // // property color overlay: "#191724" } // ---------------------------------------------------------------- - // Matugen theme colors (loaded from theme.json) + // Custom colors (loaded from colors.json) + // These can be generated by matugen or simply come from a well know color scheme (Dracula, Gruvbox, Nord, ...) QtObject { - id: matugenTheme + id: customColors // --- Key Colors - property color colorPrimary: matugenData.colorPrimary - property color colorOnPrimary: matugenData.colorOnPrimary - property color colorPrimaryContainer: matugenData.colorPrimaryContainer - property color colorOnPrimaryContainer: matugenData.colorOnPrimaryContainer + property color colorPrimary: customColorsData.colorPrimary + property color colorOnPrimary: customColorsData.colorOnPrimary - property color colorSecondary: matugenData.colorSecondary - property color colorOnSecondary: matugenData.colorOnSecondary - property color colorSecondaryContainer: matugenData.colorSecondaryContainer - property color colorOnSecondaryContainer: matugenData.colorOnSecondaryContainer + property color colorSecondary: customColorsData.colorSecondary + property color colorOnSecondary: customColorsData.colorOnSecondary - property color colorTertiary: matugenData.colorTertiary - property color colorOnTertiary: matugenData.colorOnTertiary - property color colorTertiaryContainer: matugenData.colorTertiaryContainer - property color colorOnTertiaryContainer: matugenData.colorOnTertiaryContainer + property color colorTertiary: customColorsData.colorTertiary + property color colorOnTertiary: customColorsData.colorOnTertiary // --- Utility Colors - property color colorError: matugenData.colorError - property color colorOnError: matugenData.colorOnError - property color colorErrorContainer: matugenData.colorErrorContainer - property color colorOnErrorContainer: matugenData.colorOnErrorContainer + property color colorError: customColorsData.colorError + property color colorOnError: customColorsData.colorOnError // --- Surface and Variant Colors - property color colorSurface: matugenData.colorSurface - property color colorOnSurface: matugenData.colorOnSurface - property color colorSurfaceVariant: matugenData.colorSurfaceVariant - property color colorOnSurfaceVariant: matugenData.colorOnSurfaceVariant - property color colorInversePrimary: matugenData.colorInversePrimary - property color colorOutline: matugenData.colorOutline - property color colorOutlineVariant: matugenData.colorOutlineVariant - property color colorShadow: matugenData.colorShadow + property color colorSurface: customColorsData.colorSurface + property color colorOnSurface: customColorsData.colorOnSurface + property color colorSurfaceVariant: customColorsData.colorSurfaceVariant + property color colorOnSurfaceVariant: customColorsData.colorOnSurfaceVariant + property color colorOutline: customColorsData.colorOutline + property color colorOutlineVariant: customColorsData.colorOutlineVariant + property color colorShadow: customColorsData.colorShadow } - // FileView to load Matugen theme data from colors.json + // FileView to load custom colors data from colors.json FileView { - id: matugenFile + id: customColorsFile path: Settings.configDir + "colors.json" watchChanges: true onFileChanged: reload() @@ -169,36 +119,27 @@ Singleton { } } JsonAdapter { - id: matugenData + id: customColorsData // --- Key Colors property color colorPrimary: defaultTheme.colorPrimary property color colorOnPrimary: defaultTheme.colorOnPrimary - property color colorPrimaryContainer: defaultTheme.colorPrimaryContainer - property color colorOnPrimaryContainer: defaultTheme.colorOnPrimaryContainer property color colorSecondary: defaultTheme.colorSecondary property color colorOnSecondary: defaultTheme.colorOnSecondary - property color colorSecondaryContainer: defaultTheme.colorSecondaryContainer - property color colorOnSecondaryContainer: defaultTheme.colorOnSecondaryContainer property color colorTertiary: defaultTheme.colorTertiary property color colorOnTertiary: defaultTheme.colorOnTertiary - property color colorTertiaryContainer: defaultTheme.colorTertiaryContainer - property color colorOnTertiaryContainer: defaultTheme.colorOnTertiaryContainer // --- Utility Colors property color colorError: defaultTheme.colorError property color colorOnError: defaultTheme.colorOnError - property color colorErrorContainer: defaultTheme.colorErrorContainer - property color colorOnErrorContainer: defaultTheme.colorOnErrorContainer // --- Surface and Variant Colors property color colorSurface: defaultTheme.colorSurface property color colorOnSurface: defaultTheme.colorOnSurface property color colorSurfaceVariant: defaultTheme.colorSurfaceVariant property color colorOnSurfaceVariant: defaultTheme.colorOnSurfaceVariant - property color colorInversePrimary: defaultTheme.colorInversePrimary property color colorOutline: defaultTheme.colorOutline property color colorOutlineVariant: defaultTheme.colorOutlineVariant property color colorShadow: defaultTheme.colorShadow diff --git a/Services/Settings.qml b/Services/Settings.qml index 7bab6c0..a395808 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -123,7 +123,7 @@ Singleton { property string current: "" property bool isRandom: false property int randomInterval: 300 - property bool generateTheme: false + property bool generateColors: false property JsonObject swww onDirectoryChanged: Wallpapers.loadWallpapers() diff --git a/Services/Wallpapers.qml b/Services/Wallpapers.qml index 58adb56..9363cd9 100644 --- a/Services/Wallpapers.qml +++ b/Services/Wallpapers.qml @@ -62,7 +62,7 @@ Singleton { randomWallpaperTimer.restart() } - generateTheme() + generateColors() } function setRandomWallpaper() { @@ -90,8 +90,8 @@ Singleton { } } - function generateTheme() { - if (Settings.data.wallpaper.generateTheme) { + function generateColors() { + if (Settings.data.wallpaper.generateColors) { generateThemeProcess.running = true } } From 06f8f93f835594451414eba97a40eb7d8c44eeb8 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Thu, 14 Aug 2025 19:52:03 -0400 Subject: [PATCH 284/394] Renamed all color names so they are shorter --- Assets/Matugen/templates/noctalia.json | 30 ++-- Modules/AppLauncher/AppLauncher.qml | 48 ++--- Modules/Audio/CircularSpectrum.qml | 4 +- Modules/Audio/LinearSpectrum.qml | 4 +- Modules/Background/Overview.qml | 2 +- Modules/Background/ScreenCorners.qml | 2 +- Modules/Bar/Bar.qml | 2 +- Modules/Bar/Battery.qml | 8 +- Modules/Bar/SystemMonitor.qml | 6 +- Modules/Bar/TrayMenu.qml | 18 +- Modules/Bar/Volume.qml | 4 +- Modules/Bar/WiFiMenu.qml | 40 ++--- Modules/Bar/Workspace.qml | 22 +-- Modules/Calendar/Calendar.qml | 12 +- Modules/Demo/DemoPanel.qml | 18 +- Modules/Dock/Dock.qml | 12 +- Modules/LockScreen/LockScreen.qml | 70 ++++---- Modules/Notification/Notification.qml | 14 +- .../Notification/NotificationHistoryPanel.qml | 16 +- Modules/Settings/SettingsPanel.qml | 18 +- Modules/Settings/Tabs/About.qml | 30 ++-- Modules/Settings/Tabs/Audio.qml | 20 +-- Modules/Settings/Tabs/Bar.qml | 2 +- Modules/Settings/Tabs/Display.qml | 10 +- Modules/Settings/Tabs/General.qml | 6 +- Modules/Settings/Tabs/Misc.qml | 2 +- Modules/Settings/Tabs/Network.qml | 2 +- Modules/Settings/Tabs/ScreenRecorder.qml | 6 +- Modules/Settings/Tabs/TimeWeather.qml | 6 +- Modules/Settings/Tabs/Wallpaper.qml | 24 +-- Modules/Settings/Tabs/WallpaperSelector.qml | 36 ++-- Modules/SidePanel/Cards/MediaCard.qml | 42 ++--- Modules/SidePanel/Cards/ProfileCard.qml | 4 +- Modules/SidePanel/Cards/WeatherCard.qml | 8 +- Modules/SidePanel/PowerMenu.qml | 34 ++-- Modules/SidePanel/SidePanel.qml | 4 +- Services/Colors.qml | 165 ++++++++---------- Widgets/NBox.qml | 4 +- Widgets/NBusyIndicator.qml | 2 +- Widgets/NCard.qml | 4 +- Widgets/NCircleStat.qml | 16 +- Widgets/NComboBox.qml | 16 +- Widgets/NDivider.qml | 2 +- Widgets/NIconButton.qml | 6 +- Widgets/NPill.qml | 10 +- Widgets/NRadioButton.qml | 4 +- Widgets/NSlider.qml | 16 +- Widgets/NText.qml | 2 +- Widgets/NTextInput.qml | 14 +- Widgets/NToggle.qml | 12 +- Widgets/NTooltip.qml | 4 +- 51 files changed, 423 insertions(+), 440 deletions(-) diff --git a/Assets/Matugen/templates/noctalia.json b/Assets/Matugen/templates/noctalia.json index 84c0de3..9bff769 100644 --- a/Assets/Matugen/templates/noctalia.json +++ b/Assets/Matugen/templates/noctalia.json @@ -1,21 +1,21 @@ { - "colorPrimary": "{{colors.primary.default.hex}}", - "colorOnPrimary": "{{colors.on_primary.default.hex}}", + "mPrimary": "{{colors.primary.default.hex}}", + "mOnPrimary": "{{colors.on_primary.default.hex}}", - "colorSecondary": "{{colors.secondary.default.hex}}", - "colorOnSecondary": "{{colors.on_secondary.default.hex}}", + "mSecondary": "{{colors.secondary.default.hex}}", + "mOnSecondary": "{{colors.on_secondary.default.hex}}", - "colorTertiary": "{{colors.tertiary.default.hex}}", - "colorOnTertiary": "{{colors.on_tertiary.default.hex}}", + "mTertiary": "{{colors.tertiary.default.hex}}", + "mOnTertiary": "{{colors.on_tertiary.default.hex}}", - "colorError": "{{colors.error.default.hex}}", - "colorOnError": "{{colors.on_error.default.hex}}", + "mError": "{{colors.error.default.hex}}", + "mOnError": "{{colors.on_error.default.hex}}", - "colorSurface": "{{colors.surface.default.hex}}", - "colorOnSurface": "{{colors.on_surface.default.hex}}", - "colorSurfaceVariant": "{{colors.surface_variant.default.hex}}", - "colorOnSurfaceVariant": "{{colors.on_surface_variant.default.hex}}", - "colorOutline": "{{colors.outline.default.hex}}", - "colorOutlineVariant": "{{colors.outline_variant.default.hex}}", - "colorShadow": "{{colors.shadow.default.hex}}" + "mSurface": "{{colors.surface.default.hex}}", + "mOnSurface": "{{colors.on_surface.default.hex}}", + "mSurfaceVariant": "{{colors.surface_variant.default.hex}}", + "mOnSurfaceVariant": "{{colors.on_surface_variant.default.hex}}", + "mOutline": "{{colors.outline.default.hex}}", + "mOutlineVariant": "{{colors.outline_variant.default.hex}}", + "mShadow": "{{colors.shadow.default.hex}}" } \ No newline at end of file diff --git a/Modules/AppLauncher/AppLauncher.qml b/Modules/AppLauncher/AppLauncher.qml index 2e3f870..af3dd3c 100644 --- a/Modules/AppLauncher/AppLauncher.qml +++ b/Modules/AppLauncher/AppLauncher.qml @@ -254,19 +254,19 @@ NLoader { width: Math.min(700 * scaling, parent.width * 0.75) height: Math.min(550 * scaling, parent.height * 0.8) radius: 32 * scaling - color: Colors.colorSurface - border.color: Colors.colorOutline + color: Colors.mSurface + border.color: Colors.mOutline border.width: Style.borderThin * scaling // Subtle gradient background gradient: Gradient { GradientStop { position: 0.0 - color: Qt.lighter(Colors.colorSurface, 1.02) + color: Qt.lighter(Colors.mSurface, 1.02) } GradientStop { position: 1.0 - color: Qt.darker(Colors.colorSurface, 1.1) + color: Qt.darker(Colors.mSurface, 1.1) } } @@ -281,8 +281,8 @@ NLoader { Layout.preferredHeight: 40 * scaling Layout.bottomMargin: Style.marginMedium * scaling radius: 20 * scaling - color: Colors.colorSurface - border.color: searchInput.activeFocus ? Colors.colorPrimary : Colors.colorOutline + color: Colors.mSurface + border.color: searchInput.activeFocus ? Colors.mPrimary : Colors.mOutline border.width: searchInput.activeFocus ? 2 : 1 Row { @@ -294,14 +294,14 @@ NLoader { text: "search" font.family: "Material Symbols Outlined" font.pointSize: 16 * scaling - color: searchInput.activeFocus ? Colors.colorPrimary : Colors.colorOnSurface + color: searchInput.activeFocus ? Colors.mPrimary : Colors.mOnSurface } TextField { id: searchInput placeholderText: "Search applications..." - color: Colors.colorOnSurface - placeholderTextColor: Colors.colorOnSurface + color: Colors.mOnSurface + placeholderTextColor: Colors.mOnSurface background: null font.pointSize: 13 * scaling Layout.fillWidth: true @@ -309,8 +309,8 @@ NLoader { searchText = text selectedIndex = 0 // Reset selection when search changes } - selectedTextColor: Colors.colorOnSurface - selectionColor: Colors.colorPrimary + selectedTextColor: Colors.mOnSurface + selectionColor: Colors.mPrimary padding: 0 verticalAlignment: TextInput.AlignVCenter leftPadding: 0 @@ -319,14 +319,14 @@ NLoader { bottomPadding: 0 font.bold: true Component.onCompleted: { - contentItem.cursorColor = Colors.colorOnSurface + contentItem.cursorColor = Colors.mOnSurface contentItem.verticalAlignment = TextInput.AlignVCenter // Focus the search bar by default Qt.callLater(() => { searchInput.forceActiveFocus() }) } - onActiveFocusChanged: contentItem.cursorColor = Colors.colorOnSurface + onActiveFocusChanged: contentItem.cursorColor = Colors.mOnSurface Keys.onDownPressed: selectNext() Keys.onUpPressed: selectPrev() @@ -369,10 +369,10 @@ NLoader { height: 56 * scaling radius: 16 * scaling property bool isSelected: index === selectedIndex - color: (appCardArea.containsMouse || isSelected) ? Qt.darker(Colors.colorPrimary, - 1.1) : Colors.colorSurface + color: (appCardArea.containsMouse || isSelected) ? Qt.darker(Colors.mPrimary, + 1.1) : Colors.mSurface border.color: (appCardArea.containsMouse - || isSelected) ? Colors.colorPrimary : "transparent" + || isSelected) ? Colors.mPrimary : "transparent" border.width: (appCardArea.containsMouse || isSelected) ? 2 : 0 Behavior on color { @@ -403,8 +403,8 @@ NLoader { Layout.preferredWidth: 40 * scaling Layout.preferredHeight: 40 * scaling radius: 14 * scaling - color: appCardArea.containsMouse ? Qt.darker(Colors.colorPrimary, - 1.1) : Colors.colorSurfaceVariant + color: appCardArea.containsMouse ? Qt.darker(Colors.mPrimary, + 1.1) : Colors.mSurfaceVariant property bool iconLoaded: (modelData.isCalculator || modelData.isClipboard || modelData.isCommand) || (iconImg.status === Image.Ready && iconImg.source !== "" @@ -439,7 +439,7 @@ NLoader { anchors.fill: parent anchors.margins: 6 * scaling radius: 10 * scaling - color: Colors.colorPrimary + color: Colors.mPrimary opacity: 0.3 visible: !parent.iconLoaded } @@ -451,7 +451,7 @@ NLoader { text: modelData.name ? modelData.name.charAt(0).toUpperCase() : "?" font.pointSize: 18 * scaling font.weight: Font.Bold - color: Colors.colorPrimary + color: Colors.mPrimary } Behavior on color { @@ -470,7 +470,7 @@ NLoader { text: modelData.name || "Unknown" font.pointSize: 14 * scaling font.weight: Font.Bold - color: Colors.colorOnSurface + color: Colors.mOnSurface elide: Text.ElideRight Layout.fillWidth: true } @@ -479,7 +479,7 @@ NLoader { text: modelData.isCalculator ? (modelData.expr + " = " + modelData.result) : modelData.isClipboard ? modelData.content : modelData.isCommand ? modelData.content : (modelData.genericName || modelData.comment || "") font.pointSize: 11 * scaling color: (appCardArea.containsMouse - || isSelected) ? Colors.colorOnSurface : Colors.colorOnSurface + || isSelected) ? Colors.mOnSurface : Colors.mOnSurface elide: Text.ElideRight Layout.fillWidth: true visible: text !== "" @@ -506,7 +506,7 @@ NLoader { NText { text: searchText.trim() !== "" ? "No applications found" : "No applications available" font.pointSize: Style.fontSizeLarge * scaling - color: Colors.colorOnSurface + color: Colors.mOnSurface horizontalAlignment: Text.AlignHCenter Layout.fillWidth: true visible: filteredEntries.length === 0 @@ -520,7 +520,7 @@ NLoader { ">calc") ? `${filteredEntries.length} result${filteredEntries.length !== 1 ? 's' : ''}` : `${filteredEntries.length} application${filteredEntries.length !== 1 ? 's' : ''}` font.pointSize: Style.fontSizeSmall * scaling - color: Colors.colorOnSurface + color: Colors.mOnSurface horizontalAlignment: Text.AlignHCenter Layout.fillWidth: true visible: searchText.trim() !== "" diff --git a/Modules/Audio/CircularSpectrum.qml b/Modules/Audio/CircularSpectrum.qml index 7d0de41..c3fa7e7 100644 --- a/Modules/Audio/CircularSpectrum.qml +++ b/Modules/Audio/CircularSpectrum.qml @@ -5,8 +5,8 @@ Item { id: root property int innerRadius: 32 * scaling property int outerRadius: 64 * scaling - property color fillColor: Colors.colorPrimary - property color strokeColor: Colors.colorOnSurface + property color fillColor: Colors.mPrimary + property color strokeColor: Colors.mOnSurface property int strokeWidth: 0 * scaling property var values: [] property int usableOuter: 64 diff --git a/Modules/Audio/LinearSpectrum.qml b/Modules/Audio/LinearSpectrum.qml index b5e1b08..116dd7e 100644 --- a/Modules/Audio/LinearSpectrum.qml +++ b/Modules/Audio/LinearSpectrum.qml @@ -3,8 +3,8 @@ import qs.Services Item { id: root - property color fillColor: Colors.colorPrimary - property color strokeColor: Colors.colorOnSurface + property color fillColor: Colors.mPrimary + property color strokeColor: Colors.mOnSurface property int strokeWidth: 0 property var values: [] diff --git a/Modules/Background/Overview.qml b/Modules/Background/Overview.qml index 399c25e..22765e7 100644 --- a/Modules/Background/Overview.qml +++ b/Modules/Background/Overview.qml @@ -62,7 +62,7 @@ NLoader { Rectangle { anchors.fill: parent - color: Qt.rgba(Colors.colorSurface.r, Colors.colorSurface.g, Colors.colorSurface.b, 0.5) + color: Qt.rgba(Colors.mSurface.r, Colors.mSurface.g, Colors.mSurface.b, 0.5) } } } diff --git a/Modules/Background/ScreenCorners.qml b/Modules/Background/ScreenCorners.qml index 12acee1..7dc5076 100644 --- a/Modules/Background/ScreenCorners.qml +++ b/Modules/Background/ScreenCorners.qml @@ -17,7 +17,7 @@ NLoader { required property ShellScreen modelData // Visible ring color - property color ringColor: Colors.colorSurface + property color ringColor: Colors.mSurface // The amount subtracted from full size for the inner cutout // Inner size = full size - borderWidth (per axis) property int borderWidth: Style.borderMedium diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index dadadb0..3cadc23 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -36,7 +36,7 @@ Variants { id: bar anchors.fill: parent - color: Colors.colorSurface + color: Colors.mSurface layer.enabled: true } diff --git a/Modules/Bar/Battery.qml b/Modules/Bar/Battery.qml index 3c64c96..ce4acdb 100644 --- a/Modules/Bar/Battery.qml +++ b/Modules/Bar/Battery.qml @@ -51,10 +51,10 @@ NPill { icon: root.batteryIcon() text: Math.round(root.percent) + "%" - pillColor: Colors.colorSurfaceVariant - iconCircleColor: Colors.colorPrimary - iconTextColor: Colors.colorSurface - textColor: charging ? Colors.colorPrimary : Colors.colorOnSurface + pillColor: Colors.mSurfaceVariant + iconCircleColor: Colors.mPrimary + iconTextColor: Colors.mSurface + textColor: charging ? Colors.mPrimary : Colors.mOnSurface tooltipText: { let lines = [] diff --git a/Modules/Bar/SystemMonitor.qml b/Modules/Bar/SystemMonitor.qml index d8efec0..8a8a84d 100644 --- a/Modules/Bar/SystemMonitor.qml +++ b/Modules/Bar/SystemMonitor.qml @@ -23,7 +23,7 @@ Row { font.pointSize: Style.fontSizeLarge * scaling verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter - color: Colors.colorPrimary + color: Colors.mPrimary } NText { @@ -45,7 +45,7 @@ Row { text: "thermometer" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeLarge * scaling - color: Colors.colorPrimary + color: Colors.mPrimary verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter } @@ -68,7 +68,7 @@ Row { text: "memory" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeLarge * scaling - color: Colors.colorPrimary + color: Colors.mPrimary verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter } diff --git a/Modules/Bar/TrayMenu.qml b/Modules/Bar/TrayMenu.qml index 153da44..da52e96 100644 --- a/Modules/Bar/TrayMenu.qml +++ b/Modules/Bar/TrayMenu.qml @@ -73,8 +73,8 @@ PopupWindow { Rectangle { id: bg anchors.fill: parent - color: Colors.colorSurface - border.color: Colors.colorOutline + color: Colors.mSurface + border.color: Colors.mOutline border.width: Math.max(1, Style.borderThin * scaling) radius: Style.radiusMedium * scaling z: 0 @@ -112,7 +112,7 @@ PopupWindow { Rectangle { id: bg anchors.fill: parent - color: mouseArea.containsMouse ? Colors.colorTertiary : "transparent" + color: mouseArea.containsMouse ? Colors.mTertiary : "transparent" radius: Style.radiusSmall * scaling visible: !(modelData?.isSeparator ?? false) @@ -126,7 +126,7 @@ PopupWindow { id: text Layout.fillWidth: true color: (modelData?.enabled - ?? true) ? (mouseArea.containsMouse ? Colors.colorOnSurface : Colors.colorOnSurface) : Colors.textDisabled + ?? true) ? (mouseArea.containsMouse ? Colors.mOnSurface : Colors.mOnSurface) : Colors.textDisabled text: modelData?.text ?? "" font.pointSize: Style.fontSizeSmall * scaling verticalAlignment: Text.AlignVCenter @@ -148,7 +148,7 @@ PopupWindow { font.pointSize: Style.fontSizeSmall * scaling verticalAlignment: Text.AlignVCenter visible: modelData?.hasChildren ?? false - color: Colors.colorOnSurface + color: Colors.mOnSurface } } @@ -308,8 +308,8 @@ PopupWindow { Rectangle { id: bg anchors.fill: parent - color: Colors.colorSurface - border.color: Colors.colorOutline + color: Colors.mSurface + border.color: Colors.mOutline border.width: Math.max(1, Style.borderThin * scaling) radius: Style.radiusMedium * scaling z: 0 @@ -347,10 +347,10 @@ PopupWindow { Rectangle { id: bg anchors.fill: parent - color: mouseArea.containsMouse ? Colors.colorTertiary : "transparent" + color: mouseArea.containsMouse ? Colors.mTertiary : "transparent" radius: Style.radiusSmall * scaling visible: !(modelData?.isSeparator ?? false) - property color hoverTextColor: mouseArea.containsMouse ? Colors.colorOnSurface : Colors.colorOnSurface + property color hoverTextColor: mouseArea.containsMouse ? Colors.mOnSurface : Colors.mOnSurface RowLayout { anchors.fill: parent diff --git a/Modules/Bar/Volume.qml b/Modules/Bar/Volume.qml index ee75c32..65a3f33 100644 --- a/Modules/Bar/Volume.qml +++ b/Modules/Bar/Volume.qml @@ -38,8 +38,8 @@ Item { NPill { id: pill icon: getIcon() - iconCircleColor: Colors.colorPrimary - collapsedIconColor: Colors.colorOnSurface + iconCircleColor: Colors.mPrimary + collapsedIconColor: Colors.mOnSurface autoHide: true text: Math.floor(Audio.volume * 100) + "%" tooltipText: "Volume: " + Math.round( diff --git a/Modules/Bar/WiFiMenu.qml b/Modules/Bar/WiFiMenu.qml index ba320fc..21d5389 100644 --- a/Modules/Bar/WiFiMenu.qml +++ b/Modules/Bar/WiFiMenu.qml @@ -67,9 +67,9 @@ NLoader { Rectangle { id: wifiMenuRect - color: Colors.colorSurface + color: Colors.mSurface radius: Style.radiusMedium * scaling - border.color: Colors.colorSurfaceVariant + border.color: Colors.mSurfaceVariant border.width: Math.max(1, Style.borderMedium * scaling) width: 340 * scaling height: 320 * scaling @@ -119,14 +119,14 @@ NLoader { text: "wifi" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXL * scaling - color: Colors.colorPrimary + color: Colors.mPrimary } NText { text: "WiFi" font.pointSize: Style.fontSizeLarge * scaling font.bold: true - color: Colors.colorOnSurface + color: Colors.mOnSurface Layout.fillWidth: true } @@ -180,7 +180,7 @@ NLoader { Layout.fillWidth: true Layout.preferredHeight: Style.baseWidgetSize * 1.5 * scaling radius: Style.radiusMedium * scaling - color: modelData.connected ? Colors.colorPrimary : (networkMouseArea.containsMouse ? Colors.colorTertiary : "transparent") + color: modelData.connected ? Colors.mPrimary : (networkMouseArea.containsMouse ? Colors.mTertiary : "transparent") RowLayout { anchors.fill: parent @@ -191,7 +191,7 @@ NLoader { text: network.signalIcon(modelData.signal) font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXL * scaling - color: modelData.connected ? Colors.colorSurface : (networkMouseArea.containsMouse ? Colors.colorSurface : Colors.colorOnSurface) + color: modelData.connected ? Colors.mSurface : (networkMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurface) } ColumnLayout { @@ -204,7 +204,7 @@ NLoader { font.pointSize: Style.fontSizeNormal * scaling elide: Text.ElideRight Layout.fillWidth: true - color: modelData.connected ? Colors.colorSurface : (networkMouseArea.containsMouse ? Colors.colorSurface : Colors.colorOnSurface) + color: modelData.connected ? Colors.mSurface : (networkMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurface) } // Security Protocol @@ -213,14 +213,14 @@ NLoader { font.pointSize: Style.fontSizeTiny * scaling elide: Text.ElideRight Layout.fillWidth: true - color: modelData.connected ? Colors.colorSurface : (networkMouseArea.containsMouse ? Colors.colorSurface : Colors.colorOnSurface) + color: modelData.connected ? Colors.mSurface : (networkMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurface) } NText { visible: network.connectStatusSsid === modelData.ssid && network.connectStatus === "error" && network.connectError.length > 0 text: network.connectError - color: Colors.colorError + color: Colors.mError font.pointSize: Style.fontSizeSmall * scaling elide: Text.ElideRight Layout.fillWidth: true @@ -236,7 +236,7 @@ NLoader { NBusyIndicator { visible: network.connectingSsid === modelData.ssid running: network.connectingSsid === modelData.ssid - color: Colors.colorPrimary + color: Colors.mPrimary anchors.centerIn: parent size: Style.baseWidgetSize * 0.7 * scaling } @@ -257,7 +257,7 @@ NLoader { text: "error" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeSmall * scaling - color: Colors.colorError + color: Colors.mError anchors.centerIn: parent } } @@ -266,7 +266,7 @@ NLoader { visible: modelData.connected text: "connected" font.pointSize: Style.fontSizeSmall * scaling - color: modelData.connected ? Colors.colorSurface : (networkMouseArea.containsMouse ? Colors.colorSurface : Colors.colorOnSurface) + color: modelData.connected ? Colors.mSurface : (networkMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurface) } } @@ -298,7 +298,7 @@ NLoader { Layout.preferredHeight: modelData.ssid === passwordPromptSsid && showPasswordPrompt ? 60 : 0 Layout.margins: 8 visible: modelData.ssid === passwordPromptSsid && showPasswordPrompt - color: Colors.colorSurfaceVariant + color: Colors.mSurfaceVariant radius: Style.radiusSmall * scaling RowLayout { @@ -314,7 +314,7 @@ NLoader { anchors.fill: parent radius: 8 color: "transparent" - border.color: passwordInputField.activeFocus ? Colors.colorPrimary : Colors.colorOutline + border.color: passwordInputField.activeFocus ? Colors.mPrimary : Colors.mOutline border.width: 1 TextInput { @@ -323,7 +323,7 @@ NLoader { anchors.margins: Style.marginMedium * scaling text: passwordInput font.pointSize: Style.fontSizeMedium * scaling - color: Colors.colorOnSurface + color: Colors.mOnSurface verticalAlignment: TextInput.AlignVCenter clip: true focus: true @@ -350,8 +350,8 @@ NLoader { Layout.preferredWidth: 80 Layout.preferredHeight: 36 radius: Style.radiusMedium * scaling - color: Colors.colorPrimary - border.color: Colors.colorPrimary + color: Colors.mPrimary + border.color: Colors.mPrimary border.width: 0 Behavior on color { @@ -363,7 +363,7 @@ NLoader { NText { anchors.centerIn: parent text: "Connect" - color: Colors.colorSurface + color: Colors.mSurface font.pointSize: Style.fontSizeSmall * scaling } @@ -375,8 +375,8 @@ NLoader { } cursorShape: Qt.PointingHandCursor hoverEnabled: true - onEntered: parent.color = Qt.darker(Colors.colorPrimary, 1.1) - onExited: parent.color = Colors.colorPrimary + onEntered: parent.color = Qt.darker(Colors.mPrimary, 1.1) + onExited: parent.color = Colors.mPrimary } } } diff --git a/Modules/Bar/Workspace.qml b/Modules/Bar/Workspace.qml index ac6263e..79ef0b9 100644 --- a/Modules/Bar/Workspace.qml +++ b/Modules/Bar/Workspace.qml @@ -18,7 +18,7 @@ Item { property ListModel localWorkspaces: ListModel {} property real masterProgress: 0.0 property bool effectsActive: false - property color effectColor: Colors.colorPrimary + property color effectColor: Colors.mPrimary property int horizontalPadding: Math.round(16 * s) property int spacingBetweenPills: Math.round(8 * s) @@ -72,7 +72,7 @@ Item { } function triggerUnifiedWave() { - effectColor = Colors.colorPrimary + effectColor = Colors.mPrimary masterAnimation.restart() } @@ -108,7 +108,7 @@ Item { const ws = localWorkspaces.get(i) if (ws.isFocused === true) { root.triggerUnifiedWave() - root.workspaceChanged(ws.id, Colors.colorPrimary) + root.workspaceChanged(ws.id, Colors.mPrimary) break } } @@ -121,12 +121,12 @@ Item { anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter radius: Math.round(12 * s) - color: Colors.colorSurfaceVariant - border.color: Colors.colorOutlineVariant + color: Colors.mSurfaceVariant + border.color: Colors.mOutlineVariant border.width: Math.max(1, Math.round(1 * s)) layer.enabled: true layer.effect: MultiEffect { - shadowColor: Colors.colorShadow + shadowColor: Colors.mShadow shadowVerticalOffset: 0 shadowHorizontalOffset: 0 shadowOpacity: 0.10 @@ -166,15 +166,15 @@ Item { } color: { if (model.isFocused) - return Colors.colorPrimary + return Colors.mPrimary if (model.isUrgent) - return Colors.colorError + return Colors.mError if (model.isActive || model.isOccupied) - return Colors.colorSecondary + return Colors.mSecondary if (model.isUrgent) - return Colors.colorError + return Colors.mError - return Colors.colorOutline + return Colors.mOutline } scale: model.isFocused ? 1.0 : 0.9 z: 0 diff --git a/Modules/Calendar/Calendar.qml b/Modules/Calendar/Calendar.qml index a2a67d0..d1dd99a 100644 --- a/Modules/Calendar/Calendar.qml +++ b/Modules/Calendar/Calendar.qml @@ -63,9 +63,9 @@ NLoader { Rectangle { id: calendarRect - color: Colors.colorSurface + color: Colors.mSurface radius: Style.radiusMedium * scaling - border.color: Colors.colorSurfaceVariant + border.color: Colors.mSurfaceVariant border.width: Math.max(1, Style.borderMedium * scaling) width: 340 * scaling height: 320 * scaling // Reduced height to eliminate bottom space @@ -135,7 +135,7 @@ NLoader { horizontalAlignment: Text.AlignHCenter font.pointSize: Style.fontSizeMedium * scaling font.weight: Style.fontWeightBold - color: Colors.colorPrimary + color: Colors.mPrimary } NIconButton { @@ -172,7 +172,7 @@ NLoader { let dayIndex = (firstDay + index) % 7 return Qt.locale().dayName(dayIndex, Locale.ShortFormat) } - color: Colors.colorSecondary + color: Colors.mSecondary font.pointSize: Style.fontSizeMedium * scaling font.weight: Style.fontWeightBold horizontalAlignment: Text.AlignHCenter @@ -210,12 +210,12 @@ NLoader { width: (Style.baseWidgetSize * scaling) height: (Style.baseWidgetSize * scaling) radius: Style.radiusSmall * scaling - color: model.today ? Colors.colorPrimary : "transparent" + color: model.today ? Colors.mPrimary : "transparent" NText { anchors.centerIn: parent text: model.day - color: model.today ? Colors.onAccent : Colors.colorOnSurface + color: model.today ? Colors.onAccent : Colors.mOnSurface opacity: model.month === grid.month ? Style.opacityHeavy : Style.opacityLight font.pointSize: (Style.fontSizeMedium * scaling) font.weight: model.today ? Style.fontWeightBold : Style.fontWeightRegular diff --git a/Modules/Demo/DemoPanel.qml b/Modules/Demo/DemoPanel.qml index c1aa89a..9d3eaaf 100644 --- a/Modules/Demo/DemoPanel.qml +++ b/Modules/Demo/DemoPanel.qml @@ -68,9 +68,9 @@ NLoader { Rectangle { id: bgRect - color: Colors.colorSurfaceVariant + color: Colors.mSurfaceVariant radius: Style.radiusMedium * scaling - border.color: Colors.colorOutlineVariant + border.color: Colors.mOutlineVariant border.width: Math.max(1, Style.borderThin * scaling) width: 500 * scaling height: 700 * scaling @@ -115,7 +115,7 @@ NLoader { NText { text: "DemoPanel" - color: Colors.colorPrimary + color: Colors.mPrimary font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold Layout.alignment: Qt.AlignHCenter @@ -130,7 +130,7 @@ NLoader { spacing: Style.marginLarge * scaling NText { text: "Scaling" - color: Colors.colorSecondary + color: Colors.mSecondary font.weight: Style.fontWeightBold } NText { @@ -173,7 +173,7 @@ NLoader { spacing: Style.marginLarge * scaling NText { text: "NIconButton" - color: Colors.colorSecondary + color: Colors.mSecondary font.weight: Style.fontWeightBold } @@ -193,7 +193,7 @@ NLoader { spacing: Style.marginMedium * scaling NText { text: "NToggle" - color: Colors.colorSecondary + color: Colors.mSecondary font.weight: Style.fontWeightBold } @@ -215,7 +215,7 @@ NLoader { spacing: Style.marginMedium * scaling NText { text: "NComboBox" - color: Colors.colorSecondary + color: Colors.mSecondary font.weight: Style.fontWeightBold } @@ -240,7 +240,7 @@ NLoader { spacing: Style.marginMedium * scaling NText { text: "NTextInput" - color: Colors.colorSecondary + color: Colors.mSecondary font.weight: Style.fontWeightBold } @@ -263,7 +263,7 @@ NLoader { spacing: Style.marginMedium * scaling NText { text: "NBusyIndicator" - color: Colors.colorSecondary + color: Colors.mSecondary font.weight: Style.fontWeightBold } diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml index 6fc8e82..dc10c9f 100644 --- a/Modules/Dock/Dock.qml +++ b/Modules/Dock/Dock.qml @@ -104,7 +104,7 @@ NLoader { id: dockContainer width: dock.width + 40 height: 50 - color: Colors.colorSurface + color: Colors.mSurface anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom topLeftRadius: 20 @@ -203,7 +203,7 @@ NLoader { text: appButton.appId ? appButton.appId.charAt(0).toUpperCase() : "?" font.pixelSize: 14 font.bold: true - color: appButton.isActive ? Colors.colorPrimary : Colors.colorOnSurface + color: appButton.isActive ? Colors.mPrimary : Colors.mOnSurface } MouseArea { @@ -253,7 +253,7 @@ NLoader { visible: isActive width: 20 height: 3 - color: Colors.colorPrimary + color: Colors.mPrimary radius: 1.5 anchors.bottom: parent.bottom anchors.horizontalCenter: parent.horizontalCenter @@ -292,8 +292,8 @@ NLoader { width: 80 height: 32 radius: 8 - color: closeMouseArea.containsMouse ? Colors.colorTertiary : Colors.colorSurface - border.color: Colors.colorOutline + color: closeMouseArea.containsMouse ? Colors.mTertiary : Colors.mSurface + border.color: Colors.mOutline border.width: 1 x: { @@ -315,7 +315,7 @@ NLoader { anchors.centerIn: parent text: "Close" font.pixelSize: 14 - color: Colors.colorOnSurface + color: Colors.mOnSurface } MouseArea { diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index ea50bc2..7bf4c35 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -138,7 +138,7 @@ WlSessionLock { width: Math.random() * 4 + 2 height: width radius: width * 0.5 - color: Qt.rgba(Colors.colorPrimary.r, Colors.colorPrimary.g, Colors.colorPrimary.b, 0.3) + color: Qt.rgba(Colors.mPrimary.r, Colors.mPrimary.g, Colors.mPrimary.b, 0.3) x: Math.random() * parent.width y: Math.random() * parent.height @@ -181,7 +181,7 @@ WlSessionLock { font.pointSize: Style.fontSizeXXL * 6 font.weight: Font.Bold font.letterSpacing: -2 - color: Colors.colorOnSurface + color: Colors.mOnSurface horizontalAlignment: Text.AlignHCenter SequentialAnimation on scale { @@ -205,7 +205,7 @@ WlSessionLock { font.family: "Inter" font.pointSize: Style.fontSizeXL font.weight: Font.Light - color: Colors.colorOnSurface + color: Colors.mOnSurface horizontalAlignment: Text.AlignHCenter width: timeText.width } @@ -222,7 +222,7 @@ WlSessionLock { height: 120 * Scaling.scale(screen) radius: width * 0.5 color: "transparent" - border.color: Colors.colorPrimary + border.color: Colors.mPrimary border.width: 3 * Scaling.scale(screen) anchors.horizontalCenter: parent.horizontalCenter @@ -233,7 +233,7 @@ WlSessionLock { height: parent.height + 24 * Scaling.scale(screen) radius: width * 0.5 color: "transparent" - border.color: Qt.rgba(Colors.colorPrimary.r, Colors.colorPrimary.g, Colors.colorPrimary.b, 0.3) + border.color: Qt.rgba(Colors.mPrimary.r, Colors.mPrimary.g, Colors.mPrimary.b, 0.3) border.width: 2 * Scaling.scale(screen) z: -1 @@ -301,8 +301,8 @@ WlSessionLock { id: terminalBackground anchors.fill: parent radius: 16 - color: Colors.applyOpacity(Colors.colorSurface, "E6") - border.color: Colors.colorPrimary + color: Colors.applyOpacity(Colors.mSurface, "E6") + border.color: Colors.mPrimary border.width: 2 * Scaling.scale(screen) // Scanline effect @@ -311,7 +311,7 @@ WlSessionLock { Rectangle { width: parent.width height: 1 - color: Colors.applyOpacity(Colors.colorPrimary, "1A") + color: Colors.applyOpacity(Colors.mPrimary, "1A") y: index * 10 opacity: 0.3 @@ -333,7 +333,7 @@ WlSessionLock { Rectangle { width: parent.width height: 40 * Scaling.scale(screen) - color: Colors.applyOpacity(Colors.colorPrimary, "33") + color: Colors.applyOpacity(Colors.mPrimary, "33") topLeftRadius: 14 topRightRadius: 14 @@ -344,7 +344,7 @@ WlSessionLock { Text { text: "SECURE TERMINAL" - color: Colors.colorOnSurface + color: Colors.mOnSurface font.family: "DejaVu Sans Mono" font.pointSize: Style.fontSizeLarge font.weight: Font.Bold @@ -370,7 +370,7 @@ WlSessionLock { Text { text: "root@noctalia:~$" - color: Colors.colorPrimary + color: Colors.mPrimary font.family: "DejaVu Sans Mono" font.pointSize: Style.fontSizeLarge font.weight: Font.Bold @@ -379,7 +379,7 @@ WlSessionLock { Text { id: welcomeText text: "" - color: Colors.colorOnSurface + color: Colors.mOnSurface font.family: "DejaVu Sans Mono" font.pointSize: Style.fontSizeLarge property int currentIndex: 0 @@ -408,7 +408,7 @@ WlSessionLock { Text { text: "root@noctalia:~$" - color: Colors.colorPrimary + color: Colors.mPrimary font.family: "DejaVu Sans Mono" font.pointSize: Style.fontSizeLarge font.weight: Font.Bold @@ -416,7 +416,7 @@ WlSessionLock { Text { text: "sudo unlock_session" - color: Colors.colorOnSurface + color: Colors.mOnSurface font.family: "DejaVu Sans Mono" font.pointSize: Style.fontSizeLarge } @@ -429,7 +429,7 @@ WlSessionLock { visible: false font.family: "DejaVu Sans Mono" font.pointSize: Style.fontSizeLarge - color: Colors.colorOnSurface + color: Colors.mOnSurface echoMode: TextInput.Password passwordCharacter: "*" passwordMaskDelay: 0 @@ -456,7 +456,7 @@ WlSessionLock { Text { id: asterisksText text: "*".repeat(passwordInput.text.length) - color: Colors.colorOnSurface + color: Colors.mOnSurface font.family: "DejaVu Sans Mono" font.pointSize: Style.fontSizeLarge visible: passwordInput.activeFocus @@ -483,7 +483,7 @@ WlSessionLock { Rectangle { width: 8 * Scaling.scale(screen) height: 20 * Scaling.scale(screen) - color: Colors.colorPrimary + color: Colors.mPrimary visible: passwordInput.activeFocus anchors.left: asterisksText.right anchors.leftMargin: 2 * Scaling.scale(screen) @@ -506,7 +506,7 @@ WlSessionLock { // Status messages Text { text: lock.authenticating ? "Authenticating..." : (lock.errorMessage !== "" ? "Authentication failed." : "") - color: lock.authenticating ? Colors.colorPrimary : (lock.errorMessage !== "" ? Colors.colorError : "transparent") + color: lock.authenticating ? Colors.mPrimary : (lock.errorMessage !== "" ? Colors.mError : "transparent") font.family: "DejaVu Sans Mono" font.pointSize: Style.fontSizeLarge Layout.fillWidth: true @@ -530,9 +530,9 @@ WlSessionLock { width: 120 * Scaling.scale(screen) height: 40 * Scaling.scale(screen) radius: 12 - color: executeButtonArea.containsMouse ? Colors.colorPrimary : Colors.applyOpacity( - Colors.colorPrimary, "33") - border.color: Colors.colorPrimary + color: executeButtonArea.containsMouse ? Colors.mPrimary : Colors.applyOpacity( + Colors.mPrimary, "33") + border.color: Colors.mPrimary border.width: 1 enabled: !lock.authenticating Layout.alignment: Qt.AlignRight @@ -541,7 +541,7 @@ WlSessionLock { Text { anchors.centerIn: parent text: lock.authenticating ? "EXECUTING" : "EXECUTE" - color: executeButtonArea.containsMouse ? Colors.onAccent : Colors.colorPrimary + color: executeButtonArea.containsMouse ? Colors.onAccent : Colors.mPrimary font.family: "DejaVu Sans Mono" font.pointSize: Style.fontSizeMedium font.weight: Font.Bold @@ -595,7 +595,7 @@ WlSessionLock { anchors.fill: parent radius: parent.radius color: "transparent" - border.color: Colors.applyOpacity(Colors.colorPrimary, "4D") + border.color: Colors.applyOpacity(Colors.mPrimary, "4D") border.width: 1 z: -1 @@ -631,9 +631,9 @@ WlSessionLock { width: 64 * Scaling.scale(screen) height: 64 * Scaling.scale(screen) radius: 32 - color: Qt.rgba(Colors.colorError.r, Colors.colorError.g, Colors.colorError.b, + color: Qt.rgba(Colors.mError.r, Colors.mError.g, Colors.mError.b, shutdownArea.containsMouse ? 0.9 : 0.2) - border.color: Colors.colorError + border.color: Colors.mError border.width: 2 * Scaling.scale(screen) // Glow effect @@ -643,7 +643,7 @@ WlSessionLock { height: parent.height + 10 * Scaling.scale(screen) radius: width * 0.5 color: "transparent" - border.color: Qt.rgba(Colors.colorError.r, Colors.colorError.g, Colors.colorError.b, 0.3) + border.color: Qt.rgba(Colors.mError.r, Colors.mError.g, Colors.mError.b, 0.3) border.width: 2 * Scaling.scale(screen) opacity: shutdownArea.containsMouse ? 1 : 0 z: -1 @@ -672,7 +672,7 @@ WlSessionLock { text: "power_settings_new" font.family: "Material Symbols Outlined" font.pixelSize: 28 * Scaling.scale(screen) - color: shutdownArea.containsMouse ? Colors.onAccent : Colors.colorError + color: shutdownArea.containsMouse ? Colors.onAccent : Colors.mError } Behavior on color { @@ -689,9 +689,9 @@ WlSessionLock { width: 64 * Scaling.scale(screen) height: 64 * Scaling.scale(screen) radius: 32 - color: Qt.rgba(Colors.colorPrimary.r, Colors.colorPrimary.g, Colors.colorPrimary.b, + color: Qt.rgba(Colors.mPrimary.r, Colors.mPrimary.g, Colors.mPrimary.b, rebootArea.containsMouse ? 0.9 : 0.2) - border.color: Colors.colorPrimary + border.color: Colors.mPrimary border.width: 2 * Scaling.scale(screen) // Glow effect @@ -701,7 +701,7 @@ WlSessionLock { height: parent.height + 10 * Scaling.scale(screen) radius: width * 0.5 color: "transparent" - border.color: Qt.rgba(Colors.colorPrimary.r, Colors.colorPrimary.g, Colors.colorPrimary.b, 0.3) + border.color: Qt.rgba(Colors.mPrimary.r, Colors.mPrimary.g, Colors.mPrimary.b, 0.3) border.width: 2 * Scaling.scale(screen) opacity: rebootArea.containsMouse ? 1 : 0 z: -1 @@ -729,7 +729,7 @@ WlSessionLock { text: "refresh" font.family: "Material Symbols Outlined" font.pixelSize: 28 * Scaling.scale(screen) - color: rebootArea.containsMouse ? Colors.onAccent : Colors.colorPrimary + color: rebootArea.containsMouse ? Colors.onAccent : Colors.mPrimary } Behavior on color { @@ -746,9 +746,9 @@ WlSessionLock { width: 64 * Scaling.scale(screen) height: 64 * Scaling.scale(screen) radius: 32 - color: Qt.rgba(Colors.colorSecondary.r, Colors.colorSecondary.g, Colors.colorSecondary.b, + color: Qt.rgba(Colors.mSecondary.r, Colors.mSecondary.g, Colors.mSecondary.b, logoutArea.containsMouse ? 0.9 : 0.2) - border.color: Colors.colorSecondary + border.color: Colors.mSecondary border.width: 2 * Scaling.scale(screen) // Glow effect @@ -758,7 +758,7 @@ WlSessionLock { height: parent.height + 10 * Scaling.scale(screen) radius: width * 0.5 color: "transparent" - border.color: Qt.rgba(Colors.colorSecondary.r, Colors.colorSecondary.g, Colors.colorSecondary.b, 0.3) + border.color: Qt.rgba(Colors.mSecondary.r, Colors.mSecondary.g, Colors.mSecondary.b, 0.3) border.width: 2 * Scaling.scale(screen) opacity: logoutArea.containsMouse ? 1 : 0 z: -1 @@ -788,7 +788,7 @@ WlSessionLock { text: "exit_to_app" font.family: "Material Symbols Outlined" font.pixelSize: 28 * Scaling.scale(screen) - color: logoutArea.containsMouse ? Colors.onAccent : Colors.colorSecondary + color: logoutArea.containsMouse ? Colors.onAccent : Colors.mSecondary } Behavior on color { diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index 7f71ecb..99cc20e 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -63,9 +63,9 @@ PanelWindow { height: Math.max(80 * scaling, contentColumn.implicitHeight + (Style.marginMedium * 2 * scaling)) clip: true radius: Style.radiusMedium * scaling - border.color: Colors.colorPrimary + border.color: Colors.mPrimary border.width: Math.max(1, Style.borderThin * scaling) - color: Colors.colorSurface + color: Colors.mSurface // Animation properties property real scaleValue: 0.8 @@ -133,14 +133,14 @@ PanelWindow { spacing: Style.marginSmall * scaling NText { text: (model.appName || model.desktopEntry) || "Unknown App" - color: Colors.colorSecondary + color: Colors.mSecondary font.pointSize: Style.fontSizeSmall * scaling } Rectangle { width: 6 * scaling height: 6 * scaling radius: 3 * scaling - color: (model.urgency === NotificationUrgency.Critical) ? Colors.colorError : (model.urgency === NotificationUrgency.Low) ? Colors.colorOnSurface : Colors.colorPrimary + color: (model.urgency === NotificationUrgency.Critical) ? Colors.mError : (model.urgency === NotificationUrgency.Low) ? Colors.mOnSurface : Colors.mPrimary Layout.alignment: Qt.AlignVCenter } Item { @@ -148,7 +148,7 @@ PanelWindow { } NText { text: notificationService.formatTimestamp(model.timestamp) - color: Colors.colorOnSurface + color: Colors.mOnSurface font.pointSize: Style.fontSizeSmall * scaling } } @@ -157,7 +157,7 @@ PanelWindow { text: model.summary || "No summary" font.pointSize: Style.fontSizeLarge * scaling font.weight: Style.fontWeightBold - color: Colors.colorOnSurface + color: Colors.mOnSurface wrapMode: Text.Wrap width: 300 * scaling maximumLineCount: 3 @@ -167,7 +167,7 @@ PanelWindow { NText { text: model.body || "" font.pointSize: Style.fontSizeSmall * scaling - color: Colors.colorOnSurface + color: Colors.mOnSurface wrapMode: Text.Wrap width: 300 * scaling maximumLineCount: 5 diff --git a/Modules/Notification/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml index 3dce3b3..da0c95c 100644 --- a/Modules/Notification/NotificationHistoryPanel.qml +++ b/Modules/Notification/NotificationHistoryPanel.qml @@ -65,9 +65,9 @@ NLoader { Rectangle { id: notificationRect - color: Colors.colorSurface + color: Colors.mSurface radius: Style.radiusMedium * scaling - border.color: Colors.colorSurfaceVariant + border.color: Colors.mSurfaceVariant border.width: Math.max(1, Style.borderMedium * scaling) width: 400 * scaling height: 500 * scaling @@ -117,14 +117,14 @@ NLoader { text: "notifications" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXL * scaling - color: Colors.colorPrimary + color: Colors.mPrimary } NText { text: "Notification History" font.pointSize: Style.fontSizeLarge * scaling font.bold: true - color: Colors.colorOnSurface + color: Colors.mOnSurface Layout.fillWidth: true } @@ -159,7 +159,7 @@ NLoader { width: notificationList ? (notificationList.width - 20) : 380 * scaling height: Math.max(80, notificationContent.height + 30) radius: Style.radiusMedium * scaling - color: notificationMouseArea.containsMouse ? Colors.colorPrimary : "transparent" + color: notificationMouseArea.containsMouse ? Colors.mPrimary : "transparent" RowLayout { anchors { @@ -179,7 +179,7 @@ NLoader { text: (summary || "No summary").substring(0, 100) font.pointSize: Style.fontSizeMedium * scaling font.weight: Font.Medium - color: notificationMouseArea.containsMouse ? Colors.colorSurface : Colors.colorOnSurface + color: notificationMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurface wrapMode: Text.Wrap width: parent.width - 30 maximumLineCount: 2 @@ -189,7 +189,7 @@ NLoader { NText { text: (body || "").substring(0, 150) font.pointSize: Style.fontSizeSmall * scaling - color: notificationMouseArea.containsMouse ? Colors.colorSurface : Colors.colorOnSurface + color: notificationMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurface wrapMode: Text.Wrap width: parent.width - 30 maximumLineCount: 3 @@ -199,7 +199,7 @@ NLoader { NText { text: NotificationService.formatTimestamp(timestamp) font.pointSize: Style.fontSizeSmall * scaling - color: notificationMouseArea.containsMouse ? Colors.colorSurface : Colors.colorOnSurface + color: notificationMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurface } } } diff --git a/Modules/Settings/SettingsPanel.qml b/Modules/Settings/SettingsPanel.qml index 2e03531..253ebf7 100644 --- a/Modules/Settings/SettingsPanel.qml +++ b/Modules/Settings/SettingsPanel.qml @@ -158,9 +158,9 @@ NLoader { Rectangle { id: bgRect - color: Colors.colorSurface + color: Colors.mSurface radius: Style.radiusLarge * scaling - border.color: Colors.colorOutlineVariant + border.color: Colors.mOutlineVariant border.width: Math.max(1, Style.borderThin * scaling) layer.enabled: true width: (screen.width * 0.5) * scaling @@ -209,8 +209,8 @@ NLoader { id: sidebar Layout.preferredWidth: 260 * scaling Layout.fillHeight: true - color: Colors.colorSurfaceVariant - border.color: Colors.colorOutlineVariant + color: Colors.mSurfaceVariant + border.color: Colors.mOutlineVariant border.width: Math.max(1, Style.borderThin * scaling) radius: Style.radiusMedium * scaling @@ -229,7 +229,7 @@ NLoader { width: parent.width height: 32 * scaling // Back to original height radius: Style.radiusSmall * scaling - color: selected ? Colors.colorPrimary : (tabItem.hovering ? Colors.colorTertiary : "transparent") + color: selected ? Colors.mPrimary : (tabItem.hovering ? Colors.mTertiary : "transparent") border.color: "transparent" border.width: 0 @@ -238,7 +238,7 @@ NLoader { // Subtle hover effect: only icon/text color tint on hover property bool hovering: false - property color tabTextColor: selected ? Colors.colorOnPrimary : (tabItem.hovering ? Colors.colorOnTertiary : Colors.colorOnSurface) + property color tabTextColor: selected ? Colors.mOnPrimary : (tabItem.hovering ? Colors.mOnTertiary : Colors.mOnSurface) RowLayout { anchors.fill: parent @@ -284,8 +284,8 @@ NLoader { Layout.fillWidth: true Layout.fillHeight: true radius: Style.radiusMedium * scaling - color: Colors.colorSurfaceVariant - border.color: Colors.colorOutlineVariant + color: Colors.mSurfaceVariant + border.color: Colors.mOutlineVariant border.width: Math.max(1, Style.borderThin * scaling) clip: true @@ -305,7 +305,7 @@ NLoader { text: panel.tabsModel[currentTabIndex].label font.pointSize: Style.fontSizeLarge * scaling font.weight: Style.fontWeightBold - color: Colors.colorPrimary + color: Colors.mPrimary Layout.fillWidth: true } NIconButton { diff --git a/Modules/Settings/Tabs/About.qml b/Modules/Settings/Tabs/About.qml index 82b259f..3153379 100644 --- a/Modules/Settings/Tabs/About.qml +++ b/Modules/Settings/Tabs/About.qml @@ -59,7 +59,7 @@ ColumnLayout { text: "Noctalia: quiet by design" font.pointSize: Style.fontSizeXXL * scaling font.weight: Style.fontWeightBold - color: Colors.colorOnSurface + color: Colors.mOnSurface Layout.alignment: Qt.AlignCenter Layout.bottomMargin: Style.marginSmall * scaling } @@ -67,7 +67,7 @@ ColumnLayout { NText { text: "It may just be another quickshell setup but it won't get in your way." font.pointSize: Style.fontSizeMedium * scaling - color: Colors.colorOnSurface + color: Colors.mOnSurface Layout.alignment: Qt.AlignCenter Layout.bottomMargin: Style.marginLarge * scaling } @@ -80,25 +80,25 @@ ColumnLayout { NText { text: "Latest Version:" - color: Colors.colorOnSurface + color: Colors.mOnSurface Layout.alignment: Qt.AlignRight } NText { text: root.latestVersion - color: Colors.colorOnSurface + color: Colors.mOnSurface font.weight: Style.fontWeightBold } NText { text: "Installed Version:" - color: Colors.colorOnSurface + color: Colors.mOnSurface Layout.alignment: Qt.AlignRight } NText { text: root.currentVersion - color: Colors.colorOnSurface + color: Colors.mOnSurface font.weight: Style.fontWeightBold } } @@ -109,8 +109,8 @@ ColumnLayout { Layout.preferredWidth: updateText.implicitWidth + 46 * scaling Layout.preferredHeight: 32 * scaling radius: Style.radiusLarge * scaling - color: updateArea.containsMouse ? Colors.colorPrimary : "transparent" - border.color: Colors.colorPrimary + color: updateArea.containsMouse ? Colors.mPrimary : "transparent" + border.color: Colors.mPrimary border.width: 1 visible: { if (root.currentVersion === "Unknown" || root.latestVersion === "Unknown") @@ -138,14 +138,14 @@ ColumnLayout { text: "system_update" font.family: "Material Symbols Outlined" font.pointSize: 18 * scaling - color: updateArea.containsMouse ? Colors.colorSurface : Colors.colorPrimary + color: updateArea.containsMouse ? Colors.mSurface : Colors.mPrimary } NText { id: updateText text: "Download latest release" font.pointSize: 14 * scaling - color: updateArea.containsMouse ? Colors.colorSurface : Colors.colorPrimary + color: updateArea.containsMouse ? Colors.mSurface : Colors.mPrimary } } @@ -171,7 +171,7 @@ ColumnLayout { text: `Shout-out to our ${root.contributors.length} awesome contributors!` font.pointSize: Style.fontSizeLarge * scaling font.weight: Style.fontWeightBold - color: Colors.colorOnSurface + color: Colors.mOnSurface Layout.alignment: Qt.AlignCenter Layout.topMargin: Style.marginLarge * 2 } @@ -199,7 +199,7 @@ ColumnLayout { width: contributorsGrid.cellWidth - Style.marginLarge * scaling height: contributorsGrid.cellHeight - Style.marginTiny * scaling radius: Style.radiusLarge * scaling - color: contributorArea.containsMouse ? Colors.colorTertiary : "transparent" + color: contributorArea.containsMouse ? Colors.mTertiary : "transparent" RowLayout { anchors.fill: parent @@ -216,7 +216,7 @@ ColumnLayout { anchors.fill: parent anchors.margins: Style.marginTiny * scaling fallbackIcon: "person" - borderColor: Colors.colorPrimary + borderColor: Colors.mPrimary borderWidth: Math.max(1, Style.borderMedium * scaling) imageRadius: width * 0.5 } @@ -230,7 +230,7 @@ ColumnLayout { NText { text: modelData.login || "Unknown" font.weight: Style.fontWeightBold - color: contributorArea.containsMouse ? Colors.colorSurface : Colors.colorOnSurface + color: contributorArea.containsMouse ? Colors.mSurface : Colors.mOnSurface elide: Text.ElideRight Layout.fillWidth: true } @@ -239,7 +239,7 @@ ColumnLayout { text: (modelData.contributions || 0) + " " + ((modelData.contributions || 0) === 1 ? "commit" : "commits") font.pointSize: Style.fontSizeSmall * scaling - color: contributorArea.containsMouse ? Colors.colorSurface : Colors.colorOnSurface + color: contributorArea.containsMouse ? Colors.mSurface : Colors.mOnSurface } } } diff --git a/Modules/Settings/Tabs/Audio.qml b/Modules/Settings/Tabs/Audio.qml index b174304..39aaa64 100644 --- a/Modules/Settings/Tabs/Audio.qml +++ b/Modules/Settings/Tabs/Audio.qml @@ -49,7 +49,7 @@ ColumnLayout { text: "Audio" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.colorOnSurface + color: Colors.mOnSurface Layout.bottomMargin: Style.marginSmall * scaling } @@ -70,13 +70,13 @@ ColumnLayout { NText { text: "Master Volume" font.weight: Style.fontWeightBold - color: Colors.colorOnSurface + color: Colors.mOnSurface } NText { text: "System-wide volume level" font.pointSize: Style.fontSizeSmall * scaling - color: Colors.colorOnSurface + color: Colors.mOnSurface wrapMode: Text.WordWrap Layout.fillWidth: true } @@ -110,7 +110,7 @@ ColumnLayout { NText { text: Math.floor(Audio.volume * 100) + "%" Layout.alignment: Qt.AlignVCenter - color: Colors.colorOnSurface + color: Colors.mOnSurface } } } @@ -149,7 +149,7 @@ ColumnLayout { text: "Audio Devices" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.colorOnSurface + color: Colors.mOnSurface Layout.bottomMargin: Style.marginSmall * scaling } @@ -168,13 +168,13 @@ ColumnLayout { text: "Output Device" font.pointSize: Style.fontSizeMedium * scaling font.weight: Style.fontWeightBold - color: Colors.colorOnSurface + color: Colors.mOnSurface } NText { text: "Select the desired audio output device" font.pointSize: Style.fontSizeSmall * scaling - color: Colors.colorOnSurface + color: Colors.mOnSurface wrapMode: Text.WordWrap Layout.fillWidth: true } @@ -207,13 +207,13 @@ ColumnLayout { text: "Input Device" font.pointSize: Style.fontSizeMedium * scaling font.weight: Style.fontWeightBold - color: Colors.colorOnSurface + color: Colors.mOnSurface } NText { text: "Select desired audio input device" font.pointSize: Style.fontSizeSmall * scaling - color: Colors.colorOnSurface + color: Colors.mOnSurface wrapMode: Text.WordWrap Layout.fillWidth: true } @@ -247,7 +247,7 @@ ColumnLayout { text: "Audio Visualizer" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.colorOnSurface + color: Colors.mOnSurface Layout.bottomMargin: Style.marginSmall * scaling } diff --git a/Modules/Settings/Tabs/Bar.qml b/Modules/Settings/Tabs/Bar.qml index f54a0a8..cd49a77 100644 --- a/Modules/Settings/Tabs/Bar.qml +++ b/Modules/Settings/Tabs/Bar.qml @@ -36,7 +36,7 @@ ColumnLayout { text: "Components" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.colorOnSurface + color: Colors.mOnSurface } NToggle { diff --git a/Modules/Settings/Tabs/Display.qml b/Modules/Settings/Tabs/Display.qml index f93215e..3234699 100644 --- a/Modules/Settings/Tabs/Display.qml +++ b/Modules/Settings/Tabs/Display.qml @@ -44,7 +44,7 @@ Item { text: "Per‑monitor configuration" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.colorOnSurface + color: Colors.mOnSurface } Repeater { @@ -52,8 +52,8 @@ Item { delegate: Rectangle { Layout.fillWidth: true radius: Style.radiusMedium * scaling - color: Colors.colorSurface - border.color: Colors.colorOutline + color: Colors.mSurface + border.color: Colors.mOutline border.width: Math.max(1, Style.borderThin * scaling) implicitHeight: contentCol.implicitHeight + Style.marginXL * 2 * scaling @@ -67,13 +67,13 @@ Item { text: (modelData.name || "Unknown") font.pointSize: Style.fontSizeLarge * scaling font.weight: Style.fontWeightBold - color: Colors.colorSecondary + color: Colors.mSecondary } NText { text: `Resolution: ${modelData.width}x${modelData.height} - Position: (${modelData.x}, ${modelData.y})` font.pointSize: Style.fontSizeSmall * scaling - color: Colors.colorOnSurface + color: Colors.mOnSurface } ColumnLayout { diff --git a/Modules/Settings/Tabs/General.qml b/Modules/Settings/Tabs/General.qml index 2ed620e..c161442 100644 --- a/Modules/Settings/Tabs/General.qml +++ b/Modules/Settings/Tabs/General.qml @@ -36,7 +36,7 @@ ColumnLayout { text: "General Settings" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.colorOnSurface + color: Colors.mOnSurface } // Profile section @@ -55,7 +55,7 @@ ColumnLayout { height: 64 * scaling imagePath: Settings.data.general.avatarImage fallbackIcon: "person" - borderColor: Colors.colorPrimary + borderColor: Colors.mPrimary borderWidth: Math.max(1, Style.borderMedium) } @@ -87,7 +87,7 @@ ColumnLayout { text: "User Interface" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.colorOnSurface + color: Colors.mOnSurface Layout.bottomMargin: Style.marginSmall * scaling } diff --git a/Modules/Settings/Tabs/Misc.qml b/Modules/Settings/Tabs/Misc.qml index 64ace7c..0b04700 100644 --- a/Modules/Settings/Tabs/Misc.qml +++ b/Modules/Settings/Tabs/Misc.qml @@ -36,7 +36,7 @@ ColumnLayout { text: "Miscellaneous Settings" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.colorOnSurface + color: Colors.mOnSurface Layout.bottomMargin: Style.marginSmall * scaling } } diff --git a/Modules/Settings/Tabs/Network.qml b/Modules/Settings/Tabs/Network.qml index 5b96c91..6ff7b82 100644 --- a/Modules/Settings/Tabs/Network.qml +++ b/Modules/Settings/Tabs/Network.qml @@ -38,7 +38,7 @@ ColumnLayout { text: "Interfaces" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.colorOnSurface + color: Colors.mOnSurface } NToggle { diff --git a/Modules/Settings/Tabs/ScreenRecorder.qml b/Modules/Settings/Tabs/ScreenRecorder.qml index 2bc9b2b..6240ae1 100644 --- a/Modules/Settings/Tabs/ScreenRecorder.qml +++ b/Modules/Settings/Tabs/ScreenRecorder.qml @@ -36,7 +36,7 @@ ColumnLayout { text: "Recording" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.colorOnSurface + color: Colors.mOnSurface Layout.bottomMargin: Style.marginSmall * scaling } @@ -87,7 +87,7 @@ ColumnLayout { text: "Video Settings" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.colorOnSurface + color: Colors.mOnSurface Layout.bottomMargin: Style.marginSmall * scaling } @@ -155,7 +155,7 @@ ColumnLayout { text: "Audio Settings" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.colorOnSurface + color: Colors.mOnSurface Layout.bottomMargin: Style.marginSmall * scaling } diff --git a/Modules/Settings/Tabs/TimeWeather.qml b/Modules/Settings/Tabs/TimeWeather.qml index 74d17f5..0847341 100644 --- a/Modules/Settings/Tabs/TimeWeather.qml +++ b/Modules/Settings/Tabs/TimeWeather.qml @@ -36,7 +36,7 @@ ColumnLayout { text: "Location" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.colorOnSurface + color: Colors.mOnSurface Layout.bottomMargin: Style.marginSmall * scaling } @@ -73,7 +73,7 @@ ColumnLayout { text: "Time Format" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.colorOnSurface + color: Colors.mOnSurface Layout.bottomMargin: 8 } @@ -111,7 +111,7 @@ ColumnLayout { text: "Weather" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.colorOnSurface + color: Colors.mOnSurface Layout.bottomMargin: Style.marginSmall * scaling } diff --git a/Modules/Settings/Tabs/Wallpaper.qml b/Modules/Settings/Tabs/Wallpaper.qml index 668ff36..d657b1b 100644 --- a/Modules/Settings/Tabs/Wallpaper.qml +++ b/Modules/Settings/Tabs/Wallpaper.qml @@ -36,7 +36,7 @@ ColumnLayout { text: "Directory" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.colorOnSurface + color: Colors.mOnSurface Layout.bottomMargin: Style.marginSmall * scaling } @@ -78,7 +78,7 @@ ColumnLayout { text: "Automation" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.colorOnSurface + color: Colors.mOnSurface Layout.bottomMargin: Style.marginSmall * scaling } @@ -111,13 +111,13 @@ ColumnLayout { NText { text: "Wallpaper Interval" font.weight: Style.fontWeightBold - color: Colors.colorOnSurface + color: Colors.mOnSurface } NText { text: "How often to change wallpapers automatically (in seconds)" font.pointSize: Style.fontSizeSmall * scaling - color: Colors.colorOnSurface + color: Colors.mOnSurface wrapMode: Text.WordWrap Layout.fillWidth: true } @@ -137,7 +137,7 @@ ColumnLayout { stepSize: 10 value: Settings.data.wallpaper.randomInterval onPressedChanged: Settings.data.wallpaper.randomInterval = Math.round(value) - cutoutColor: Colors.colorSurface + cutoutColor: Colors.mSurface } } } @@ -158,7 +158,7 @@ ColumnLayout { text: "SWWW" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.colorOnSurface + color: Colors.mOnSurface Layout.bottomMargin: 8 } @@ -212,13 +212,13 @@ ColumnLayout { NText { text: "Transition FPS" font.weight: Style.fontWeightBold - color: Colors.colorOnSurface + color: Colors.mOnSurface } NText { text: "Frames per second for transition animations" font.pointSize: Style.fontSizeSmall * scaling - color: Colors.colorOnSurface + color: Colors.mOnSurface wrapMode: Text.WordWrap Layout.fillWidth: true } @@ -238,7 +238,7 @@ ColumnLayout { stepSize: 5 value: Settings.data.wallpaper.swww.transitionFps onPressedChanged: Settings.data.wallpaper.swww.transitionFps = Math.round(value) - cutoutColor: Colors.colorSurface + cutoutColor: Colors.mSurface } } @@ -251,13 +251,13 @@ ColumnLayout { NText { text: "Transition Duration" font.weight: Style.fontWeightBold - color: Colors.colorOnSurface + color: Colors.mOnSurface } NText { text: "Duration of transition animations in seconds" font.pointSize: Style.fontSizeSmall * scaling - color: Colors.colorOnSurface + color: Colors.mOnSurface wrapMode: Text.WordWrap Layout.fillWidth: true } @@ -277,7 +277,7 @@ ColumnLayout { stepSize: 0.05 value: Settings.data.wallpaper.swww.transitionDuration onPressedChanged: Settings.data.wallpaper.swww.transitionDuration = value - cutoutColor: Colors.colorSurface + cutoutColor: Colors.mSurface } } } diff --git a/Modules/Settings/Tabs/WallpaperSelector.qml b/Modules/Settings/Tabs/WallpaperSelector.qml index 3caa2b7..46e1e40 100644 --- a/Modules/Settings/Tabs/WallpaperSelector.qml +++ b/Modules/Settings/Tabs/WallpaperSelector.qml @@ -32,15 +32,15 @@ Item { text: "Current Wallpaper" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.colorOnSurface + color: Colors.mOnSurface } Rectangle { Layout.fillWidth: true Layout.preferredHeight: 120 * scaling radius: Style.radiusMedium * scaling - color: Colors.colorSurface - border.color: Colors.colorOutline + color: Colors.mSurface + border.color: Colors.mOutline border.width: Math.max(1, Style.borderThin * scaling) clip: true @@ -50,7 +50,7 @@ Item { anchors.margins: Style.marginSmall * scaling imagePath: Wallpapers.currentWallpaper fallbackIcon: "image" - borderColor: Colors.colorOutline + borderColor: Colors.mOutline borderWidth: Math.max(1, Style.borderThin * scaling) imageRadius: Style.radiusMedium * scaling } @@ -73,12 +73,12 @@ Item { text: "Wallpaper Selector" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.colorOnSurface + color: Colors.mOnSurface } NText { text: "Click on a wallpaper to set it as your current wallpaper" - color: Colors.colorOnSurface + color: Colors.mOnSurface wrapMode: Text.WordWrap Layout.fillWidth: true } @@ -86,7 +86,7 @@ Item { NText { text: Settings.data.wallpaper.swww.enabled ? "Wallpapers will change with " + Settings.data.wallpaper.swww.transitionType + " transition" : "Wallpapers will change instantly" - color: Colors.colorOnSurface + color: Colors.mOnSurface font.pointSize: Style.fontSizeSmall * scaling visible: Settings.data.wallpaper.swww.enabled } @@ -147,8 +147,8 @@ Item { width: wallpaperGridView.itemSize height: Math.floor(wallpaperGridView.itemSize * 0.67) radius: Style.radiusMedium * scaling - color: isSelected ? Colors.colorPrimary : Colors.colorSurface - border.color: isSelected ? Colors.colorSecondary : Colors.colorOutline + color: isSelected ? Colors.mPrimary : Colors.mSurface + border.color: isSelected ? Colors.mSecondary : Colors.mOutline border.width: Math.max(1, Style.borderThin * scaling) clip: true @@ -170,8 +170,8 @@ Item { width: 20 * scaling height: 20 * scaling radius: width / 2 - color: Colors.colorPrimary - border.color: Colors.colorOutline + color: Colors.mPrimary + border.color: Colors.mOutline border.width: Math.max(1, Style.borderThin * scaling) visible: isSelected @@ -180,14 +180,14 @@ Item { text: "check" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeSmall * scaling - color: Colors.colorOnPrimary + color: Colors.mOnPrimary } } // Hover effect Rectangle { anchors.fill: parent - color: Colors.colorOnSurface + color: Colors.mOnSurface opacity: mouseArea.containsMouse ? 0.1 : 0 radius: parent.radius @@ -213,9 +213,9 @@ Item { // Empty state Rectangle { anchors.fill: parent - color: Colors.colorSurface + color: Colors.mSurface radius: Style.radiusMedium * scaling - border.color: Colors.colorOutline + border.color: Colors.mOutline border.width: Math.max(1, Style.borderThin * scaling) visible: folderModel.count === 0 && !Wallpapers.scanning @@ -227,20 +227,20 @@ Item { text: "folder_open" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeLarge * scaling - color: Colors.colorOnSurface + color: Colors.mOnSurface Layout.alignment: Qt.AlignHCenter } NText { text: "No wallpapers found" - color: Colors.colorOnSurface + color: Colors.mOnSurface font.weight: Style.fontWeightBold Layout.alignment: Qt.AlignHCenter } NText { text: "Make sure your wallpaper directory is configured and contains image files" - color: Colors.colorOnSurface + color: Colors.mOnSurface wrapMode: Text.WordWrap horizontalAlignment: Text.AlignHCenter Layout.preferredWidth: 300 * scaling diff --git a/Modules/SidePanel/Cards/MediaCard.qml b/Modules/SidePanel/Cards/MediaCard.qml index 1397c2d..d1d7312 100644 --- a/Modules/SidePanel/Cards/MediaCard.qml +++ b/Modules/SidePanel/Cards/MediaCard.qml @@ -40,12 +40,12 @@ NBox { text: "album" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXXL * 2.5 * scaling - color: Colors.colorOnSurfaceVariant + color: Colors.mOnSurfaceVariant Layout.alignment: Qt.AlignHCenter } NText { text: "No media player detected" - color: Colors.colorOnSurfaceVariant + color: Colors.mOnSurfaceVariant Layout.alignment: Qt.AlignHCenter } @@ -76,7 +76,7 @@ NBox { // implicitWidth: 120 * scaling // implicitHeight: 30 * scaling color: "transparent" - border.color: playerSelector.activeFocus ? Colors.colorTertiary : Colors.colorOutline + border.color: playerSelector.activeFocus ? Colors.mTertiary : Colors.mOutline border.width: Math.max(1, Style.borderThin * scaling) radius: Style.radiusMedium * scaling } @@ -87,7 +87,7 @@ NBox { rightPadding: playerSelector.indicator.width + playerSelector.spacing text: playerSelector.displayText font.pointSize: Style.fontSizeSmall * scaling - color: Colors.colorOnSurface + color: Colors.mOnSurface verticalAlignment: Text.AlignVCenter elide: Text.ElideRight } @@ -98,7 +98,7 @@ NBox { text: "arrow_drop_down" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXL * scaling - color: Colors.colorOnSurface + color: Colors.mOnSurface horizontalAlignment: Text.AlignRight } @@ -119,8 +119,8 @@ NBox { } background: Rectangle { - color: Colors.colorSurface - border.color: Colors.colorOutline + color: Colors.mSurface + border.color: Colors.mOutline border.width: Math.max(1, Style.borderThin * scaling) radius: Style.radiusTiny * scaling } @@ -131,7 +131,7 @@ NBox { contentItem: NText { text: modelData.identity font.pointSize: Style.fontSizeSmall * scaling - color: highlighted ? Colors.colorSurface : Colors.colorOnSurface + color: highlighted ? Colors.mSurface : Colors.mOnSurface verticalAlignment: Text.AlignVCenter elide: Text.ElideRight } @@ -139,7 +139,7 @@ NBox { background: Rectangle { width: popup.width - Style.marginSmall * scaling * 2 - color: highlighted ? Colors.colorTertiary : "transparent" + color: highlighted ? Colors.mTertiary : "transparent" radius: Style.radiusTiny * scaling } } @@ -160,8 +160,8 @@ NBox { width: 90 * scaling height: 90 * scaling radius: width * 0.5 - color: trackArt.visible ? Colors.colorPrimary : "transparent" - border.color: trackArt.visible ? Colors.colorOutline : "transparent" + color: trackArt.visible ? Colors.mPrimary : "transparent" + border.color: trackArt.visible ? Colors.mOutline : "transparent" border.width: Math.max(1, Style.borderThin * scaling) clip: true @@ -173,7 +173,7 @@ NBox { anchors.margins: Style.marginTiny * scaling imagePath: MediaPlayer.trackArtUrl fallbackIcon: "image" - borderColor: Colors.colorOutline + borderColor: Colors.mOutline borderWidth: Math.max(1, Style.borderThin * scaling) imageRadius: width * 0.5 } @@ -208,7 +208,7 @@ NBox { NText { visible: MediaPlayer.trackArtist !== "" text: MediaPlayer.trackArtist - color: Colors.colorOnSurface + color: Colors.mOnSurface font.pointSize: Style.fontSizeSmall * scaling elide: Text.ElideRight Layout.fillWidth: true @@ -217,7 +217,7 @@ NBox { NText { visible: MediaPlayer.trackAlbum !== "" text: MediaPlayer.trackAlbum - color: Colors.colorOnSurface + color: Colors.mOnSurface font.pointSize: Style.fontSizeSmall * scaling elide: Text.ElideRight Layout.fillWidth: true @@ -232,7 +232,7 @@ NBox { width: parent.width height: 4 * scaling radius: Style.radiusSmall * scaling - color: Colors.colorSurfaceVariant + color: Colors.mSurfaceVariant Layout.fillWidth: true property real progressRatio: { @@ -247,7 +247,7 @@ NBox { width: progressBarBackground.progressRatio * parent.width height: parent.height radius: parent.radius - color: Colors.colorPrimary + color: Colors.mPrimary Behavior on width { NumberAnimation { @@ -262,8 +262,8 @@ NBox { width: 16 * scaling height: 16 * scaling radius: width * 0.5 - color: Colors.colorPrimary - border.color: Colors.colorSurface + color: Colors.mPrimary + border.color: Colors.mSurface border.width: Math.max(1 * Style.borderMedium * scaling) x: Math.max(0, Math.min(parent.width - width, progressFill.width - width / 2)) @@ -336,7 +336,7 @@ NBox { width: 300 * scaling height: 80 * scaling values: Cava.values - fillColor: Colors.colorOnSurface + fillColor: Colors.mOnSurface Layout.alignment: Qt.AlignHCenter } } @@ -346,8 +346,8 @@ NBox { // values: Cava.values // innerRadius: 30 * scaling // Position just outside 60x60 album art // outerRadius: 48 * scaling // Extend bars outward from album art - // fillColor: Colors.colorPrimary - // strokeColor: Colors.colorPrimary + // fillColor: Colors.mPrimary + // strokeColor: Colors.mPrimary // strokeWidth: 0 * scaling // } } diff --git a/Modules/SidePanel/Cards/ProfileCard.qml b/Modules/SidePanel/Cards/ProfileCard.qml index ec74814..b1b1bbf 100644 --- a/Modules/SidePanel/Cards/ProfileCard.qml +++ b/Modules/SidePanel/Cards/ProfileCard.qml @@ -33,7 +33,7 @@ NBox { height: Style.baseWidgetSize * 1.25 * scaling imagePath: Settings.data.general.avatarImage fallbackIcon: "person" - borderColor: Colors.colorPrimary + borderColor: Colors.mPrimary borderWidth: Math.max(1, Style.borderMedium * scaling) } @@ -46,7 +46,7 @@ NBox { } NText { text: `System Uptime: ${uptimeText}` - color: Colors.colorOnSurface + color: Colors.mOnSurface } } diff --git a/Modules/SidePanel/Cards/WeatherCard.qml b/Modules/SidePanel/Cards/WeatherCard.qml index 48eb790..9a3a84e 100644 --- a/Modules/SidePanel/Cards/WeatherCard.qml +++ b/Modules/SidePanel/Cards/WeatherCard.qml @@ -30,7 +30,7 @@ NBox { text: weatherReady ? Location.weatherSymbolFromCode(Location.data.weather.current_weather.weathercode) : "" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXXL * 1.5 * scaling - color: Colors.colorPrimary + color: Colors.mPrimary } ColumnLayout { @@ -91,13 +91,13 @@ NBox { spacing: Style.marginSmall * scaling NText { text: Qt.formatDateTime(new Date(Location.data.weather.daily.time[index]), "ddd") - color: Colors.colorOnSurface + color: Colors.mOnSurface } NText { text: Location.weatherSymbolFromCode(Location.data.weather.daily.weathercode[index]) font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXL * scaling - color: Colors.colorPrimary + color: Colors.mPrimary } NText { text: { @@ -112,7 +112,7 @@ NBox { return `${max}°/${min}°` } font.pointSize: Style.fontSizeSmall * scaling - color: Colors.colorOnSurfaceVariant + color: Colors.mOnSurfaceVariant } } } diff --git a/Modules/SidePanel/PowerMenu.qml b/Modules/SidePanel/PowerMenu.qml index 9cbe954..7a434f5 100644 --- a/Modules/SidePanel/PowerMenu.qml +++ b/Modules/SidePanel/PowerMenu.qml @@ -25,9 +25,9 @@ NPanel { width: 160 * scaling height: 220 * scaling radius: Style.radiusMedium * scaling - border.color: Colors.colorOutline + border.color: Colors.mOutline border.width: Math.max(1, Style.borderThin * scaling) - color: Colors.colorSurface + color: Colors.mSurface visible: true z: 9999 @@ -56,7 +56,7 @@ NPanel { Layout.fillWidth: true Layout.preferredHeight: 36 * scaling radius: Style.radiusSmall * scaling - color: lockButtonArea.containsMouse ? Colors.colorTertiary : "transparent" + color: lockButtonArea.containsMouse ? Colors.mTertiary : "transparent" Item { anchors.left: parent.left @@ -79,7 +79,7 @@ NPanel { font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 } - color: lockButtonArea.containsMouse ? Colors.colorOnTertiary : Colors.colorOnSurface + color: lockButtonArea.containsMouse ? Colors.mOnTertiary : Colors.mOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -87,7 +87,7 @@ NPanel { Text { text: "Lock Screen" - color: lockButtonArea.containsMouse ? Colors.colorOnTertiary : Colors.colorOnSurface + color: lockButtonArea.containsMouse ? Colors.mOnTertiary : Colors.mOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -116,7 +116,7 @@ NPanel { Layout.fillWidth: true Layout.preferredHeight: 36 * scaling radius: Style.radiusSmall * scaling - color: suspendButtonArea.containsMouse ? Colors.colorTertiary : "transparent" + color: suspendButtonArea.containsMouse ? Colors.mTertiary : "transparent" Item { anchors.left: parent.left @@ -139,7 +139,7 @@ NPanel { font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 } - color: suspendButtonArea.containsMouse ? Colors.colorOnTertiary : Colors.colorOnSurface + color: suspendButtonArea.containsMouse ? Colors.mOnTertiary : Colors.mOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -147,7 +147,7 @@ NPanel { Text { text: "Suspend" - color: suspendButtonArea.containsMouse ? Colors.colorOnTertiary : Colors.colorOnSurface + color: suspendButtonArea.containsMouse ? Colors.mOnTertiary : Colors.mOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -174,7 +174,7 @@ NPanel { Layout.fillWidth: true Layout.preferredHeight: 36 * scaling radius: Style.radiusSmall * scaling - color: rebootButtonArea.containsMouse ? Colors.colorTertiary : "transparent" + color: rebootButtonArea.containsMouse ? Colors.mTertiary : "transparent" Item { anchors.left: parent.left @@ -197,7 +197,7 @@ NPanel { font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 } - color: rebootButtonArea.containsMouse ? Colors.colorOnTertiary : Colors.colorOnSurface + color: rebootButtonArea.containsMouse ? Colors.mOnTertiary : Colors.mOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -205,7 +205,7 @@ NPanel { Text { text: "Reboot" - color: rebootButtonArea.containsMouse ? Colors.colorOnTertiary : Colors.colorOnSurface + color: rebootButtonArea.containsMouse ? Colors.mOnTertiary : Colors.mOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -232,7 +232,7 @@ NPanel { Layout.fillWidth: true Layout.preferredHeight: 36 * scaling radius: Style.radiusSmall * scaling - color: logoutButtonArea.containsMouse ? Colors.colorTertiary : "transparent" + color: logoutButtonArea.containsMouse ? Colors.mTertiary : "transparent" Item { anchors.left: parent.left @@ -255,7 +255,7 @@ NPanel { font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 } - color: logoutButtonArea.containsMouse ? Colors.colorOnTertiary : Colors.colorOnSurface + color: logoutButtonArea.containsMouse ? Colors.mOnTertiary : Colors.mOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -263,7 +263,7 @@ NPanel { Text { text: "Logout" - color: logoutButtonArea.containsMouse ? Colors.colorOnTertiary : Colors.colorOnSurface + color: logoutButtonArea.containsMouse ? Colors.mOnTertiary : Colors.mOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -290,7 +290,7 @@ NPanel { Layout.fillWidth: true Layout.preferredHeight: 36 * scaling radius: Style.radiusSmall * scaling - color: shutdownButtonArea.containsMouse ? Colors.colorTertiary : "transparent" + color: shutdownButtonArea.containsMouse ? Colors.mTertiary : "transparent" Item { anchors.left: parent.left @@ -313,7 +313,7 @@ NPanel { font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 } - color: shutdownButtonArea.containsMouse ? Colors.colorOnTertiary : Colors.colorOnSurface + color: shutdownButtonArea.containsMouse ? Colors.mOnTertiary : Colors.mOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -321,7 +321,7 @@ NPanel { Text { text: "Shutdown" - color: shutdownButtonArea.containsMouse ? Colors.colorOnTertiary : Colors.colorOnSurface + color: shutdownButtonArea.containsMouse ? Colors.mOnTertiary : Colors.mOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml index 0d6962c..e9d89cb 100644 --- a/Modules/SidePanel/SidePanel.qml +++ b/Modules/SidePanel/SidePanel.qml @@ -81,9 +81,9 @@ NLoader { // Inline helpers moved to dedicated widgets: NCard and NCircleStat Rectangle { id: panelBackground - color: Colors.colorSurface + color: Colors.mSurface radius: Style.radiusLarge * scaling - border.color: Colors.colorOutlineVariant + border.color: Colors.mOutlineVariant border.width: Math.max(1, Style.borderThin * scaling) layer.enabled: true width: 460 * scaling diff --git a/Services/Colors.qml b/Services/Colors.qml index 0a2ac85..eef8aae 100644 --- a/Services/Colors.qml +++ b/Services/Colors.qml @@ -6,38 +6,36 @@ import Quickshell.Io import qs.Services // -------------------------------- -// Material3 Colors -// We only use a subset of all materials colors to avoid complexity +// Noctalia Colors - Material Design 3 +// We only use a very small subset of all available m3 colors to avoid complexity + Singleton { id: root + // --- Key Colors: These are the main accent colors that define your app's theme + property color mPrimary: useCustom ? customColors.mPrimary : defaultColors.mPrimary + property color mOnPrimary: useCustom ? customColors.mOnPrimary : defaultColors.mOnPrimary + property color mSecondary: useCustom ? customColors.mSecondary : defaultColors.mSecondary + property color mOnSecondary: useCustom ? customColors.mOnSecondary : defaultColors.mOnSecondary + property color mTertiary: useCustom ? customColors.mTertiary : defaultColors.mTertiary + property color mOnTertiary: useCustom ? customColors.mOnTertiary : defaultColors.mOnTertiary + + // --- Utility Colors: These colors serve specific, universal purposes like indicating errors + property color mError: useCustom ? customColors.mError : defaultColors.mError + property color mOnError: useCustom ? customColors.mOnError : defaultColors.mOnError + + // --- Surface and Variant Colors: These provide additional options for surfaces and their contents, creating visual hierarchy + property color mSurface: useCustom ? customColors.mSurface : defaultColors.mSurface + property color mOnSurface: useCustom ? customColors.mOnSurface : defaultColors.mOnSurface + property color mSurfaceVariant: useCustom ? customColors.mSurfaceVariant : defaultColors.mSurfaceVariant + property color mOnSurfaceVariant: useCustom ? customColors.mOnSurfaceVariant : defaultColors.mOnSurfaceVariant + property color mOutline: useCustom ? customColors.mOutline : defaultColors.mOutline + property color mOutlineVariant: useCustom ? customColors.mOutlineVariant : defaultColors.mOutlineVariant + property color mShadow: useCustom ? customColors.mShadow : defaultColors.mShadow + // ----------- // Check if we should use custom colors - property bool useCustom: Settings.data.wallpaper.generateColors && customColorsFile.loaded - - // --- Key Colors - property color colorPrimary: useCustom ? customColors.colorPrimary : defaultTheme.colorPrimary - property color colorOnPrimary: useCustom ? customColors.colorOnPrimary : defaultTheme.colorOnPrimary - - property color colorSecondary: useCustom ? customColors.colorSecondary : defaultTheme.colorSecondary - property color colorOnSecondary: useCustom ? customColors.colorOnSecondary : defaultTheme.colorOnSecondary - - property color colorTertiary: useCustom ? customColors.colorTertiary : defaultTheme.colorTertiary - property color colorOnTertiary: useCustom ? customColors.colorOnTertiary : defaultTheme.colorOnTertiary - - // --- Utility Colors - property color colorError: useCustom ? customColors.colorError : defaultTheme.colorError - property color colorOnError: useCustom ? customColors.colorOnError : defaultTheme.colorOnError - - // --- Surface and Variant Colors - property color colorSurface: useCustom ? customColors.colorSurface : defaultTheme.colorSurface - property color colorOnSurface: useCustom ? customColors.colorOnSurface : defaultTheme.colorOnSurface - property color colorSurfaceVariant: useCustom ? customColors.colorSurfaceVariant : defaultTheme.colorSurfaceVariant - property color colorOnSurfaceVariant: useCustom ? customColors.colorOnSurfaceVariant : defaultTheme.colorOnSurfaceVariant - property color colorOutline: useCustom ? customColors.colorOutline : defaultTheme.colorOutline - property color colorOutlineVariant: useCustom ? customColors.colorOutlineVariant : defaultTheme.colorOutlineVariant - property color colorShadow: useCustom ? customColors.colorShadow : defaultTheme.colorShadow - + property bool useCustom: (Settings.data.wallpaper.generateColors && customColorsFile.loaded) // ----------- function applyOpacity(color, opacity) { @@ -46,63 +44,53 @@ Singleton { } // -------------------------------- - // Default theme colors - RosePine + // Default colors: RosePine QtObject { - id: defaultTheme + id: defaultColors - // // --- Key Colors: These are the main accent colors that define your app's theme. - property color colorPrimary: "#ebbcba" // The main brand color, used most frequently. - property color colorOnPrimary: "#191724" // Color for text/icons on a Primary background. + property color mPrimary: "#ebbcba" + property color mOnPrimary: "#191724" + property color mSecondary: "#31748f" + property color mOnSecondary: "#e0def4" + property color mTertiary: "#9ccfd8" + property color mOnTertiary: "#191724" - property color colorSecondary: "#31748f" // An accent color for less prominent components. - property color colorOnSecondary: "#e0def4" // Color for text/icons on a Secondary background. - - property color colorTertiary: "#9ccfd8" // A contrasting accent color used for things like highlights or special actions. - property color colorOnTertiary: "#191724" // Color for text/icons on a Tertiary background. - - // --- Utility colorColors: These colors serve specific, universal purposes like indicating errors or providing neutral backgrounds. - property color colorError: "#eb6f92" // Indicates an error state. - property color colorOnError: "#191724" // Color for text/icons on an Error background. - - // --- Surface colorand Variant Colors: These provide additional options for surfaces and their contents, creating visual hierarchy. - property color colorSurface: "#191724" // The color for component surfaces like cards, sheets, and menus. - property color colorOnSurface: "#e0def4" // The primary color for text/icons on a Surface background. - property color colorSurfaceVariant: "#26233a" // A surface color with a slightly different tint for differentiation. - property color colorOnSurfaceVariant: "#908caa" // The color for less prominent text/icons on a Surface. - property color colorOutline: "#44415a" // The color for component outlines, like text fields or buttons. - property color colorOutlineVariant: "#514e6c" // A subtler outline color for decorative elements or dividers. - property color colorShadow: "#191724" // The color used for shadows to create elevation. + property color mError: "#eb6f92" + property color mOnError: "#191724" + + property color mSurface: "#191724" + property color mOnSurface: "#e0def4" + property color mSurfaceVariant: "#26233a" + property color mOnSurfaceVariant: "#908caa" + property color mOutline: "#44415a" + property color mOutlineVariant: "#514e6c" + property color mShadow: "#191724" } // ---------------------------------------------------------------- - // Custom colors (loaded from colors.json) + // Custom colors loaded from colors.json // These can be generated by matugen or simply come from a well know color scheme (Dracula, Gruvbox, Nord, ...) QtObject { id: customColors - // --- Key Colors - property color colorPrimary: customColorsData.colorPrimary - property color colorOnPrimary: customColorsData.colorOnPrimary + property color mPrimary: customColorsData.mPrimary + property color mOnPrimary: customColorsData.mOnPrimary + property color mSecondary: customColorsData.mSecondary + property color mOnSecondary: customColorsData.mOnSecondary + property color mTertiary: customColorsData.mTertiary + property color mOnTertiary: customColorsData.mOnTertiary - property color colorSecondary: customColorsData.colorSecondary - property color colorOnSecondary: customColorsData.colorOnSecondary + property color mError: customColorsData.mError + property color mOnError: customColorsData.mOnError - property color colorTertiary: customColorsData.colorTertiary - property color colorOnTertiary: customColorsData.colorOnTertiary - - // --- Utility Colors - property color colorError: customColorsData.colorError - property color colorOnError: customColorsData.colorOnError - - // --- Surface and Variant Colors - property color colorSurface: customColorsData.colorSurface - property color colorOnSurface: customColorsData.colorOnSurface - property color colorSurfaceVariant: customColorsData.colorSurfaceVariant - property color colorOnSurfaceVariant: customColorsData.colorOnSurfaceVariant - property color colorOutline: customColorsData.colorOutline - property color colorOutlineVariant: customColorsData.colorOutlineVariant - property color colorShadow: customColorsData.colorShadow + property color mSurface: customColorsData.mSurface + property color mOnSurface: customColorsData.mOnSurface + property color mSurfaceVariant: customColorsData.mSurfaceVariant + property color mOnSurfaceVariant: customColorsData.mOnSurfaceVariant + property color mOutline: customColorsData.mOutline + property color mOutlineVariant: customColorsData.mOutlineVariant + property color mShadow: customColorsData.mShadow } // FileView to load custom colors data from colors.json @@ -121,28 +109,23 @@ Singleton { JsonAdapter { id: customColorsData - // --- Key Colors - property color colorPrimary: defaultTheme.colorPrimary - property color colorOnPrimary: defaultTheme.colorOnPrimary + property color mPrimary: defaultColors.mPrimary + property color mOnPrimary: defaultColors.mOnPrimary + property color mSecondary: defaultColors.mSecondary + property color mOnSecondary: defaultColors.mOnSecondary + property color mTertiary: defaultColors.mTertiary + property color mOnTertiary: defaultColors.mOnTertiary - property color colorSecondary: defaultTheme.colorSecondary - property color colorOnSecondary: defaultTheme.colorOnSecondary + property color mError: defaultColors.mError + property color mOnError: defaultColors.mOnError - property color colorTertiary: defaultTheme.colorTertiary - property color colorOnTertiary: defaultTheme.colorOnTertiary - - // --- Utility Colors - property color colorError: defaultTheme.colorError - property color colorOnError: defaultTheme.colorOnError - - // --- Surface and Variant Colors - property color colorSurface: defaultTheme.colorSurface - property color colorOnSurface: defaultTheme.colorOnSurface - property color colorSurfaceVariant: defaultTheme.colorSurfaceVariant - property color colorOnSurfaceVariant: defaultTheme.colorOnSurfaceVariant - property color colorOutline: defaultTheme.colorOutline - property color colorOutlineVariant: defaultTheme.colorOutlineVariant - property color colorShadow: defaultTheme.colorShadow + property color mSurface: defaultColors.mSurface + property color mOnSurface: defaultColors.mOnSurface + property color mSurfaceVariant: defaultColors.mSurfaceVariant + property color mOnSurfaceVariant: defaultColors.mOnSurfaceVariant + property color mOutline: defaultColors.mOutline + property color mOutlineVariant: defaultColors.mOutlineVariant + property color mShadow: defaultColors.mShadow } } } diff --git a/Widgets/NBox.qml b/Widgets/NBox.qml index 310f1fe..d418638 100644 --- a/Widgets/NBox.qml +++ b/Widgets/NBox.qml @@ -11,9 +11,9 @@ Rectangle { implicitWidth: childrenRect.width implicitHeight: childrenRect.height - color: Colors.colorSurfaceVariant + color: Colors.mSurfaceVariant radius: Style.radiusMedium * scaling - border.color: Colors.colorOutlineVariant + border.color: Colors.mOutlineVariant border.width: Math.max(1, Style.borderThin * scaling) clip: true } diff --git a/Widgets/NBusyIndicator.qml b/Widgets/NBusyIndicator.qml index 3164fe9..7c2b2da 100644 --- a/Widgets/NBusyIndicator.qml +++ b/Widgets/NBusyIndicator.qml @@ -7,7 +7,7 @@ Item { readonly property real scaling: Scaling.scale(screen) property bool running: true - property color color: Colors.colorPrimary + property color color: Colors.mPrimary property int size: Style.baseWidgetSize * scaling property int strokeWidth: Style.borderThick * scaling property int duration: 1000 diff --git a/Widgets/NCard.qml b/Widgets/NCard.qml index 3361305..19e162c 100644 --- a/Widgets/NCard.qml +++ b/Widgets/NCard.qml @@ -10,8 +10,8 @@ Rectangle { implicitWidth: childrenRect.width implicitHeight: childrenRect.height - color: Colors.colorSurface + color: Colors.mSurface radius: Style.radiusMedium * scaling - border.color: Colors.colorSurfaceVariant + border.color: Colors.mSurfaceVariant border.width: Math.max(1, Style.borderThin * scaling) } diff --git a/Widgets/NCircleStat.qml b/Widgets/NCircleStat.qml index 4909d62..46fc207 100644 --- a/Widgets/NCircleStat.qml +++ b/Widgets/NCircleStat.qml @@ -18,9 +18,9 @@ Rectangle { width: 68 * scaling height: 92 * scaling - color: flat ? "transparent" : Colors.colorSurface + color: flat ? "transparent" : Colors.mSurface radius: Style.radiusSmall * scaling - border.color: flat ? "transparent" : Colors.colorSurfaceVariant + border.color: flat ? "transparent" : Colors.mSurfaceVariant border.width: flat ? 0 : Math.max(1, Style.borderThin * scaling) clip: true @@ -58,14 +58,14 @@ Rectangle { ctx.reset() ctx.lineWidth = 6 * root.scaling * contentScale // Track uses surfaceVariant for stronger contrast - ctx.strokeStyle = Colors.colorSurface + ctx.strokeStyle = Colors.mSurface ctx.beginPath() ctx.arc(cx, cy, r, start, endBg) ctx.stroke() // Value arc const ratio = Math.max(0, Math.min(1, root.value / 100)) const end = start + (endBg - start) * ratio - ctx.strokeStyle = Colors.colorPrimary + ctx.strokeStyle = Colors.mPrimary ctx.beginPath() ctx.arc(cx, cy, r, start, end) ctx.stroke() @@ -79,7 +79,7 @@ Rectangle { text: `${root.value}${root.suffix}` font.pointSize: Style.fontSizeMedium * scaling * contentScale font.weight: Style.fontWeightBold - color: Colors.colorOnSurface + color: Colors.mOnSurface horizontalAlignment: Text.AlignHCenter } @@ -89,8 +89,8 @@ Rectangle { width: 28 * scaling * contentScale height: width radius: width / 2 - color: Colors.colorSurface - // border.color: Colors.colorPrimary + color: Colors.mSurface + // border.color: Colors.mPrimary // border.width: Math.max(1, Style.borderThin * scaling) anchors.right: parent.right anchors.top: parent.top @@ -102,7 +102,7 @@ Rectangle { text: root.icon font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeLargeXL * scaling * contentScale - color: Colors.colorOnSurface + color: Colors.mOnSurface horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } diff --git a/Widgets/NComboBox.qml b/Widgets/NComboBox.qml index 8b37d5b..f489011 100644 --- a/Widgets/NComboBox.qml +++ b/Widgets/NComboBox.qml @@ -29,13 +29,13 @@ ColumnLayout { text: label font.pointSize: Style.fontSizeMedium * scaling font.weight: Style.fontWeightBold - color: Colors.colorOnSurface + color: Colors.mOnSurface } NText { text: description font.pointSize: Style.fontSizeSmall * scaling - color: Colors.colorOnSurface + color: Colors.mOnSurface wrapMode: Text.WordWrap } } @@ -56,8 +56,8 @@ ColumnLayout { background: Rectangle { implicitWidth: 120 * scaling implicitHeight: preferredHeight - color: Colors.colorSurface - border.color: combo.activeFocus ? Colors.colorTertiary : Colors.colorOutline + color: Colors.mSurface + border.color: combo.activeFocus ? Colors.mTertiary : Colors.mOutline border.width: Math.max(1, Style.borderThin * scaling) radius: Style.radiusMedium * scaling } @@ -97,8 +97,8 @@ ColumnLayout { } background: Rectangle { - color: Colors.colorSurfaceVariant - border.color: Colors.colorOutline + color: Colors.mSurfaceVariant + border.color: Colors.mOutline border.width: Math.max(1, Style.borderThin * scaling) radius: Style.radiusMedium * scaling } @@ -112,14 +112,14 @@ ColumnLayout { text: (combo.model.indexOf(modelData) >= 0 && combo.model.indexOf( modelData) < root.optionsLabels.length) ? root.optionsLabels[combo.model.indexOf(modelData)] : "" font.pointSize: Style.fontSizeMedium * scaling - color: highlighted ? Colors.colorSurface : Colors.colorOnSurface + color: highlighted ? Colors.mSurface : Colors.mOnSurface verticalAlignment: Text.AlignVCenter elide: Text.ElideRight } background: Rectangle { width: combo.width - Style.marginMedium * scaling * 3 - color: highlighted ? Colors.colorTertiary : "transparent" + color: highlighted ? Colors.mTertiary : "transparent" radius: Style.radiusSmall * scaling } } diff --git a/Widgets/NDivider.qml b/Widgets/NDivider.qml index 5cb2ea0..7b82fe2 100644 --- a/Widgets/NDivider.qml +++ b/Widgets/NDivider.qml @@ -6,5 +6,5 @@ import qs.Services Rectangle { width: parent.width height: Math.max(1, Style.borderThin * scaling) - color: Colors.colorOutline + color: Colors.mOutline } diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml index 9566fcf..3d7f858 100644 --- a/Widgets/NIconButton.qml +++ b/Widgets/NIconButton.qml @@ -26,9 +26,9 @@ Rectangle { implicitWidth: size implicitHeight: size - color: (root.hovering || showFilled) ? Colors.colorPrimary : "transparent" + color: (root.hovering || showFilled) ? Colors.mPrimary : "transparent" radius: width * 0.5 - border.color: showBorder ? Colors.colorPrimary : "transparent" + border.color: showBorder ? Colors.mPrimary : "transparent" border.width: Math.max(1, Style.borderThin * scaling) NText { @@ -43,7 +43,7 @@ Rectangle { "wght": (Font.Normal + Font.Bold) / 2.0 } color: (root.hovering - || showFilled) ? Colors.colorOnPrimary : showBorder ? Colors.colorPrimary : Colors.colorOnSurface + || showFilled) ? Colors.mOnPrimary : showBorder ? Colors.mPrimary : Colors.mOnSurface horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter opacity: root.enabled ? Style.opacityFull : Style.opacityMedium diff --git a/Widgets/NPill.qml b/Widgets/NPill.qml index 65a0752..58bbef3 100644 --- a/Widgets/NPill.qml +++ b/Widgets/NPill.qml @@ -10,11 +10,11 @@ Item { property string icon: "" property string text: "" property string tooltipText: "" - property color pillColor: Colors.colorSurfaceVariant - property color textColor: Colors.colorOnSurface - property color iconCircleColor: Colors.colorPrimary - property color iconTextColor: Colors.colorSurface - property color collapsedIconColor: Colors.colorOnSurface + property color pillColor: Colors.mSurfaceVariant + property color textColor: Colors.mOnSurface + property color iconCircleColor: Colors.mPrimary + property color iconTextColor: Colors.mSurface + property color collapsedIconColor: Colors.mOnSurface property real sizeMultiplier: 0.8 property bool autoHide: false diff --git a/Widgets/NRadioButton.qml b/Widgets/NRadioButton.qml index 14e6f49..ce94040 100644 --- a/Widgets/NRadioButton.qml +++ b/Widgets/NRadioButton.qml @@ -13,7 +13,7 @@ RadioButton { implicitHeight: 20 * scaling radius: width * 0.5 color: "transparent" - border.color: root.checked ? Colors.colorPrimary : Colors.colorOnSurface + border.color: root.checked ? Colors.mPrimary : Colors.mOnSurface border.width: 2 anchors.verticalCenter: parent.verticalCenter @@ -23,7 +23,7 @@ RadioButton { implicitHeight: Style.marginSmall * scaling radius: width * 0.5 - color: Qt.alpha(Colors.colorPrimary, root.checked ? 1 : 0) + color: Qt.alpha(Colors.mPrimary, root.checked ? 1 : 0) } Behavior on border.color { diff --git a/Widgets/NSlider.qml b/Widgets/NSlider.qml index 962af98..13d748f 100644 --- a/Widgets/NSlider.qml +++ b/Widgets/NSlider.qml @@ -27,13 +27,13 @@ Slider { width: root.availableWidth height: implicitHeight radius: height / 2 - color: Colors.colorSurface + color: Colors.mSurface Rectangle { id: activeTrack width: root.visualPosition * parent.width height: parent.height - color: Colors.colorPrimary + color: Colors.mPrimary radius: parent.radius } @@ -43,7 +43,7 @@ Slider { width: knobDiameter + cutoutExtra height: knobDiameter + cutoutExtra radius: width / 2 - color: root.cutoutColor !== undefined ? root.cutoutColor : Colors.colorSurface + color: root.cutoutColor !== undefined ? root.cutoutColor : Colors.mSurface x: Math.max(0, Math.min(parent.width - width, root.visualPosition * (parent.width - root.knobDiameter) - cutoutExtra / 2)) y: (parent.height - height) / 2 @@ -56,12 +56,12 @@ Slider { x: root.leftPadding + root.visualPosition * (root.availableWidth - width) y: root.topPadding + root.availableHeight / 2 - height / 2 - // Subtle shadow for a more polished look (keeps theme colors) + // Subtle shadow for a more polished look MultiEffect { anchors.fill: knob source: knob shadowEnabled: true - shadowColor: Colors.colorShadow + shadowColor: Colors.mShadow shadowOpacity: 0.25 shadowHorizontalOffset: 0 shadowVerticalOffset: 1 @@ -73,8 +73,8 @@ Slider { implicitWidth: knobDiameter implicitHeight: knobDiameter radius: width * 0.5 - color: root.pressed ? Colors.colorSurfaceVariant : Colors.colorSurface - border.color: Colors.colorPrimary + color: root.pressed ? Colors.mSurfaceVariant : Colors.mSurface + border.color: Colors.mPrimary border.width: Math.max(1, Style.borderThick * scaling) // Press feedback halo (using accent color, low opacity) @@ -83,7 +83,7 @@ Slider { width: parent.width + 8 * scaling height: parent.height + 8 * scaling radius: width / 2 - color: Colors.colorPrimary + color: Colors.mPrimary opacity: root.pressed ? 0.16 : 0.0 } } diff --git a/Widgets/NText.qml b/Widgets/NText.qml index d410537..7e8ddb3 100644 --- a/Widgets/NText.qml +++ b/Widgets/NText.qml @@ -10,5 +10,5 @@ Text { font.family: Settings.data.ui.fontFamily font.pointSize: Style.fontSizeMedium * scaling font.weight: Style.fontWeightRegular - color: Colors.colorOnSurface + color: Colors.mOnSurface } diff --git a/Widgets/NTextInput.qml b/Widgets/NTextInput.qml index 5fc9faf..ccfd832 100644 --- a/Widgets/NTextInput.qml +++ b/Widgets/NTextInput.qml @@ -29,13 +29,13 @@ Item { text: label font.pointSize: Style.fontSizeMedium * scaling font.weight: Style.fontWeightBold - color: Colors.colorOnSurface + color: Colors.mOnSurface } NText { text: description font.pointSize: Style.fontSizeSmall * scaling - color: Colors.colorOnSurface + color: Colors.mOnSurface wrapMode: Text.WordWrap Layout.fillWidth: true } @@ -47,8 +47,8 @@ Item { implicitWidth: root.width implicitHeight: Style.baseWidgetSize * 1.35 * scaling radius: Style.radiusMedium * scaling - color: Colors.colorSurface - border.color: Colors.colorOutline + color: Colors.mSurface + border.color: Colors.mOutline border.width: Math.max(1, Style.borderThin * scaling) // Focus ring @@ -56,7 +56,7 @@ Item { anchors.fill: parent radius: frame.radius color: "transparent" - border.color: input.activeFocus ? Colors.colorTertiary : "transparent" + border.color: input.activeFocus ? Colors.mTertiary : "transparent" border.width: input.activeFocus ? Math.max(1, Style.borderThin * scaling) : 0 } @@ -74,8 +74,8 @@ Item { echoMode: TextInput.Normal readOnly: root.readOnly enabled: root.enabled - color: Colors.colorOnSurface - placeholderTextColor: Colors.colorOnSurface + color: Colors.mOnSurface + placeholderTextColor: Colors.mOnSurface background: null font.pointSize: Style.fontSizeSmall * scaling onEditingFinished: root.editingFinished() diff --git a/Widgets/NToggle.qml b/Widgets/NToggle.qml index 217fc9e..eb61dd4 100644 --- a/Widgets/NToggle.qml +++ b/Widgets/NToggle.qml @@ -27,13 +27,13 @@ RowLayout { text: label font.pointSize: Style.fontSizeMedium * scaling font.weight: Style.fontWeightBold - color: Colors.colorOnSurface + color: Colors.mOnSurface } NText { text: description font.pointSize: Style.fontSizeSmall * scaling - color: Colors.colorOnSurface + color: Colors.mOnSurface wrapMode: Text.WordWrap Layout.fillWidth: true } @@ -45,16 +45,16 @@ RowLayout { implicitWidth: root.baseSize * 1.625 * scaling implicitHeight: root.baseSize * scaling radius: height * 0.5 - color: value ? Colors.colorPrimary : Colors.colorSurface - border.color: value ? Colors.colorPrimary : Colors.colorOutline + color: value ? Colors.mPrimary : Colors.mSurface + border.color: value ? Colors.mPrimary : Colors.mOutline border.width: Math.max(1, Style.borderMedium * scaling) Rectangle { implicitWidth: (root.baseSize - 5) * scaling implicitHeight: (root.baseSize - 5) * scaling radius: height * 0.5 - color: value ? Colors.colorOnPrimary: Colors.colorPrimary - border.color: value ? Colors.colorSurface : Colors.colorSurface + color: value ? Colors.mOnPrimary: Colors.mPrimary + border.color: value ? Colors.mSurface : Colors.mSurface border.width: Math.max(1, Style.borderMedium * scaling) y: 2 * scaling x: value ? switcher.width - width - 2 * scaling : 2 * scaling diff --git a/Widgets/NTooltip.qml b/Widgets/NTooltip.qml index 3319083..70d3079 100644 --- a/Widgets/NTooltip.qml +++ b/Widgets/NTooltip.qml @@ -135,8 +135,8 @@ Window { id: tooltipRect anchors.fill: parent radius: Style.radiusMedium * scaling - color: Colors.colorSurface - border.color: Colors.colorOutline + color: Colors.mSurface + border.color: Colors.mOutline border.width: Math.max(1, Style.borderThin * scaling) z: 1 From 92590eada7c73c15cc4ed48fab3577b721622b3f Mon Sep 17 00:00:00 2001 From: quadbyte Date: Thu, 14 Aug 2025 19:56:19 -0400 Subject: [PATCH 285/394] Revised colors comments --- Services/Colors.qml | 3 ++- Widgets/NCard.qml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Services/Colors.qml b/Services/Colors.qml index eef8aae..4d80001 100644 --- a/Services/Colors.qml +++ b/Services/Colors.qml @@ -8,11 +8,12 @@ import qs.Services // -------------------------------- // Noctalia Colors - Material Design 3 // We only use a very small subset of all available m3 colors to avoid complexity +// All color names start with a 'm' to avoid QML assuming some of them are signals (ex: onPrimary) Singleton { id: root - // --- Key Colors: These are the main accent colors that define your app's theme + // --- Key Colors: These are the main accent colors that define your app's style property color mPrimary: useCustom ? customColors.mPrimary : defaultColors.mPrimary property color mOnPrimary: useCustom ? customColors.mOnPrimary : defaultColors.mOnPrimary property color mSecondary: useCustom ? customColors.mSecondary : defaultColors.mSecondary diff --git a/Widgets/NCard.qml b/Widgets/NCard.qml index 19e162c..f468091 100644 --- a/Widgets/NCard.qml +++ b/Widgets/NCard.qml @@ -1,7 +1,7 @@ import QtQuick import qs.Services -// Generic themed card container +// Generic card container Rectangle { id: root From 2748e029881f3115a9a4910b3c41cc9b9ff5f73a Mon Sep 17 00:00:00 2001 From: quadbyte Date: Thu, 14 Aug 2025 21:25:14 -0400 Subject: [PATCH 286/394] Settings: improved tabs managment so changing the model actually reorder them properly + code formatting --- Modules/AppLauncher/AppLauncher.qml | 3 +- Modules/LockScreen/LockScreen.qml | 9 +- Modules/Settings/SettingsPanel.qml | 212 ++++++++++++++++---------- Modules/Settings/Tabs/ColorScheme.qml | 89 +++++++++++ Modules/Settings/Tabs/Misc.qml | 45 ------ Services/Colors.qml | 6 +- Widgets/NIconButton.qml | 3 +- Widgets/NToggle.qml | 2 +- 8 files changed, 229 insertions(+), 140 deletions(-) create mode 100644 Modules/Settings/Tabs/ColorScheme.qml delete mode 100644 Modules/Settings/Tabs/Misc.qml diff --git a/Modules/AppLauncher/AppLauncher.qml b/Modules/AppLauncher/AppLauncher.qml index af3dd3c..3f90006 100644 --- a/Modules/AppLauncher/AppLauncher.qml +++ b/Modules/AppLauncher/AppLauncher.qml @@ -371,8 +371,7 @@ NLoader { property bool isSelected: index === selectedIndex color: (appCardArea.containsMouse || isSelected) ? Qt.darker(Colors.mPrimary, 1.1) : Colors.mSurface - border.color: (appCardArea.containsMouse - || isSelected) ? Colors.mPrimary : "transparent" + border.color: (appCardArea.containsMouse || isSelected) ? Colors.mPrimary : "transparent" border.width: (appCardArea.containsMouse || isSelected) ? 2 : 0 Behavior on color { diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index 7bf4c35..04693c2 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -530,8 +530,7 @@ WlSessionLock { width: 120 * Scaling.scale(screen) height: 40 * Scaling.scale(screen) radius: 12 - color: executeButtonArea.containsMouse ? Colors.mPrimary : Colors.applyOpacity( - Colors.mPrimary, "33") + color: executeButtonArea.containsMouse ? Colors.mPrimary : Colors.applyOpacity(Colors.mPrimary, "33") border.color: Colors.mPrimary border.width: 1 enabled: !lock.authenticating @@ -631,8 +630,7 @@ WlSessionLock { width: 64 * Scaling.scale(screen) height: 64 * Scaling.scale(screen) radius: 32 - color: Qt.rgba(Colors.mError.r, Colors.mError.g, Colors.mError.b, - shutdownArea.containsMouse ? 0.9 : 0.2) + color: Qt.rgba(Colors.mError.r, Colors.mError.g, Colors.mError.b, shutdownArea.containsMouse ? 0.9 : 0.2) border.color: Colors.mError border.width: 2 * Scaling.scale(screen) @@ -689,8 +687,7 @@ WlSessionLock { width: 64 * Scaling.scale(screen) height: 64 * Scaling.scale(screen) radius: 32 - color: Qt.rgba(Colors.mPrimary.r, Colors.mPrimary.g, Colors.mPrimary.b, - rebootArea.containsMouse ? 0.9 : 0.2) + color: Qt.rgba(Colors.mPrimary.r, Colors.mPrimary.g, Colors.mPrimary.b, rebootArea.containsMouse ? 0.9 : 0.2) border.color: Colors.mPrimary border.width: 2 * Scaling.scale(screen) diff --git a/Modules/Settings/SettingsPanel.qml b/Modules/Settings/SettingsPanel.qml index 253ebf7..40bbac2 100644 --- a/Modules/Settings/SettingsPanel.qml +++ b/Modules/Settings/SettingsPanel.qml @@ -10,18 +10,18 @@ import qs.Widgets NLoader { id: root + // Enumerate all the tabs, ordering is NOT relevant enum Tab { - General, - Bar, - TimeWeather, - ScreenRecorder, - Network, + About, Audio, + Bar, Display, + General, + Network, + ScreenRecorder, + TimeWeather, Wallpaper, - WallpaperSelector, - //Misc, - About + WallpaperSelector } property int requestedTab: SettingsPanel.Tab.General @@ -33,6 +33,61 @@ NLoader { readonly property real scaling: Scaling.scale(screen) property int currentTabIndex: 0 + // List of all the tabs, ordering is relevant. + property var tabsModel: [{ + "id": SettingsPanel.Tab.General, + "label": "General", + "icon": "tune", + "source": "Tabs/General.qml" + }, { + "id": SettingsPanel.Tab.Bar, + "label": "Bar", + "icon": "web_asset", + "source": "Tabs/Bar.qml" + }, { + "id": SettingsPanel.Tab.Display, + "label": "Display", + "icon": "monitor", + "source": "Tabs/Display.qml" + }, { + "id": SettingsPanel.Tab.Audio, + "label": "Audio", + "icon": "volume_up", + "source": "Tabs/Audio.qml" + }, { + "id": SettingsPanel.Tab.Network, + "label": "Network", + "icon": "lan", + "source": "Tabs/Network.qml" + }, { + "id": SettingsPanel.Tab.TimeWeather, + "label": "Time & Weather", + "icon": "schedule", + "source": "Tabs/TimeWeather.qml" + }, { + "id": SettingsPanel.Tab.Wallpaper, + "label": "Wallpaper", + "icon": "image", + "source": "Tabs/Wallpaper.qml" + }, { + "id": SettingsPanel.Tab.WallpaperSelector, + "label": "Wallpaper Selector", + "icon": "wallpaper_slideshow", + "source": "Tabs/WallpaperSelector.qml" + }, { + "id": SettingsPanel.Tab.ScreenRecorder, + "label": "Screen Recorder", + "icon": "videocam", + "source": "Tabs/ScreenRecorder.qml" + }, { + "id": SettingsPanel.Tab.About, + "label": "About", + "icon": "info", + "source": "Tabs/About.qml" + }] + + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + // Override hide function to animate first function hide() { // Start hide animation @@ -43,6 +98,69 @@ NLoader { hideTimer.start() } + function getTab(tabId) { + switch (tabId) { + case SettingsPanel.Tab.About: + return tabAbount + case SettingsPanel.Tab.Audio: + return tabAudio + case SettingsPanel.Tab.Bar: + return tabBar + case SettingsPanel.Tab.General: + return tabGeneral + case SettingsPanel.Tab.Network: + return tabNetwork + case SettingsPanel.Tab.ScreenRecorder: + return tabScreenRecorder + case SettingsPanel.Tab.TimeWeather: + return tabTimeWeather + case SettingsPanel.Tab.Wallpaper: + return tabWallpaper + case SettingsPanel.Tab.WallpaperSelector: + return tabWallpaperSelector + default: + return tabGeneral + } + } + + // Wrap each tab in a Component so we can spawn them with flexibility + Component { + id: tabAbount + Tabs.About {} + } + Component { + id: tabAudio + Tabs.Audio {} + } + Component { + id: tabBar + Tabs.Bar {} + } + Component { + id: tabGeneral + Tabs.General {} + } + Component { + id: tabNetwork + Tabs.Network {} + } + Component { + id: tabScreenRecorder + Tabs.ScreenRecorder {} + } + Component { + id: tabTimeWeather + Tabs.TimeWeather {} + } + Component { + id: tabWallpaper + Tabs.Wallpaper {} + } + Component { + id: tabWallpaperSelector + Tabs.WallpaperSelector {} + } + // Connect to NPanel's dismissed signal to handle external close events Connections { target: panel @@ -67,66 +185,6 @@ NLoader { } } - WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand - - property var tabsModel: [{ - "id": SettingsPanel.Tab.General, - "label": "General", - "icon": "tune", - "source": "Tabs/General.qml" - }, { - "id": SettingsPanel.Tab.Bar, - "label": "Bar", - "icon": "web_asset", - "source": "Tabs/Bar.qml" - }, { - "id": SettingsPanel.Tab.TimeWeather, - "label": "Time & Weather", - "icon": "schedule", - "source": "Tabs/TimeWeather.qml" - }, { - "id": SettingsPanel.Tab.ScreenRecorder, - "label": "Screen Recorder", - "icon": "videocam", - "source": "Tabs/ScreenRecorder.qml" - }, { - "id": SettingsPanel.Tab.Network, - "label": "Network", - "icon": "lan", - "source": "Tabs/Network.qml" - }, { - "id": SettingsPanel.Tab.Audio, - "label": "Audio", - "icon": "volume_up", - "source": "Tabs/Audio.qml" - }, { - "id": SettingsPanel.Tab.Display, - "label": "Display", - "icon": "monitor", - "source": "Tabs/Display.qml" - }, { - "id": SettingsPanel.Tab.Wallpaper, - "label": "Wallpaper", - "icon": "image", - "source": "Tabs/Wallpaper.qml" - }, { - "id": SettingsPanel.Tab.WallpaperSelector, - "label": "Wallpaper Selector", - "icon": "wallpaper_slideshow", - "source": "Tabs/WallpaperSelector.qml" - }, // { - // "id": SettingsPanel.Tab.Misc, - // "label": "Misc", - // "icon": "more_horiz", - // "source": "Tabs/Misc.qml" - // }, - { - "id": SettingsPanel.Tab.About, - "label": "About", - "icon": "info", - "source": "Tabs/About.qml" - }] - Component.onCompleted: { show() } @@ -220,7 +278,6 @@ NLoader { spacing: Style.marginTiny * 1.5 * scaling // Minimal spacing between tabs Repeater { - id: sections model: panel.tabsModel delegate: Rectangle { @@ -328,17 +385,12 @@ NLoader { Layout.fillHeight: true currentIndex: currentTabIndex - Tabs.General {} - Tabs.Bar {} - Tabs.TimeWeather {} - Tabs.ScreenRecorder {} - Tabs.Network {} - Tabs.Audio {} - Tabs.Display {} - Tabs.Wallpaper {} - Tabs.WallpaperSelector {} - //Tabs.Misc {} - Tabs.About {} + Repeater { + model: panel.tabsModel + delegate: Loader { + sourceComponent: getTab(modelData.id) + } + } } } } diff --git a/Modules/Settings/Tabs/ColorScheme.qml b/Modules/Settings/Tabs/ColorScheme.qml new file mode 100644 index 0000000..cd49a77 --- /dev/null +++ b/Modules/Settings/Tabs/ColorScheme.qml @@ -0,0 +1,89 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Services +import qs.Widgets + +ColumnLayout { + id: root + + spacing: 0 + + ScrollView { + id: scrollView + + Layout.fillWidth: true + Layout.fillHeight: true + padding: Style.marginMedium * scaling + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + + ColumnLayout { + width: scrollView.availableWidth + spacing: 0 + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 0 + } + + ColumnLayout { + spacing: Style.marginLarge * scaling + Layout.fillWidth: true + + NText { + text: "Components" + font.pointSize: Style.fontSizeXL * scaling + font.weight: Style.fontWeightBold + color: Colors.mOnSurface + } + + NToggle { + label: "Show Active Window" + description: "Display the title of the currently focused window below the bar" + value: Settings.data.bar.showActiveWindow + onToggled: function (newValue) { + Settings.data.bar.showActiveWindow = newValue + } + } + + NToggle { + label: "Show Active Window Icon" + description: "Display the icon of the currently focused window" + value: Settings.data.bar.showActiveWindowIcon + onToggled: function (newValue) { + Settings.data.bar.showActiveWindowIcon = newValue + } + } + + NToggle { + label: "Show System Info" + description: "Display system information (CPU, RAM, Temperature)" + value: Settings.data.bar.showSystemInfo + onToggled: function (newValue) { + Settings.data.bar.showSystemInfo = newValue + } + } + + NToggle { + label: "Show Taskbar" + description: "Display a taskbar showing currently open windows" + value: Settings.data.bar.showTaskbar + onToggled: function (newValue) { + Settings.data.bar.showTaskbar = newValue + } + } + + NToggle { + label: "Show Media" + description: "Display media controls and information" + value: Settings.data.bar.showMedia + onToggled: function (newValue) { + Settings.data.bar.showMedia = newValue + } + } + } + } + } +} diff --git a/Modules/Settings/Tabs/Misc.qml b/Modules/Settings/Tabs/Misc.qml deleted file mode 100644 index 0b04700..0000000 --- a/Modules/Settings/Tabs/Misc.qml +++ /dev/null @@ -1,45 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import qs.Services -import qs.Widgets - -ColumnLayout { - id: root - - spacing: 0 - - ScrollView { - id: scrollView - - Layout.fillWidth: true - Layout.fillHeight: true - padding: Style.marginMedium * scaling - clip: true - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - ScrollBar.vertical.policy: ScrollBar.AsNeeded - - ColumnLayout { - width: scrollView.availableWidth - spacing: 0 - - Item { - Layout.fillWidth: true - Layout.preferredHeight: 0 - } - - ColumnLayout { - spacing: Style.marginLarge * scaling - Layout.fillWidth: true - - NText { - text: "Miscellaneous Settings" - font.pointSize: Style.fontSizeXL * scaling - font.weight: Style.fontWeightBold - color: Colors.mOnSurface - Layout.bottomMargin: Style.marginSmall * scaling - } - } - } - } -} diff --git a/Services/Colors.qml b/Services/Colors.qml index 4d80001..df0cc39 100644 --- a/Services/Colors.qml +++ b/Services/Colors.qml @@ -6,10 +6,9 @@ import Quickshell.Io import qs.Services // -------------------------------- -// Noctalia Colors - Material Design 3 +// Noctalia Colors - Material Design 3 // We only use a very small subset of all available m3 colors to avoid complexity // All color names start with a 'm' to avoid QML assuming some of them are signals (ex: onPrimary) - Singleton { id: root @@ -58,7 +57,7 @@ Singleton { property color mError: "#eb6f92" property color mOnError: "#191724" - + property color mSurface: "#191724" property color mOnSurface: "#e0def4" property color mSurfaceVariant: "#26233a" @@ -66,7 +65,6 @@ Singleton { property color mOutline: "#44415a" property color mOutlineVariant: "#514e6c" property color mShadow: "#191724" - } // ---------------------------------------------------------------- diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml index 3d7f858..d3ebf16 100644 --- a/Widgets/NIconButton.qml +++ b/Widgets/NIconButton.qml @@ -42,8 +42,7 @@ Rectangle { font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 } - color: (root.hovering - || showFilled) ? Colors.mOnPrimary : showBorder ? Colors.mPrimary : Colors.mOnSurface + color: (root.hovering || showFilled) ? Colors.mOnPrimary : showBorder ? Colors.mPrimary : Colors.mOnSurface horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter opacity: root.enabled ? Style.opacityFull : Style.opacityMedium diff --git a/Widgets/NToggle.qml b/Widgets/NToggle.qml index eb61dd4..8974340 100644 --- a/Widgets/NToggle.qml +++ b/Widgets/NToggle.qml @@ -53,7 +53,7 @@ RowLayout { implicitWidth: (root.baseSize - 5) * scaling implicitHeight: (root.baseSize - 5) * scaling radius: height * 0.5 - color: value ? Colors.mOnPrimary: Colors.mPrimary + color: value ? Colors.mOnPrimary : Colors.mPrimary border.color: value ? Colors.mSurface : Colors.mSurface border.width: Math.max(1, Style.borderMedium * scaling) y: 2 * scaling From a79d4b916eed110fa6648707511163c597de6ae5 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Thu, 14 Aug 2025 22:50:13 -0400 Subject: [PATCH 287/394] ColorScheme: empty settings for now --- Modules/Settings/SettingsPanel.qml | 188 +++++++++----------------- Modules/Settings/Tabs/ColorScheme.qml | 71 ++++------ 2 files changed, 91 insertions(+), 168 deletions(-) diff --git a/Modules/Settings/SettingsPanel.qml b/Modules/Settings/SettingsPanel.qml index 40bbac2..d8645ba 100644 --- a/Modules/Settings/SettingsPanel.qml +++ b/Modules/Settings/SettingsPanel.qml @@ -10,11 +10,12 @@ import qs.Widgets NLoader { id: root - // Enumerate all the tabs, ordering is NOT relevant + // Tabs enumeration, order is NOT relevant enum Tab { About, Audio, Bar, + ColorScheme, Display, General, Network, @@ -33,61 +34,6 @@ NLoader { readonly property real scaling: Scaling.scale(screen) property int currentTabIndex: 0 - // List of all the tabs, ordering is relevant. - property var tabsModel: [{ - "id": SettingsPanel.Tab.General, - "label": "General", - "icon": "tune", - "source": "Tabs/General.qml" - }, { - "id": SettingsPanel.Tab.Bar, - "label": "Bar", - "icon": "web_asset", - "source": "Tabs/Bar.qml" - }, { - "id": SettingsPanel.Tab.Display, - "label": "Display", - "icon": "monitor", - "source": "Tabs/Display.qml" - }, { - "id": SettingsPanel.Tab.Audio, - "label": "Audio", - "icon": "volume_up", - "source": "Tabs/Audio.qml" - }, { - "id": SettingsPanel.Tab.Network, - "label": "Network", - "icon": "lan", - "source": "Tabs/Network.qml" - }, { - "id": SettingsPanel.Tab.TimeWeather, - "label": "Time & Weather", - "icon": "schedule", - "source": "Tabs/TimeWeather.qml" - }, { - "id": SettingsPanel.Tab.Wallpaper, - "label": "Wallpaper", - "icon": "image", - "source": "Tabs/Wallpaper.qml" - }, { - "id": SettingsPanel.Tab.WallpaperSelector, - "label": "Wallpaper Selector", - "icon": "wallpaper_slideshow", - "source": "Tabs/WallpaperSelector.qml" - }, { - "id": SettingsPanel.Tab.ScreenRecorder, - "label": "Screen Recorder", - "icon": "videocam", - "source": "Tabs/ScreenRecorder.qml" - }, { - "id": SettingsPanel.Tab.About, - "label": "About", - "icon": "info", - "source": "Tabs/About.qml" - }] - - WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand - // Override hide function to animate first function hide() { // Start hide animation @@ -98,69 +44,6 @@ NLoader { hideTimer.start() } - function getTab(tabId) { - switch (tabId) { - case SettingsPanel.Tab.About: - return tabAbount - case SettingsPanel.Tab.Audio: - return tabAudio - case SettingsPanel.Tab.Bar: - return tabBar - case SettingsPanel.Tab.General: - return tabGeneral - case SettingsPanel.Tab.Network: - return tabNetwork - case SettingsPanel.Tab.ScreenRecorder: - return tabScreenRecorder - case SettingsPanel.Tab.TimeWeather: - return tabTimeWeather - case SettingsPanel.Tab.Wallpaper: - return tabWallpaper - case SettingsPanel.Tab.WallpaperSelector: - return tabWallpaperSelector - default: - return tabGeneral - } - } - - // Wrap each tab in a Component so we can spawn them with flexibility - Component { - id: tabAbount - Tabs.About {} - } - Component { - id: tabAudio - Tabs.Audio {} - } - Component { - id: tabBar - Tabs.Bar {} - } - Component { - id: tabGeneral - Tabs.General {} - } - Component { - id: tabNetwork - Tabs.Network {} - } - Component { - id: tabScreenRecorder - Tabs.ScreenRecorder {} - } - Component { - id: tabTimeWeather - Tabs.TimeWeather {} - } - Component { - id: tabWallpaper - Tabs.Wallpaper {} - } - Component { - id: tabWallpaperSelector - Tabs.WallpaperSelector {} - } - // Connect to NPanel's dismissed signal to handle external close events Connections { target: panel @@ -185,6 +68,55 @@ NLoader { } } + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + + // Order is NOT relevant, and should match the Tabs.xxxx {} in the StackLayout + property var tabsModel: [{ + "id": SettingsPanel.Tab.General, + "label": "General", + "icon": "tune" + }, { + "id": SettingsPanel.Tab.Bar, + "label": "Bar", + "icon": "web_asset" + }, { + "id": SettingsPanel.Tab.Audio, + "label": "Audio", + "icon": "volume_up" + }, { + "id": SettingsPanel.Tab.Display, + "label": "Display", + "icon": "monitor" + }, { + "id": SettingsPanel.Tab.Network, + "label": "Network", + "icon": "lan" + }, { + "id": SettingsPanel.Tab.TimeWeather, + "label": "Time & Weather", + "icon": "schedule" + }, { + "id": SettingsPanel.Tab.ColorScheme, + "label": "Color Scheme", + "icon": "palette" + }, { + "id": SettingsPanel.Tab.Wallpaper, + "label": "Wallpaper", + "icon": "image" + }, { + "id": SettingsPanel.Tab.WallpaperSelector, + "label": "Wallpaper Selector", + "icon": "wallpaper_slideshow" + }, { + "id": SettingsPanel.Tab.ScreenRecorder, + "label": "Screen Recorder", + "icon": "videocam" + }, { + "id": SettingsPanel.Tab.About, + "label": "About", + "icon": "info" + }] + Component.onCompleted: { show() } @@ -278,6 +210,7 @@ NLoader { spacing: Style.marginTiny * 1.5 * scaling // Minimal spacing between tabs Repeater { + id: sections model: panel.tabsModel delegate: Rectangle { @@ -385,12 +318,17 @@ NLoader { Layout.fillHeight: true currentIndex: currentTabIndex - Repeater { - model: panel.tabsModel - delegate: Loader { - sourceComponent: getTab(modelData.id) - } - } + Tabs.General {} + Tabs.Bar {} + Tabs.Audio {} + Tabs.Display {} + Tabs.Network {} + Tabs.TimeWeather {} + Tabs.ColorScheme {} + Tabs.Wallpaper {} + Tabs.WallpaperSelector {} + Tabs.ScreenRecorder {} + Tabs.About {} } } } diff --git a/Modules/Settings/Tabs/ColorScheme.qml b/Modules/Settings/Tabs/ColorScheme.qml index cd49a77..7e95a97 100644 --- a/Modules/Settings/Tabs/ColorScheme.qml +++ b/Modules/Settings/Tabs/ColorScheme.qml @@ -9,6 +9,22 @@ ColumnLayout { spacing: 0 + // property var colorSchemes: [{ + // "label": "Generated from Wallpaper (Matugen required)" + // }, { + // "label": "Catppuccin" + // }, { + // "label": "Dracula" + // }, { + // "label": "Gruvbox" + // }, { + // "label": "Nord" + // "file": "nord.json" + // }, , { + // "label": "Rosé Pine", + // "file": "rosepine.json" + // }] + ScrollView { id: scrollView @@ -33,56 +49,25 @@ ColumnLayout { Layout.fillWidth: true NText { - text: "Components" + text: "TODO" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold color: Colors.mOnSurface } - NToggle { - label: "Show Active Window" - description: "Display the title of the currently focused window below the bar" - value: Settings.data.bar.showActiveWindow - onToggled: function (newValue) { - Settings.data.bar.showActiveWindow = newValue - } + ButtonGroup { + id: schemes } - NToggle { - label: "Show Active Window Icon" - description: "Display the icon of the currently focused window" - value: Settings.data.bar.showActiveWindowIcon - onToggled: function (newValue) { - Settings.data.bar.showActiveWindowIcon = newValue - } - } - - NToggle { - label: "Show System Info" - description: "Display system information (CPU, RAM, Temperature)" - value: Settings.data.bar.showSystemInfo - onToggled: function (newValue) { - Settings.data.bar.showSystemInfo = newValue - } - } - - NToggle { - label: "Show Taskbar" - description: "Display a taskbar showing currently open windows" - value: Settings.data.bar.showTaskbar - onToggled: function (newValue) { - Settings.data.bar.showTaskbar = newValue - } - } - - NToggle { - label: "Show Media" - description: "Display media controls and information" - value: Settings.data.bar.showMedia - onToggled: function (newValue) { - Settings.data.bar.showMedia = newValue - } - } + // Repeater { + // model: root.colorSchemes + // ButtonGroup.group: schemes + // NRadioButton { + // // checked: Audio.sink?.id === modelData.id + // //onClicked: Audio.setAudioSink(modelData) + // text: modelData.label + // } + // } } } } From cbe65db8ec78c7240886686cd34b025cd0cf6c5a Mon Sep 17 00:00:00 2001 From: quadbyte Date: Thu, 14 Aug 2025 23:16:00 -0400 Subject: [PATCH 288/394] Wallpapers service remove unused stuff --- Services/Wallpapers.qml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Services/Wallpapers.qml b/Services/Wallpapers.qml index 9363cd9..8cfa789 100644 --- a/Services/Wallpapers.qml +++ b/Services/Wallpapers.qml @@ -6,7 +6,7 @@ import Quickshell import Quickshell.Io Singleton { - id: manager //TBC + id: root Item { Component.onCompleted: { @@ -121,7 +121,6 @@ Singleton { onStatusChanged: { if (status === FolderListModel.Ready) { var files = [] - var filesSwww = [] for (var i = 0; i < count; i++) { var filepath = (Settings.data.wallpaper.directory !== undefined ? Settings.data.wallpaper.directory : "") + "/" + get( i, "fileName") From 1763fdcd9738cec30015d5b57440b24b92c24951 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Thu, 14 Aug 2025 23:16:11 -0400 Subject: [PATCH 289/394] preparing colorschemes json folder --- Assets/{Colors => ColorSchemes}/rosepine.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Assets/{Colors => ColorSchemes}/rosepine.json (100%) diff --git a/Assets/Colors/rosepine.json b/Assets/ColorSchemes/rosepine.json similarity index 100% rename from Assets/Colors/rosepine.json rename to Assets/ColorSchemes/rosepine.json From 3108b5b1a42d20ba6be646a776d043608ab04d39 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Thu, 14 Aug 2025 23:38:07 -0400 Subject: [PATCH 290/394] Wallpapers + Selector: Fixed the refresh button and removed duplicate logic that was already present in the wallpaper service --- Modules/Settings/Tabs/WallpaperSelector.qml | 16 ++++------------ Services/Settings.qml | 6 +----- Services/Wallpapers.qml | 21 ++++++++++----------- 3 files changed, 15 insertions(+), 28 deletions(-) diff --git a/Modules/Settings/Tabs/WallpaperSelector.qml b/Modules/Settings/Tabs/WallpaperSelector.qml index 46e1e40..5100b9a 100644 --- a/Modules/Settings/Tabs/WallpaperSelector.qml +++ b/Modules/Settings/Tabs/WallpaperSelector.qml @@ -106,22 +106,14 @@ Item { Item { Layout.fillWidth: true Layout.preferredHeight: { - return Math.ceil(folderModel.count / wallpaperGridView.columns) * wallpaperGridView.cellHeight - } - - FolderListModel { - id: folderModel - folder: "file://" + (Settings.data.wallpaper.directory !== undefined ? Settings.data.wallpaper.directory : "") - nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.pnm", "*.bmp"] - showDirs: false - sortField: FolderListModel.Name + return Math.ceil(Wallpapers.wallpaperList.length / wallpaperGridView.columns) * wallpaperGridView.cellHeight } GridView { id: wallpaperGridView anchors.fill: parent clip: true - model: folderModel + model: Wallpapers.wallpaperList boundsBehavior: Flickable.StopAtBounds flickableDirection: Flickable.AutoFlickDirection @@ -141,7 +133,7 @@ Item { delegate: Rectangle { id: wallpaperItem - property string wallpaperPath: Settings.data.wallpaper.directory + "/" + fileName + property string wallpaperPath: modelData property bool isSelected: wallpaperPath === Wallpapers.currentWallpaper width: wallpaperGridView.itemSize @@ -217,7 +209,7 @@ Item { radius: Style.radiusMedium * scaling border.color: Colors.mOutline border.width: Math.max(1, Style.borderThin * scaling) - visible: folderModel.count === 0 && !Wallpapers.scanning + visible: Wallpapers.wallpaperList.length === 0 && !Wallpapers.scanning ColumnLayout { anchors.centerIn: parent diff --git a/Services/Settings.qml b/Services/Settings.qml index a395808..2e219c6 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -37,11 +37,6 @@ Singleton { } FileView { - - // TBC ? needed for SWWW only ? - // Qt.callLater(function () { - // WallpaperManager.setCurrentWallpaper(settings.currentWallpaper, true); - // }) path: settingsFile watchChanges: true onFileChanged: reload() @@ -50,6 +45,7 @@ Singleton { reload() } onLoaded: function () { + console.log("[Settings] loaded") Qt.callLater(function () { if (adapter.wallpaper.current !== "") { console.log("[Settings] Set current wallpaper") diff --git a/Services/Wallpapers.qml b/Services/Wallpapers.qml index 8cfa789..e5197b8 100644 --- a/Services/Wallpapers.qml +++ b/Services/Wallpapers.qml @@ -8,17 +8,12 @@ import Quickshell.Io Singleton { id: root - Item { - Component.onCompleted: { - loadWallpapers() - // Only set initial wallpaper if it's not empty - if (currentWallpaper !== "") { - console.log("[WP] initializing with:", currentWallpaper) - setCurrentWallpaper(currentWallpaper, true) - } - // Don't start random wallpaper during initialization - // toggleRandomWallpaper() - } + Component.onCompleted: { + console.log("[WP] Service initialized") + loadWallpapers() + + // Wallpaper is set when the settings are loaded. + // Don't start random wallpaper during initialization } property var wallpaperList: [] @@ -28,8 +23,11 @@ Singleton { property var randomChoices: ["simple", "fade", "left", "right", "top", "bottom", "wipe", "wave", "grow", "center", "any", "outer"] function loadWallpapers() { + console.log("[WP] Load Wallpapers") scanning = true wallpaperList = [] + // Unsetting, then setting the folder will re-trigger the parsing! + folderModel.folder = ""; folderModel.folder = "file://" + (Settings.data.wallpaper.directory !== undefined ? Settings.data.wallpaper.directory : "") } @@ -128,6 +126,7 @@ Singleton { } wallpaperList = files scanning = false + console.log("[WP] List refreshed, count:", wallpaperList.length) } } } From eac335ab955f2c9d5756cab54b366d44de088737 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Thu, 14 Aug 2025 23:50:41 -0400 Subject: [PATCH 291/394] Removed double Connections in settings. --- Services/Settings.qml | 15 +-------------- Services/Wallpapers.qml | 10 +++++----- Services/Workspaces.qml | 14 +++++++------- 3 files changed, 13 insertions(+), 26 deletions(-) diff --git a/Services/Settings.qml b/Services/Settings.qml index 2e219c6..a6a5f4c 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -45,7 +45,7 @@ Singleton { reload() } onLoaded: function () { - console.log("[Settings] loaded") + console.log("[Settings] Loaded") Qt.callLater(function () { if (adapter.wallpaper.current !== "") { console.log("[Settings] Set current wallpaper") @@ -181,17 +181,4 @@ Singleton { } } } - - Connections { - target: adapter.wallpaper - function onIsRandomChanged() { - Wallpapers.toggleRandomWallpaper() - } - function onRandomIntervalChanged() { - Wallpapers.restartRandomWallpaperTimer() - } - function onDirectoryChanged() { - Wallpapers.loadWallpapers() - } - } } diff --git a/Services/Wallpapers.qml b/Services/Wallpapers.qml index e5197b8..3e656cf 100644 --- a/Services/Wallpapers.qml +++ b/Services/Wallpapers.qml @@ -9,7 +9,7 @@ Singleton { id: root Component.onCompleted: { - console.log("[WP] Service initialized") + console.log("[Wallpapers] Service initialized") loadWallpapers() // Wallpaper is set when the settings are loaded. @@ -23,7 +23,7 @@ Singleton { property var randomChoices: ["simple", "fade", "left", "right", "top", "bottom", "wipe", "wave", "grow", "center", "any", "outer"] function loadWallpapers() { - console.log("[WP] Load Wallpapers") + console.log("[Wallpapers] Load Wallpapers") scanning = true wallpaperList = [] // Unsetting, then setting the folder will re-trigger the parsing! @@ -32,7 +32,7 @@ Singleton { } function changeWallpaper(path) { - console.log("[WP] changing to:", path) + console.log("[Wallpapers] Changing to:", path) setCurrentWallpaper(path, false) } @@ -53,7 +53,7 @@ Singleton { } else { // Fallback: update the settings directly for non-SWWW mode - //console.log("[WP] Not using Swww, setting wallpaper directly") + //console.log("[Wallpapers] Not using Swww, setting wallpaper directly") } if (randomWallpaperTimer.running) { @@ -126,7 +126,7 @@ Singleton { } wallpaperList = files scanning = false - console.log("[WP] List refreshed, count:", wallpaperList.length) + console.log("[Wallpapers] List refreshed, count:", wallpaperList.length) } } } diff --git a/Services/Workspaces.qml b/Services/Workspaces.qml index 7bdf040..f513b37 100644 --- a/Services/Workspaces.qml +++ b/Services/Workspaces.qml @@ -17,7 +17,7 @@ Singleton { property var hlWorkspaces: Hyprland.workspaces.values // Detect which compositor we're using Component.onCompleted: { - console.log("[WS] Initializing workspaces service") + console.log("[Workspaces] Initializing workspaces service") detectCompositor() } @@ -25,27 +25,27 @@ Singleton { try { try { if (Hyprland.eventSocketPath) { - console.log("[WS] Detected Hyprland compositor") + console.log("[Workspaces] Detected Hyprland compositor") isHyprland = true isNiri = false initHyprland() return } } catch (e) { - console.log("[WS] Hyprland not available:", e) + console.log("[Workspaces] Hyprland not available:", e) } if (typeof Niri !== "undefined") { - console.log("[WS] Detected Niri service") + console.log("[Workspaces] Detected Niri service") isHyprland = false isNiri = true initNiri() return } - console.log("[WS] Could not detect any supported compositor") + console.log("[Workspaces] Could not detect any supported compositor") } catch (e) { - console.error("[WS] Error detecting compositor:", e) + console.error("[Workspaces] Error detecting compositor:", e) } } @@ -102,7 +102,7 @@ Singleton { } workspacesChanged() } catch (e) { - console.error("[WS] Error updating Hyprland workspaces:", e) + console.error("[Workspaces] Error updating Hyprland workspaces:", e) } } From 39cc72f0670a66d3969aab1af9d5e1f8c96a1e6b Mon Sep 17 00:00:00 2001 From: quadbyte Date: Thu, 14 Aug 2025 23:57:18 -0400 Subject: [PATCH 292/394] Renamed all tabs qml, so its easier to distinguish them from services --- Modules/Settings/SettingsPanel.qml | 22 ++++----- .../Settings/Tabs/{About.qml => AboutTab.qml} | 0 .../Settings/Tabs/{Audio.qml => AudioTab.qml} | 0 Modules/Settings/Tabs/{Bar.qml => BarTab.qml} | 0 .../{ColorScheme.qml => ColorSchemeTab.qml} | 31 +++++++----- .../Tabs/{Display.qml => DisplayTab.qml} | 0 .../Tabs/{General.qml => GeneralTab.qml} | 0 .../Tabs/{Network.qml => NetworkTab.qml} | 0 ...reenRecorder.qml => ScreenRecorderTab.qml} | 0 .../{TimeWeather.qml => TimeWeatherTab.qml} | 0 ...rSelector.qml => WallpaperSelectorTab.qml} | 0 .../Tabs/{Wallpaper.qml => WallpaperTab.qml} | 0 Services/ColorSchemes.qml | 49 +++++++++++++++++++ 13 files changed, 80 insertions(+), 22 deletions(-) rename Modules/Settings/Tabs/{About.qml => AboutTab.qml} (100%) rename Modules/Settings/Tabs/{Audio.qml => AudioTab.qml} (100%) rename Modules/Settings/Tabs/{Bar.qml => BarTab.qml} (100%) rename Modules/Settings/Tabs/{ColorScheme.qml => ColorSchemeTab.qml} (69%) rename Modules/Settings/Tabs/{Display.qml => DisplayTab.qml} (100%) rename Modules/Settings/Tabs/{General.qml => GeneralTab.qml} (100%) rename Modules/Settings/Tabs/{Network.qml => NetworkTab.qml} (100%) rename Modules/Settings/Tabs/{ScreenRecorder.qml => ScreenRecorderTab.qml} (100%) rename Modules/Settings/Tabs/{TimeWeather.qml => TimeWeatherTab.qml} (100%) rename Modules/Settings/Tabs/{WallpaperSelector.qml => WallpaperSelectorTab.qml} (100%) rename Modules/Settings/Tabs/{Wallpaper.qml => WallpaperTab.qml} (100%) create mode 100644 Services/ColorSchemes.qml diff --git a/Modules/Settings/SettingsPanel.qml b/Modules/Settings/SettingsPanel.qml index d8645ba..61387d6 100644 --- a/Modules/Settings/SettingsPanel.qml +++ b/Modules/Settings/SettingsPanel.qml @@ -318,17 +318,17 @@ NLoader { Layout.fillHeight: true currentIndex: currentTabIndex - Tabs.General {} - Tabs.Bar {} - Tabs.Audio {} - Tabs.Display {} - Tabs.Network {} - Tabs.TimeWeather {} - Tabs.ColorScheme {} - Tabs.Wallpaper {} - Tabs.WallpaperSelector {} - Tabs.ScreenRecorder {} - Tabs.About {} + Tabs.GeneralTab {} + Tabs.BarTab {} + Tabs.AudioTab {} + Tabs.DisplayTab {} + Tabs.NetworkTab {} + Tabs.TimeWeatherTab {} + Tabs.ColorSchemeTab {} + Tabs.WallpaperTab {} + Tabs.WallpaperSelectorTab {} + Tabs.ScreenRecorderTab {} + Tabs.AboutTab {} } } } diff --git a/Modules/Settings/Tabs/About.qml b/Modules/Settings/Tabs/AboutTab.qml similarity index 100% rename from Modules/Settings/Tabs/About.qml rename to Modules/Settings/Tabs/AboutTab.qml diff --git a/Modules/Settings/Tabs/Audio.qml b/Modules/Settings/Tabs/AudioTab.qml similarity index 100% rename from Modules/Settings/Tabs/Audio.qml rename to Modules/Settings/Tabs/AudioTab.qml diff --git a/Modules/Settings/Tabs/Bar.qml b/Modules/Settings/Tabs/BarTab.qml similarity index 100% rename from Modules/Settings/Tabs/Bar.qml rename to Modules/Settings/Tabs/BarTab.qml diff --git a/Modules/Settings/Tabs/ColorScheme.qml b/Modules/Settings/Tabs/ColorSchemeTab.qml similarity index 69% rename from Modules/Settings/Tabs/ColorScheme.qml rename to Modules/Settings/Tabs/ColorSchemeTab.qml index 7e95a97..3783d5f 100644 --- a/Modules/Settings/Tabs/ColorScheme.qml +++ b/Modules/Settings/Tabs/ColorSchemeTab.qml @@ -9,6 +9,12 @@ ColumnLayout { spacing: 0 + // Component.onCompleted: { + // console.log("[ColorSchemes] Service initialized") + // ColorScheme.loadColorSchemes() + // } + + // property var colorSchemes: [{ // "label": "Generated from Wallpaper (Matugen required)" // }, { @@ -24,7 +30,6 @@ ColumnLayout { // "label": "Rosé Pine", // "file": "rosepine.json" // }] - ScrollView { id: scrollView @@ -56,18 +61,22 @@ ColumnLayout { } ButtonGroup { - id: schemes + id: schemesGroup } - // Repeater { - // model: root.colorSchemes - // ButtonGroup.group: schemes - // NRadioButton { - // // checked: Audio.sink?.id === modelData.id - // //onClicked: Audio.setAudioSink(modelData) - // text: modelData.label - // } - // } + Repeater { + model: ColorSchemes.schemes + delegate: NRadioButton { + ButtonGroup.group: schemesGroup + // checked: Audio.sink?.id === modelData.id + //onClicked: Audio.setAudioSink(modelData) + text: { + console.log(modelData.fileName) + return modelData.fileName + } + } + + } } } } diff --git a/Modules/Settings/Tabs/Display.qml b/Modules/Settings/Tabs/DisplayTab.qml similarity index 100% rename from Modules/Settings/Tabs/Display.qml rename to Modules/Settings/Tabs/DisplayTab.qml diff --git a/Modules/Settings/Tabs/General.qml b/Modules/Settings/Tabs/GeneralTab.qml similarity index 100% rename from Modules/Settings/Tabs/General.qml rename to Modules/Settings/Tabs/GeneralTab.qml diff --git a/Modules/Settings/Tabs/Network.qml b/Modules/Settings/Tabs/NetworkTab.qml similarity index 100% rename from Modules/Settings/Tabs/Network.qml rename to Modules/Settings/Tabs/NetworkTab.qml diff --git a/Modules/Settings/Tabs/ScreenRecorder.qml b/Modules/Settings/Tabs/ScreenRecorderTab.qml similarity index 100% rename from Modules/Settings/Tabs/ScreenRecorder.qml rename to Modules/Settings/Tabs/ScreenRecorderTab.qml diff --git a/Modules/Settings/Tabs/TimeWeather.qml b/Modules/Settings/Tabs/TimeWeatherTab.qml similarity index 100% rename from Modules/Settings/Tabs/TimeWeather.qml rename to Modules/Settings/Tabs/TimeWeatherTab.qml diff --git a/Modules/Settings/Tabs/WallpaperSelector.qml b/Modules/Settings/Tabs/WallpaperSelectorTab.qml similarity index 100% rename from Modules/Settings/Tabs/WallpaperSelector.qml rename to Modules/Settings/Tabs/WallpaperSelectorTab.qml diff --git a/Modules/Settings/Tabs/Wallpaper.qml b/Modules/Settings/Tabs/WallpaperTab.qml similarity index 100% rename from Modules/Settings/Tabs/Wallpaper.qml rename to Modules/Settings/Tabs/WallpaperTab.qml diff --git a/Services/ColorSchemes.qml b/Services/ColorSchemes.qml new file mode 100644 index 0000000..e879a65 --- /dev/null +++ b/Services/ColorSchemes.qml @@ -0,0 +1,49 @@ +pragma Singleton + +import QtQuick +import Qt.labs.folderlistmodel +import Quickshell +import Quickshell.Io + +Singleton { + id: root + + // Component.onCompleted: { + // console.log("[ColorSchemes] Service initialized") + // loadColorSchemes() + // } + + // property var schemes: [] + // //property string currentScheme: Settings.data.wallpaper.current + // property bool scanning: false + + // function loadColorSchemes() { + // scanning = true + // schemes = [] + // // Unsetting, then setting the folder will re-trigger the parsing! + // folderModel.folder = "" + // folderModel.folder = "file://" + Quickshell.shellDir + "/Assets/Matugen/ColorSchemes" + // } + + // FolderListModel { + // id: folderModel + // nameFilters: ["*.json"] + // showDirs: false + // sortField: FolderListModel.Name + // onStatusChanged: { + // console.log("sasfjsaflkhfkjhasf") + // if (status === FolderListModel.Ready) { + // var files = [] + // for (var i = 0; i < count; i++) { + // console.log(get(i, "fileName")) + // // var filepath = (Settings.data.wallpaper.directory !== undefined ? Settings.data.wallpaper.directory : "") + "/" + get( + // // i, "fileName") + // // files.push(filepath) + // } + // schemes = files + // scanning = false + // console.log(schemes) + // } + // } + // } +} From b79744c13554d64a744b842316a85131363af8f0 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 00:09:03 -0400 Subject: [PATCH 293/394] ColorScheme: selector wip --- Modules/Settings/Tabs/ColorSchemeTab.qml | 32 ++--------- Services/ColorSchemes.qml | 67 ++++++++++++------------ Services/Wallpapers.qml | 6 +-- 3 files changed, 40 insertions(+), 65 deletions(-) diff --git a/Modules/Settings/Tabs/ColorSchemeTab.qml b/Modules/Settings/Tabs/ColorSchemeTab.qml index 3783d5f..72ba676 100644 --- a/Modules/Settings/Tabs/ColorSchemeTab.qml +++ b/Modules/Settings/Tabs/ColorSchemeTab.qml @@ -9,27 +9,6 @@ ColumnLayout { spacing: 0 - // Component.onCompleted: { - // console.log("[ColorSchemes] Service initialized") - // ColorScheme.loadColorSchemes() - // } - - - // property var colorSchemes: [{ - // "label": "Generated from Wallpaper (Matugen required)" - // }, { - // "label": "Catppuccin" - // }, { - // "label": "Dracula" - // }, { - // "label": "Gruvbox" - // }, { - // "label": "Nord" - // "file": "nord.json" - // }, , { - // "label": "Rosé Pine", - // "file": "rosepine.json" - // }] ScrollView { id: scrollView @@ -66,16 +45,13 @@ ColumnLayout { Repeater { model: ColorSchemes.schemes - delegate: NRadioButton { + NRadioButton { + property string schemePath: modelData ButtonGroup.group: schemesGroup - // checked: Audio.sink?.id === modelData.id + //checked: Audio.sink?.id === modelData.id //onClicked: Audio.setAudioSink(modelData) - text: { - console.log(modelData.fileName) - return modelData.fileName - } + text: schemePath } - } } } diff --git a/Services/ColorSchemes.qml b/Services/ColorSchemes.qml index e879a65..6144322 100644 --- a/Services/ColorSchemes.qml +++ b/Services/ColorSchemes.qml @@ -8,42 +8,41 @@ import Quickshell.Io Singleton { id: root - // Component.onCompleted: { - // console.log("[ColorSchemes] Service initialized") - // loadColorSchemes() - // } + Component.onCompleted: { + console.log("[ColorSchemes] Service initialized") + loadColorSchemes() + } - // property var schemes: [] + property var schemes: [] + property string baseDirectory: "file://" + Quickshell.shellDir + "/Assets/ColorSchemes" // //property string currentScheme: Settings.data.wallpaper.current - // property bool scanning: false + property bool scanning: false - // function loadColorSchemes() { - // scanning = true - // schemes = [] - // // Unsetting, then setting the folder will re-trigger the parsing! - // folderModel.folder = "" - // folderModel.folder = "file://" + Quickshell.shellDir + "/Assets/Matugen/ColorSchemes" - // } + function loadColorSchemes() { + console.log("[ColorSchemes] Load ColorSchemes") + scanning = true + schemes = [] + // Unsetting, then setting the folder will re-trigger the parsing! + folderModel.folder = "" + folderModel.folder = baseDirectory + } - // FolderListModel { - // id: folderModel - // nameFilters: ["*.json"] - // showDirs: false - // sortField: FolderListModel.Name - // onStatusChanged: { - // console.log("sasfjsaflkhfkjhasf") - // if (status === FolderListModel.Ready) { - // var files = [] - // for (var i = 0; i < count; i++) { - // console.log(get(i, "fileName")) - // // var filepath = (Settings.data.wallpaper.directory !== undefined ? Settings.data.wallpaper.directory : "") + "/" + get( - // // i, "fileName") - // // files.push(filepath) - // } - // schemes = files - // scanning = false - // console.log(schemes) - // } - // } - // } + FolderListModel { + id: folderModel + nameFilters: ["*.json"] + showDirs: false + sortField: FolderListModel.Name + onStatusChanged: { + if (status === FolderListModel.Ready) { + var files = [] + for (var i = 0; i < count; i++) { + var filepath = baseDirectory + "/" + get(i, "fileName") + files.push(filepath) + } + schemes = files + scanning = false + console.log(schemes) + } + } + } } diff --git a/Services/Wallpapers.qml b/Services/Wallpapers.qml index 3e656cf..71fa8a5 100644 --- a/Services/Wallpapers.qml +++ b/Services/Wallpapers.qml @@ -21,13 +21,13 @@ Singleton { property bool scanning: false property string transitionType: Settings.data.wallpaper.swww.transitionType property var randomChoices: ["simple", "fade", "left", "right", "top", "bottom", "wipe", "wave", "grow", "center", "any", "outer"] - + function loadWallpapers() { console.log("[Wallpapers] Load Wallpapers") scanning = true wallpaperList = [] // Unsetting, then setting the folder will re-trigger the parsing! - folderModel.folder = ""; + folderModel.folder = "" folderModel.folder = "file://" + (Settings.data.wallpaper.directory !== undefined ? Settings.data.wallpaper.directory : "") } @@ -126,7 +126,7 @@ Singleton { } wallpaperList = files scanning = false - console.log("[Wallpapers] List refreshed, count:", wallpaperList.length) + console.log("[Wallpapers] List refreshed, count:", wallpaperList.length) } } } From dd471581cd88820481d1f3605a12193ebf075eb8 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 00:11:56 -0400 Subject: [PATCH 294/394] FolderListModel: improve robustness by reusing the same path --- Services/ColorSchemes.qml | 6 ++---- Services/Wallpapers.qml | 3 +-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/Services/ColorSchemes.qml b/Services/ColorSchemes.qml index 6144322..d8c3638 100644 --- a/Services/ColorSchemes.qml +++ b/Services/ColorSchemes.qml @@ -14,8 +14,6 @@ Singleton { } property var schemes: [] - property string baseDirectory: "file://" + Quickshell.shellDir + "/Assets/ColorSchemes" - // //property string currentScheme: Settings.data.wallpaper.current property bool scanning: false function loadColorSchemes() { @@ -24,7 +22,7 @@ Singleton { schemes = [] // Unsetting, then setting the folder will re-trigger the parsing! folderModel.folder = "" - folderModel.folder = baseDirectory + folderModel.folder = "file://" + Quickshell.shellDir + "/Assets/ColorSchemes" } FolderListModel { @@ -36,7 +34,7 @@ Singleton { if (status === FolderListModel.Ready) { var files = [] for (var i = 0; i < count; i++) { - var filepath = baseDirectory + "/" + get(i, "fileName") + var filepath = folderModel.folder + "/" + get(i, "fileName") files.push(filepath) } schemes = files diff --git a/Services/Wallpapers.qml b/Services/Wallpapers.qml index 71fa8a5..e3a1895 100644 --- a/Services/Wallpapers.qml +++ b/Services/Wallpapers.qml @@ -120,8 +120,7 @@ Singleton { if (status === FolderListModel.Ready) { var files = [] for (var i = 0; i < count; i++) { - var filepath = (Settings.data.wallpaper.directory !== undefined ? Settings.data.wallpaper.directory : "") + "/" + get( - i, "fileName") + var filepath = folderModel.folder + "/" + get(i, "fileName") files.push(filepath) } wallpaperList = files From 867444a29ce57a88cf8db54ef7ba3f2d371e6c44 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 06:49:24 -0400 Subject: [PATCH 295/394] Fix Matugen, NPanel overlay and probably SWWW --- Services/Colors.qml | 10 ++++++++-- Services/Settings.qml | 2 +- Services/Wallpapers.qml | 15 ++++++++++++--- Widgets/NPanel.qml | 2 +- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/Services/Colors.qml b/Services/Colors.qml index df0cc39..d77af2d 100644 --- a/Services/Colors.qml +++ b/Services/Colors.qml @@ -97,8 +97,14 @@ Singleton { id: customColorsFile path: Settings.configDir + "colors.json" watchChanges: true - onFileChanged: reload() - onAdapterUpdated: writeAdapter() + onFileChanged: { + console.log("[Colors] reloading colors file from disk") + reload() + } + onAdapterUpdated: { + console.log("[Colors] writing colors to disk, primary color:", mPrimary) + writeAdapter() + } onLoadFailed: function (error) { if (error.toString().includes("No such file") || error === 2) { // File doesn't exist, create it with default values diff --git a/Services/Settings.qml b/Services/Settings.qml index a6a5f4c..5e2d4f4 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -48,7 +48,7 @@ Singleton { console.log("[Settings] Loaded") Qt.callLater(function () { if (adapter.wallpaper.current !== "") { - console.log("[Settings] Set current wallpaper") + console.log("[Settings] Set current wallpaper", adapter.wallpaper.current) Wallpapers.setCurrentWallpaper(adapter.wallpaper.current, true) } }) diff --git a/Services/Wallpapers.qml b/Services/Wallpapers.qml index e3a1895..82e9e38 100644 --- a/Services/Wallpapers.qml +++ b/Services/Wallpapers.qml @@ -19,6 +19,8 @@ Singleton { property var wallpaperList: [] property string currentWallpaper: Settings.data.wallpaper.current property bool scanning: false + + // SWWW property string transitionType: Settings.data.wallpaper.swww.transitionType property var randomChoices: ["simple", "fade", "left", "right", "top", "bottom", "wipe", "wave", "grow", "center", "any", "outer"] @@ -120,7 +122,8 @@ Singleton { if (status === FolderListModel.Ready) { var files = [] for (var i = 0; i < count; i++) { - var filepath = folderModel.folder + "/" + get(i, "fileName") + var directory = (Settings.data.wallpaper.directory !== undefined ? Settings.data.wallpaper.directory : "") + var filepath = directory + "/" + get(i, "fileName") files.push(filepath) } wallpaperList = files @@ -157,8 +160,14 @@ Singleton { running: false stdout: StdioCollector { onStreamFinished: { - - //console.log(this.text) + console.log("[Wallpapers] generated colors from image") + } + } + stderr: StdioCollector { + onStreamFinished: { + if (this.text !== "") { + console.error(this.text) + } } } } diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 58353b1..8c1c67e 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -9,7 +9,7 @@ PanelWindow { readonly property real scaling: Scaling.scale(screen) property bool showOverlay: Settings.data.general.dimDesktop property int topMargin: Style.barHeight * scaling - property color overlayColor: showOverlay ? Colors.overlay : "transparent" + property color overlayColor: showOverlay ? Colors.applyOpacity(Colors.mShadow, "AA") : "transparent" signal dismissed From a1cd673fb54c12a53ebd9cc735758faeb4d8eb68 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 06:58:04 -0400 Subject: [PATCH 296/394] NPanel: slower overlay fadein --- Widgets/NPanel.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 8c1c67e..a3d76e3 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -51,7 +51,7 @@ PanelWindow { Behavior on color { ColorAnimation { - duration: Style.animationNormal + duration: Style.animationSlow easing.type: Easing.InOutCubic } } From 2d31e04eaf5eed6f57ef1cd4c6c1a080652e9cc3 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Fri, 15 Aug 2025 13:35:44 +0200 Subject: [PATCH 297/394] Possible brightness implementation --- Modules/Bar/Bar.qml | 4 + Modules/Bar/Brightness.qml | 63 +++++ Modules/Demo/DemoPanel.qml | 54 +++++ Services/BrightnessService.qml | 428 +++++++++++++++++++++++++++++++++ 4 files changed, 549 insertions(+) create mode 100644 Modules/Bar/Brightness.qml create mode 100644 Services/BrightnessService.qml diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 3cadc23..a7db3ab 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -119,6 +119,10 @@ Variants { anchors.verticalCenter: parent.verticalCenter } + Brightness { + anchors.verticalCenter: parent.verticalCenter + } + Clock { anchors.verticalCenter: parent.verticalCenter } diff --git a/Modules/Bar/Brightness.qml b/Modules/Bar/Brightness.qml new file mode 100644 index 0000000..0d5d690 --- /dev/null +++ b/Modules/Bar/Brightness.qml @@ -0,0 +1,63 @@ +import QtQuick +import Quickshell +import qs.Modules.Settings +import qs.Services +import qs.Widgets + +Item { + id: root + + width: pill.width + height: pill.height + + // Used to avoid opening the pill on Quickshell startup + property bool firstBrightnessReceived: false + + function getIcon() { + if (!BrightnessService.available) { + return "brightness_auto" + } + var brightness = BrightnessService.brightness + return brightness <= 0 ? "brightness_1" : + brightness < 33 ? "brightness_low" : + brightness < 66 ? "brightness_medium" : "brightness_high" + } + + // Connection used to open the pill when brightness changes + Connections { + target: Brightness + function onBrightnessUpdated() { + // console.log("[Bar:Brightness] onBrightnessUpdated") + if (!firstBrightnessReceived) { + // Ignore the first brightness change + firstBrightnessReceived = true + } else { + pill.show() + } + } + } + + NPill { + id: pill + icon: getIcon() + iconCircleColor: Colors.mPrimary + collapsedIconColor: Colors.mOnSurface + autoHide: true + text: Math.round(BrightnessService.brightness) + "%" + tooltipText: "Brightness: " + Math.round(BrightnessService.brightness) + "%\nMethod: " + BrightnessService.currentMethod + "\nLeft click for advanced settings.\nScroll up/down to change BrightnessService." + + onWheel: function (angle) { + if (!BrightnessService.available) return + + if (angle > 0) { + BrightnessService.increaseBrightness(1) + } else if (angle < 0) { + BrightnessService.decreaseBrightness(1) + } + } + onClicked: { + settingsPanel.requestedTab = SettingsPanel.Tab.Display + settingsPanel.isLoaded = true + } + } +} \ No newline at end of file diff --git a/Modules/Demo/DemoPanel.qml b/Modules/Demo/DemoPanel.qml index 9d3eaaf..91e797b 100644 --- a/Modules/Demo/DemoPanel.qml +++ b/Modules/Demo/DemoPanel.qml @@ -273,6 +273,60 @@ NLoader { Layout.fillWidth: true } } + + // Brightness Control + ColumnLayout { + spacing: Style.marginMedium * scaling + NText { + text: "Brightness Control" + color: Colors.mSecondary + font.weight: Style.fontWeightBold + } + + NText { + text: `Brightness: ${Math.round(Brightness.brightness)}%` + Layout.alignment: Qt.AlignVCenter + } + + RowLayout { + spacing: Style.marginSmall * scaling + NIconButton { + icon: "brightness_low" + fontPointSize: Style.fontSizeLarge * scaling + onClicked: { + Brightness.decreaseBrightness(1) + } + } + NSlider { + from: 0 + to: 100 + stepSize: 1 + value: Brightness.brightness + implicitWidth: bgRect.width * 0.5 + onMoved: { + Brightness.setBrightnessDebounced(value) + } + } + NIconButton { + icon: "brightness_high" + fontPointSize: Style.fontSizeLarge * scaling + onClicked: { + Brightness.increaseBrightness(1) + } + } + } + + NText { + text: `Method: ${Brightness.currentMethod} | Available: ${Brightness.available}` + color: Colors.mOnSurfaceVariant + font.pointSize: Style.fontSizeSmall * scaling + Layout.alignment: Qt.AlignHCenter + } + + NDivider { + Layout.fillWidth: true + } + } } } } diff --git a/Services/BrightnessService.qml b/Services/BrightnessService.qml new file mode 100644 index 0000000..31b0866 --- /dev/null +++ b/Services/BrightnessService.qml @@ -0,0 +1,428 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io + +Singleton { + id: root + + // Public properties + property real brightness: 0 + property real maxBrightness: 100 + property bool available: false + property string currentMethod: "" + property var detectedDisplays: [] + + // Private properties + property var _brightnessMethods: [] + property var _currentDisplay: null + property bool _initialized: false + property real _targetBrightness: 0 + property bool _isSettingBrightness: false + + // Signal when brightness changes + signal brightnessUpdated(real newBrightness) + signal methodChanged(string newMethod) + + // Initialize the service + Component.onCompleted: { + initializeBrightness() + } + + function initializeBrightness() { + if (_initialized) return + + console.log("[Brightness] Initializing brightness service...") + + // Start method detection + detectMethods() + + _initialized = true + } + + function detectMethods() { + _brightnessMethods = [] + + // Check for brightnessctl + brightnessctlProcess.running = true + + // Check for ddcutil + ddcutilProcess.running = true + + // Internal backlight is always available if we can access /sys/class/backlight + backlightCheck.running = true + + console.log("[Brightness] Starting method detection...") + } + + function checkMethodsComplete() { + // Check if all method detection processes have finished + if (!brightnessctlProcess.running && !ddcutilProcess.running && !backlightCheck.running) { + console.log("[Brightness] Available methods:", _brightnessMethods) + + // Now detect displays + detectDisplays() + } + } + + // Process objects for method detection + Process { + id: brightnessctlProcess + command: ["which", "brightnessctl"] + running: false + onExited: function(exitCode, exitStatus) { + if (exitCode === 0) { + _brightnessMethods.push("brightnessctl") + console.log("[Brightness] brightnessctl available") + } + checkMethodsComplete() + } + } + + Process { + id: ddcutilProcess + command: ["which", "ddcutil"] + running: false + onExited: function(exitCode, exitStatus) { + if (exitCode === 0) { + _brightnessMethods.push("ddcutil") + console.log("[Brightness] ddcutil available") + } + checkMethodsComplete() + } + } + + Process { + id: backlightCheck + command: ["test", "-d", "/sys/class/backlight"] + running: false + onExited: function(exitCode, exitStatus) { + if (exitCode === 0) { + _brightnessMethods.push("internal") + console.log("[Brightness] Internal backlight available") + } + checkMethodsComplete() + } + } + + function detectDisplays() { + detectedDisplays = [] + + // Get internal displays + backlightProcess.running = true + + // Get external displays via ddcutil + if (_brightnessMethods.indexOf("ddcutil") !== -1) { + ddcutilDetectProcess.running = true + } else { + // If no ddcutil, just check internal displays + checkDisplaysComplete() + } + + console.log("[Brightness] Starting display detection...") + } + + function checkDisplaysComplete() { + // Check if all display detection processes have finished + var internalFinished = !backlightProcess.running + var externalFinished = _brightnessMethods.indexOf("ddcutil") === -1 || !ddcutilDetectProcess.running + + if (internalFinished && externalFinished) { + console.log("[Brightness] Detected displays:", detectedDisplays) + + // Set current display to first available + if (detectedDisplays.length > 0) { + _currentDisplay = detectedDisplays[0] + currentMethod = _currentDisplay.method + available = true + console.log("[Brightness] Using display:", _currentDisplay.name, "method:", currentMethod) + + // Start initial brightness update + updateBrightness() + } else { + console.warn("[Brightness] No displays detected") + } + } + } + + // Process objects for display detection + Process { + id: backlightProcess + command: ["ls", "/sys/class/backlight"] + running: false + stdout: SplitParser { + onRead: function(line) { + var trimmedLine = line.replace(/^\s+|\s+$/g, "") + if (trimmedLine) { + detectedDisplays.push({ + name: trimmedLine, + type: "internal", + method: "internal" + }) + } + } + } + onExited: function(exitCode, exitStatus) { + checkDisplaysComplete() + } + } + + Process { + id: ddcutilDetectProcess + command: ["ddcutil", "detect"] + running: false + stdout: SplitParser { + onRead: function(line) { + console.log("[Brightness] ddcutil detect line:", line) + if (line.indexOf("Display") !== -1) { + // Simple parsing for Display number + var parts = line.split("Display") + if (parts.length > 1) { + var numberPart = parts[1].replace(/^\s+|\s+$/g, "") + var number = numberPart.split(" ")[0] + if (number && !isNaN(number)) { + detectedDisplays.push({ + name: "Display " + number, + type: "external", + method: "ddcutil", + index: number + }) + console.log("[Brightness] Added external display:", "Display " + number) + } + } + } + // Also look for connector information + if (line.indexOf("DRM connector:") !== -1) { + console.log("[Brightness] Found DRM connector:", line) + } + } + } + onExited: function(exitCode, exitStatus) { + checkDisplaysComplete() + } + } + + function updateBrightness() { + if (!_currentDisplay) return + + // Prevent multiple simultaneous brightness checks + if (brightnessGetProcess.running) { + console.log("[Brightness] Brightness check already in progress, skipping...") + return + } + + // Don't update if we're currently setting brightness + if (_isSettingBrightness) { + console.log("[Brightness] Skipping update while setting brightness...") + return + } + + console.log("[Brightness] Updating brightness for display:", _currentDisplay.name) + + // Try the brightness script first + if (_currentDisplay.method === "ddcutil" && _currentDisplay.index) { + // For ddcutil, try using the display index directly + brightnessGetProcess.command = ["ddcutil", "--display", _currentDisplay.index, "getvcp", "10"] + } else { + // Use the brightness script + brightnessGetProcess.command = ["sh", "-c", Quickshell.shellDir + "/Bin/brigthness.sh", "get", _currentDisplay.name] + } + brightnessGetProcess.running = true + } + + function updateBrightnessDebounced() { + // Use debouncing to prevent excessive updates + debounceTimer.restart() + } + + function setBrightness(newBrightness) { + if (!_currentDisplay || !available) { + console.warn("[Brightness] No display available for brightness control") + return false + } + + // Clamp brightness to valid range + newBrightness = Math.max(0, Math.min(100, newBrightness)) + + // Prevent setting if already setting + if (brightnessSetProcess.running) { + console.log("[Brightness] Brightness set already in progress, skipping...") + return false + } + + console.log("[Brightness] Setting brightness to:", newBrightness, "for display:", _currentDisplay.name) + + // Mark that we're setting brightness + _isSettingBrightness = true + + // Try ddcutil directly for external displays + if (_currentDisplay.method === "ddcutil" && _currentDisplay.index) { + brightnessSetProcess.command = ["ddcutil", "--display", _currentDisplay.index, "setvcp", "10", newBrightness.toString()] + } else { + // Use the brightness script for internal displays + brightnessSetProcess.command = ["sh", "-c", Quickshell.shellDir + "/Bin/brigthness.sh", "set", _currentDisplay.name, newBrightness.toString()] + } + brightnessSetProcess.running = true + + return true + } + + function setBrightnessDebounced(newBrightness) { + // Store the target brightness for debounced setting + _targetBrightness = newBrightness + + // Update UI immediately for responsiveness + if (brightness !== newBrightness) { + brightness = newBrightness + brightnessUpdated(brightness) + } + + setDebounceTimer.restart() + } + + // Process objects for brightness control + Process { + id: brightnessGetProcess + running: false + stdout: SplitParser { + onRead: function(line) { + var newBrightness = -1 + + // Handle ddcutil output format: "current value = X," + if (line.indexOf("current value =") !== -1) { + var match = line.match(/current value\s*=\s*(\d+)/) + if (match) { + newBrightness = parseFloat(match[1]) + } + } else { + // Handle direct numeric output + newBrightness = parseFloat(line.replace(/^\s+|\s+$/g, "")) + } + + if (!isNaN(newBrightness) && newBrightness >= 0) { + if (brightness !== newBrightness) { + brightness = newBrightness + brightnessUpdated(brightness) + console.log("[Brightness] Brightness updated to:", brightness) + } + } else { + console.warn("[Brightness] Invalid brightness value:", line) + } + } + } + onExited: function(exitCode, exitStatus) { + // Only log errors + if (exitCode !== 0) { + console.warn("[Brightness] Brightness get process failed with code:", exitCode) + } + } + } + + Process { + id: brightnessSetProcess + running: false + stdout: SplitParser { + onRead: function(line) { + var result = parseFloat(line.replace(/^\s+|\s+$/g, "")) + if (!isNaN(result) && result >= 0) { + brightness = result + brightnessUpdated(brightness) + console.log("[Brightness] Brightness set to:", brightness) + } else { + console.warn("[Brightness] Failed to set brightness - invalid output:", line) + } + } + } + onExited: function(exitCode, exitStatus) { + if (exitCode === 0) { + // If ddcutil succeeded but didn't output a number, refresh the brightness + if (_currentDisplay.method === "ddcutil") { + // Longer delay to let the display update and avoid conflicts + refreshTimer.interval = 1000 + refreshTimer.start() + } + } else { + console.warn("[Brightness] Set brightness process failed with exit code:", exitCode) + } + + // Clear the setting flag after a delay + settingCompleteTimer.start() + } + } + + // Timer to clear the setting flag + Timer { + id: settingCompleteTimer + interval: 800 + repeat: false + onTriggered: { + _isSettingBrightness = false + } + } + + // Timer to refresh brightness after setting + Timer { + id: refreshTimer + interval: 500 + repeat: false + onTriggered: updateBrightnessDebounced() + } + + + + function increaseBrightness(step = 5) { + return setBrightnessDebounced(brightness + step) + } + + function decreaseBrightness(step = 5) { + return setBrightnessDebounced(brightness - step) + } + + function setDisplay(displayIndex) { + if (displayIndex >= 0 && displayIndex < detectedDisplays.length) { + _currentDisplay = detectedDisplays[displayIndex] + currentMethod = _currentDisplay.method + methodChanged(currentMethod) + updateBrightness() + return true + } + return false + } + + function getDisplayInfo() { + return _currentDisplay || null + } + + function getAvailableMethods() { + return _brightnessMethods + } + + function getDetectedDisplays() { + return detectedDisplays + } + + // Refresh brightness periodically - but less frequently + Timer { + interval: 5000 // Update every 5 seconds instead of 2 + running: available && _initialized + repeat: true + onTriggered: updateBrightness() + } + + // Debounce timer for UI updates + Timer { + id: debounceTimer + interval: 300 + repeat: false + onTriggered: updateBrightness() + } + + // Debounce timer for setting brightness + Timer { + id: setDebounceTimer + interval: 100 + repeat: false + onTriggered: setBrightness(_targetBrightness) + } +} \ No newline at end of file From 22df558e144375baeae550b1b2926c17da4e5c24 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Fri, 15 Aug 2025 14:05:02 +0200 Subject: [PATCH 298/394] Brightness implementation with IPC --- Modules/Bar/Brightness.qml | 25 +- Modules/Demo/DemoPanel.qml | 4 +- Modules/Settings/Tabs/DisplayTab.qml | 49 +++ Services/BrightnessService.qml | 627 +++++++++++---------------- Services/IPCManager.qml | 12 + Services/Settings.qml | 10 + 6 files changed, 335 insertions(+), 392 deletions(-) diff --git a/Modules/Bar/Brightness.qml b/Modules/Bar/Brightness.qml index 0d5d690..aa754b7 100644 --- a/Modules/Bar/Brightness.qml +++ b/Modules/Bar/Brightness.qml @@ -12,6 +12,7 @@ Item { // Used to avoid opening the pill on Quickshell startup property bool firstBrightnessReceived: false + property real lastBrightness: -1 function getIcon() { if (!BrightnessService.available) { @@ -25,18 +26,28 @@ Item { // Connection used to open the pill when brightness changes Connections { - target: Brightness + target: BrightnessService.focusedMonitor function onBrightnessUpdated() { - // console.log("[Bar:Brightness] onBrightnessUpdated") + var currentBrightness = BrightnessService.brightness + + // Ignore if this is the first time or if brightness hasn't actually changed if (!firstBrightnessReceived) { - // Ignore the first brightness change firstBrightnessReceived = true - } else { + lastBrightness = currentBrightness + return + } + + // Only show pill if brightness actually changed (not just loaded from settings) + if (Math.abs(currentBrightness - lastBrightness) > 0.1) { pill.show() } + + lastBrightness = currentBrightness } } + + NPill { id: pill icon: getIcon() @@ -44,15 +55,15 @@ Item { collapsedIconColor: Colors.mOnSurface autoHide: true text: Math.round(BrightnessService.brightness) + "%" - tooltipText: "Brightness: " + Math.round(BrightnessService.brightness) + "%\nMethod: " + BrightnessService.currentMethod + "\nLeft click for advanced settings.\nScroll up/down to change BrightnessService." + tooltipText: "Brightness: " + Math.round(BrightnessService.brightness) + "%\nMethod: " + BrightnessService.currentMethod + "\nLeft click for advanced settings.\nScroll up/down to change brightness." onWheel: function (angle) { if (!BrightnessService.available) return if (angle > 0) { - BrightnessService.increaseBrightness(1) + BrightnessService.increaseBrightness() } else if (angle < 0) { - BrightnessService.decreaseBrightness(1) + BrightnessService.decreaseBrightness() } } onClicked: { diff --git a/Modules/Demo/DemoPanel.qml b/Modules/Demo/DemoPanel.qml index 91e797b..ed5e613 100644 --- a/Modules/Demo/DemoPanel.qml +++ b/Modules/Demo/DemoPanel.qml @@ -294,7 +294,7 @@ NLoader { icon: "brightness_low" fontPointSize: Style.fontSizeLarge * scaling onClicked: { - Brightness.decreaseBrightness(1) + Brightness.decreaseBrightness() } } NSlider { @@ -311,7 +311,7 @@ NLoader { icon: "brightness_high" fontPointSize: Style.fontSizeLarge * scaling onClicked: { - Brightness.increaseBrightness(1) + Brightness.increaseBrightness() } } } diff --git a/Modules/Settings/Tabs/DisplayTab.qml b/Modules/Settings/Tabs/DisplayTab.qml index 3234699..c4694bd 100644 --- a/Modules/Settings/Tabs/DisplayTab.qml +++ b/Modules/Settings/Tabs/DisplayTab.qml @@ -47,6 +47,55 @@ Item { color: Colors.mOnSurface } + // Brightness Section + ColumnLayout { + spacing: Style.marginSmall * scaling + Layout.fillWidth: true + Layout.topMargin: Style.marginSmall * scaling + + NText { + text: "Brightness Step Size" + font.weight: Style.fontWeightBold + color: Colors.mOnSurface + } + + NText { + text: "Adjust the step size for brightness changes (scroll wheel, ipc bind)" + font.pointSize: Style.fontSizeSmall * scaling + color: Colors.mOnSurface + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginMedium * scaling + + NSlider { + Layout.fillWidth: true + from: 1 + to: 50 + value: Settings.data.brightness.brightnessStep + stepSize: 1 + onMoved: { + Settings.data.brightness.brightnessStep = value + } + } + + NText { + text: Settings.data.brightness.brightnessStep + "%" + Layout.alignment: Qt.AlignVCenter + color: Colors.mOnSurface + } + } + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginLarge * 2 * scaling + Layout.bottomMargin: Style.marginLarge * scaling + } + Repeater { model: Quickshell.screens || [] delegate: Rectangle { diff --git a/Services/BrightnessService.qml b/Services/BrightnessService.qml index 31b0866..ce9e130 100644 --- a/Services/BrightnessService.qml +++ b/Services/BrightnessService.qml @@ -1,4 +1,5 @@ pragma Singleton +pragma ComponentBehavior: Bound import QtQuick import Quickshell @@ -7,422 +8,282 @@ import Quickshell.Io Singleton { id: root - // Public properties - property real brightness: 0 - property real maxBrightness: 100 - property bool available: false - property string currentMethod: "" - property var detectedDisplays: [] + property list ddcMonitors: [] + readonly property list monitors: variants.instances + property bool appleDisplayPresent: false - // Private properties - property var _brightnessMethods: [] - property var _currentDisplay: null - property bool _initialized: false - property real _targetBrightness: 0 - property bool _isSettingBrightness: false + // Public properties for backward compatibility + readonly property real brightness: focusedMonitor ? focusedMonitor.brightness * 100 : Settings.data.brightness.lastBrightness + readonly property bool available: focusedMonitor !== null + readonly property string currentMethod: focusedMonitor ? focusedMonitor.method : Settings.data.brightness.lastMethod + readonly property var detectedDisplays: monitors.map(m => ({ + name: m.modelData.name, + type: m.isDdc ? "external" : "internal", + method: m.method, + index: m.busNum + })) - // Signal when brightness changes - signal brightnessUpdated(real newBrightness) - signal methodChanged(string newMethod) - - // Initialize the service - Component.onCompleted: { - initializeBrightness() + // Get the currently focused monitor + readonly property Monitor focusedMonitor: { + if (monitors.length === 0) return null + // For now, return the first monitor. Could be enhanced to detect focused monitor + return monitors[0] } - function initializeBrightness() { - if (_initialized) return - - console.log("[Brightness] Initializing brightness service...") - - // Start method detection - detectMethods() - - _initialized = true + function getMonitorForScreen(screen: ShellScreen): var { + return monitors.find(m => m.modelData === screen) } - function detectMethods() { - _brightnessMethods = [] - - // Check for brightnessctl - brightnessctlProcess.running = true - - // Check for ddcutil - ddcutilProcess.running = true - - // Internal backlight is always available if we can access /sys/class/backlight - backlightCheck.running = true - - console.log("[Brightness] Starting method detection...") - } - - function checkMethodsComplete() { - // Check if all method detection processes have finished - if (!brightnessctlProcess.running && !ddcutilProcess.running && !backlightCheck.running) { - console.log("[Brightness] Available methods:", _brightnessMethods) - - // Now detect displays - detectDisplays() + function increaseBrightness(step = null): void { + if (focusedMonitor) { + var stepSize = step !== null ? step : Settings.data.brightness.brightnessStep + focusedMonitor.setBrightness(focusedMonitor.brightness + (stepSize / 100)) } } - // Process objects for method detection - Process { - id: brightnessctlProcess - command: ["which", "brightnessctl"] - running: false - onExited: function(exitCode, exitStatus) { - if (exitCode === 0) { - _brightnessMethods.push("brightnessctl") - console.log("[Brightness] brightnessctl available") - } - checkMethodsComplete() + function decreaseBrightness(step = null): void { + if (focusedMonitor) { + var stepSize = step !== null ? step : Settings.data.brightness.brightnessStep + focusedMonitor.setBrightness(focusedMonitor.brightness - (stepSize / 100)) } } - Process { - id: ddcutilProcess - command: ["which", "ddcutil"] - running: false - onExited: function(exitCode, exitStatus) { - if (exitCode === 0) { - _brightnessMethods.push("ddcutil") - console.log("[Brightness] ddcutil available") - } - checkMethodsComplete() + function setBrightness(newBrightness: real): void { + if (focusedMonitor) { + focusedMonitor.setBrightness(newBrightness / 100) } } - Process { - id: backlightCheck - command: ["test", "-d", "/sys/class/backlight"] - running: false - onExited: function(exitCode, exitStatus) { - if (exitCode === 0) { - _brightnessMethods.push("internal") - console.log("[Brightness] Internal backlight available") - } - checkMethodsComplete() + function setBrightnessDebounced(newBrightness: real): void { + if (focusedMonitor) { + focusedMonitor.setBrightnessDebounced(newBrightness / 100) } } - function detectDisplays() { - detectedDisplays = [] - - // Get internal displays - backlightProcess.running = true - - // Get external displays via ddcutil - if (_brightnessMethods.indexOf("ddcutil") !== -1) { - ddcutilDetectProcess.running = true - } else { - // If no ddcutil, just check internal displays - checkDisplaysComplete() - } - - console.log("[Brightness] Starting display detection...") + // Backward compatibility functions + function updateBrightness(): void { + // No longer needed with the new architecture } - function checkDisplaysComplete() { - // Check if all display detection processes have finished - var internalFinished = !backlightProcess.running - var externalFinished = _brightnessMethods.indexOf("ddcutil") === -1 || !ddcutilDetectProcess.running - - if (internalFinished && externalFinished) { - console.log("[Brightness] Detected displays:", detectedDisplays) - - // Set current display to first available - if (detectedDisplays.length > 0) { - _currentDisplay = detectedDisplays[0] - currentMethod = _currentDisplay.method - available = true - console.log("[Brightness] Using display:", _currentDisplay.name, "method:", currentMethod) - - // Start initial brightness update - updateBrightness() - } else { - console.warn("[Brightness] No displays detected") - } - } - } - - // Process objects for display detection - Process { - id: backlightProcess - command: ["ls", "/sys/class/backlight"] - running: false - stdout: SplitParser { - onRead: function(line) { - var trimmedLine = line.replace(/^\s+|\s+$/g, "") - if (trimmedLine) { - detectedDisplays.push({ - name: trimmedLine, - type: "internal", - method: "internal" - }) - } - } - } - onExited: function(exitCode, exitStatus) { - checkDisplaysComplete() - } - } - - Process { - id: ddcutilDetectProcess - command: ["ddcutil", "detect"] - running: false - stdout: SplitParser { - onRead: function(line) { - console.log("[Brightness] ddcutil detect line:", line) - if (line.indexOf("Display") !== -1) { - // Simple parsing for Display number - var parts = line.split("Display") - if (parts.length > 1) { - var numberPart = parts[1].replace(/^\s+|\s+$/g, "") - var number = numberPart.split(" ")[0] - if (number && !isNaN(number)) { - detectedDisplays.push({ - name: "Display " + number, - type: "external", - method: "ddcutil", - index: number - }) - console.log("[Brightness] Added external display:", "Display " + number) - } - } - } - // Also look for connector information - if (line.indexOf("DRM connector:") !== -1) { - console.log("[Brightness] Found DRM connector:", line) - } - } - } - onExited: function(exitCode, exitStatus) { - checkDisplaysComplete() - } - } - - function updateBrightness() { - if (!_currentDisplay) return - - // Prevent multiple simultaneous brightness checks - if (brightnessGetProcess.running) { - console.log("[Brightness] Brightness check already in progress, skipping...") - return - } - - // Don't update if we're currently setting brightness - if (_isSettingBrightness) { - console.log("[Brightness] Skipping update while setting brightness...") - return - } - - console.log("[Brightness] Updating brightness for display:", _currentDisplay.name) - - // Try the brightness script first - if (_currentDisplay.method === "ddcutil" && _currentDisplay.index) { - // For ddcutil, try using the display index directly - brightnessGetProcess.command = ["ddcutil", "--display", _currentDisplay.index, "getvcp", "10"] - } else { - // Use the brightness script - brightnessGetProcess.command = ["sh", "-c", Quickshell.shellDir + "/Bin/brigthness.sh", "get", _currentDisplay.name] - } - brightnessGetProcess.running = true - } - - function updateBrightnessDebounced() { - // Use debouncing to prevent excessive updates - debounceTimer.restart() - } - - function setBrightness(newBrightness) { - if (!_currentDisplay || !available) { - console.warn("[Brightness] No display available for brightness control") - return false - } - - // Clamp brightness to valid range - newBrightness = Math.max(0, Math.min(100, newBrightness)) - - // Prevent setting if already setting - if (brightnessSetProcess.running) { - console.log("[Brightness] Brightness set already in progress, skipping...") - return false - } - - console.log("[Brightness] Setting brightness to:", newBrightness, "for display:", _currentDisplay.name) - - // Mark that we're setting brightness - _isSettingBrightness = true - - // Try ddcutil directly for external displays - if (_currentDisplay.method === "ddcutil" && _currentDisplay.index) { - brightnessSetProcess.command = ["ddcutil", "--display", _currentDisplay.index, "setvcp", "10", newBrightness.toString()] - } else { - // Use the brightness script for internal displays - brightnessSetProcess.command = ["sh", "-c", Quickshell.shellDir + "/Bin/brigthness.sh", "set", _currentDisplay.name, newBrightness.toString()] - } - brightnessSetProcess.running = true - + function setDisplay(displayIndex: int): bool { + // No longer needed with the new architecture return true } - function setBrightnessDebounced(newBrightness) { - // Store the target brightness for debounced setting - _targetBrightness = newBrightness - - // Update UI immediately for responsiveness - if (brightness !== newBrightness) { - brightness = newBrightness - brightnessUpdated(brightness) - } - - setDebounceTimer.restart() + function getDisplayInfo(): var { + return focusedMonitor ? { + name: focusedMonitor.modelData.name, + type: focusedMonitor.isDdc ? "external" : "internal", + method: focusedMonitor.method, + index: focusedMonitor.busNum + } : null } - // Process objects for brightness control - Process { - id: brightnessGetProcess - running: false - stdout: SplitParser { - onRead: function(line) { - var newBrightness = -1 - - // Handle ddcutil output format: "current value = X," - if (line.indexOf("current value =") !== -1) { - var match = line.match(/current value\s*=\s*(\d+)/) - if (match) { - newBrightness = parseFloat(match[1]) - } - } else { - // Handle direct numeric output - newBrightness = parseFloat(line.replace(/^\s+|\s+$/g, "")) - } - - if (!isNaN(newBrightness) && newBrightness >= 0) { - if (brightness !== newBrightness) { - brightness = newBrightness - brightnessUpdated(brightness) - console.log("[Brightness] Brightness updated to:", brightness) - } - } else { - console.warn("[Brightness] Invalid brightness value:", line) - } - } - } - onExited: function(exitCode, exitStatus) { - // Only log errors - if (exitCode !== 0) { - console.warn("[Brightness] Brightness get process failed with code:", exitCode) - } - } + function getAvailableMethods(): list { + var methods = [] + if (monitors.some(m => m.isDdc)) methods.push("ddcutil") + if (monitors.some(m => !m.isDdc)) methods.push("internal") + if (appleDisplayPresent) methods.push("apple") + return methods } - Process { - id: brightnessSetProcess - running: false - stdout: SplitParser { - onRead: function(line) { - var result = parseFloat(line.replace(/^\s+|\s+$/g, "")) - if (!isNaN(result) && result >= 0) { - brightness = result - brightnessUpdated(brightness) - console.log("[Brightness] Brightness set to:", brightness) - } else { - console.warn("[Brightness] Failed to set brightness - invalid output:", line) - } - } - } - onExited: function(exitCode, exitStatus) { - if (exitCode === 0) { - // If ddcutil succeeded but didn't output a number, refresh the brightness - if (_currentDisplay.method === "ddcutil") { - // Longer delay to let the display update and avoid conflicts - refreshTimer.interval = 1000 - refreshTimer.start() - } - } else { - console.warn("[Brightness] Set brightness process failed with exit code:", exitCode) - } - - // Clear the setting flag after a delay - settingCompleteTimer.start() - } - } - - // Timer to clear the setting flag - Timer { - id: settingCompleteTimer - interval: 800 - repeat: false - onTriggered: { - _isSettingBrightness = false - } - } - - // Timer to refresh brightness after setting - Timer { - id: refreshTimer - interval: 500 - repeat: false - onTriggered: updateBrightnessDebounced() - } - - - - function increaseBrightness(step = 5) { - return setBrightnessDebounced(brightness + step) - } - - function decreaseBrightness(step = 5) { - return setBrightnessDebounced(brightness - step) - } - - function setDisplay(displayIndex) { - if (displayIndex >= 0 && displayIndex < detectedDisplays.length) { - _currentDisplay = detectedDisplays[displayIndex] - currentMethod = _currentDisplay.method - methodChanged(currentMethod) - updateBrightness() - return true - } - return false - } - - function getDisplayInfo() { - return _currentDisplay || null - } - - function getAvailableMethods() { - return _brightnessMethods - } - - function getDetectedDisplays() { + function getDetectedDisplays(): list { return detectedDisplays } - // Refresh brightness periodically - but less frequently - Timer { - interval: 5000 // Update every 5 seconds instead of 2 - running: available && _initialized - repeat: true - onTriggered: updateBrightness() + reloadableId: "brightness" + + onMonitorsChanged: { + ddcMonitors = [] + ddcProc.running = true } - // Debounce timer for UI updates - Timer { - id: debounceTimer - interval: 300 - repeat: false - onTriggered: updateBrightness() + Variants { + id: variants + model: Quickshell.screens + Monitor {} } - // Debounce timer for setting brightness - Timer { - id: setDebounceTimer - interval: 100 - repeat: false - onTriggered: setBrightness(_targetBrightness) + // Check for Apple Display support + Process { + running: true + command: ["sh", "-c", "which asdbctl >/dev/null 2>&1 && asdbctl get || echo ''"] + stdout: StdioCollector { + onStreamFinished: root.appleDisplayPresent = text.trim().length > 0 + } + } + + // Detect DDC monitors + Process { + id: ddcProc + command: ["ddcutil", "detect", "--brief"] + stdout: StdioCollector { + onStreamFinished: { + var displays = text.trim().split("\n\n").filter(d => d.startsWith("Display ")) + root.ddcMonitors = displays.map(d => { + var modelMatch = d.match(/Monitor:.*:(.*):.*/) + var busMatch = d.match(/I2C bus:[ ]*\/dev\/i2c-([0-9]+)/) + return { + model: modelMatch ? modelMatch[1] : "", + busNum: busMatch ? busMatch[1] : "" + } + }) + } + } + } + + + + component Monitor: QtObject { + id: monitor + + required property ShellScreen modelData + readonly property bool isDdc: root.ddcMonitors.some(m => m.model === modelData.model) + readonly property string busNum: root.ddcMonitors.find(m => m.model === modelData.model)?.busNum ?? "" + readonly property bool isAppleDisplay: root.appleDisplayPresent && modelData.model.startsWith("StudioDisplay") + readonly property string method: isAppleDisplay ? "apple" : (isDdc ? "ddcutil" : "internal") + + property real brightness: getStoredBrightness() + property real queuedBrightness: NaN + + // Signal for brightness changes + signal brightnessUpdated(real newBrightness) + + // Initialize brightness + readonly property Process initProc: Process { + stdout: StdioCollector { + onStreamFinished: { + console.log("[BrightnessService] Raw brightness data for", monitor.modelData.name + ":", text.trim()) + + if (monitor.isAppleDisplay) { + var val = parseInt(text.trim()) + if (!isNaN(val)) { + monitor.brightness = val / 101 + console.log("[BrightnessService] Apple display brightness:", monitor.brightness) + } + } else if (monitor.isDdc) { + var parts = text.trim().split(" ") + if (parts.length >= 2) { + var current = parseInt(parts[0]) + var max = parseInt(parts[1]) + if (!isNaN(current) && !isNaN(max) && max > 0) { + monitor.brightness = current / max + console.log("[BrightnessService] DDC brightness:", current + "/" + max + " =", monitor.brightness) + } + } + } else { + // Internal backlight + var parts = text.trim().split(" ") + if (parts.length >= 2) { + var current = parseInt(parts[0]) + var max = parseInt(parts[1]) + if (!isNaN(current) && !isNaN(max) && max > 0) { + monitor.brightness = current / max + console.log("[BrightnessService] Internal brightness:", current + "/" + max + " =", monitor.brightness) + } + } + } + + if (monitor.brightness > 0) { + // Save the detected brightness to settings + monitor.saveBrightness(monitor.brightness) + monitor.brightnessUpdated(monitor.brightness) + } + } + } + } + + // Timer for debouncing rapid changes + readonly property Timer timer: Timer { + interval: 200 + onTriggered: { + if (!isNaN(monitor.queuedBrightness)) { + monitor.setBrightness(monitor.queuedBrightness) + monitor.queuedBrightness = NaN + } + } + } + + function getStoredBrightness(): real { + // Try to get stored brightness for this specific monitor + var stored = Settings.data.brightness.monitorBrightness.find(m => m.name === modelData.name) + if (stored) { + return stored.brightness / 100 + } + // Fallback to general last brightness + return Settings.data.brightness.lastBrightness / 100 + } + + function saveBrightness(value: real): void { + var brightnessPercent = Math.round(value * 100) + + // Update general last brightness + Settings.data.brightness.lastBrightness = brightnessPercent + Settings.data.brightness.lastMethod = method + + // Update monitor-specific brightness + var monitorIndex = Settings.data.brightness.monitorBrightness.findIndex(m => m.name === modelData.name) + var monitorData = { + name: modelData.name, + brightness: brightnessPercent, + method: method + } + + if (monitorIndex >= 0) { + Settings.data.brightness.monitorBrightness[monitorIndex] = monitorData + } else { + Settings.data.brightness.monitorBrightness.push(monitorData) + } + } + + function setBrightness(value: real): void { + value = Math.max(0, Math.min(1, value)) + var rounded = Math.round(value * 100) + + if (Math.round(brightness * 100) === rounded) return + + if (isDdc && timer.running) { + queuedBrightness = value + return + } + + brightness = value + brightnessUpdated(brightness) + + // Save to settings + saveBrightness(value) + + if (isAppleDisplay) { + Quickshell.execDetached(["asdbctl", "set", rounded]) + } else if (isDdc) { + Quickshell.execDetached(["ddcutil", "-b", busNum, "setvcp", "10", rounded]) + } else { + Quickshell.execDetached(["brightnessctl", "s", rounded + "%"]) + } + + if (isDdc) { + timer.restart() + } + } + + function setBrightnessDebounced(value: real): void { + queuedBrightness = value + timer.restart() + } + + function initBrightness(): void { + if (isAppleDisplay) { + initProc.command = ["asdbctl", "get"] + } else if (isDdc) { + initProc.command = ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"] + } else { + // Internal backlight - try to find the first available backlight device + initProc.command = ["sh", "-c", "for dev in /sys/class/backlight/*; do if [ -f \"$dev/brightness\" ] && [ -f \"$dev/max_brightness\" ]; then echo \"$(cat $dev/brightness) $(cat $dev/max_brightness)\"; break; fi; done"] + } + initProc.running = true + } + + onBusNumChanged: initBrightness() + Component.onCompleted: initBrightness() } } \ No newline at end of file diff --git a/Services/IPCManager.qml b/Services/IPCManager.qml index 07f195b..745c529 100644 --- a/Services/IPCManager.qml +++ b/Services/IPCManager.qml @@ -48,4 +48,16 @@ Item { lockScreen.locked = !lockScreen.locked } } + + IpcHandler { + target: "brightness" + + function increase() { + BrightnessService.increaseBrightness() + } + + function decrease() { + BrightnessService.decreaseBrightness() + } + } } diff --git a/Services/Settings.qml b/Services/Settings.qml index 5e2d4f4..b5168bd 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -179,6 +179,16 @@ Singleton { property string fontFamily: "Roboto" // Family for all text property list monitorsScale: [] } + + // brightness + property JsonObject brightness + + brightness: JsonObject { + property real lastBrightness: 50.0 + property string lastMethod: "internal" + property list monitorBrightness: [] + property int brightnessStep: 5 + } } } } From 3f3a13d2548f907f5cab5efea1225a1a6e82d3bd Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 08:19:18 -0400 Subject: [PATCH 299/394] NComBox: refactored to follow the QML way, should help with bindings. --- Modules/Demo/DemoPanel.qml | 40 ++++++-- Modules/Settings/Tabs/AudioTab.qml | 12 ++- Modules/Settings/Tabs/ScreenRecorderTab.qml | 105 +++++++++++++++++--- Modules/Settings/Tabs/WallpaperTab.qml | 80 ++++++++++++++- Services/ColorSchemes.qml | 2 +- Services/Settings.qml | 2 - Services/Wallpapers.qml | 4 +- Widgets/NComboBox.qml | 87 +++++++++------- 8 files changed, 267 insertions(+), 65 deletions(-) diff --git a/Modules/Demo/DemoPanel.qml b/Modules/Demo/DemoPanel.qml index 9d3eaaf..0a5c748 100644 --- a/Modules/Demo/DemoPanel.qml +++ b/Modules/Demo/DemoPanel.qml @@ -221,12 +221,40 @@ NLoader { NComboBox { label: "Animal" - description: "What's your favorite" - optionsKeys: ["cat", "dog", "bird", "monkey", "fish", "turtle", "elephant", "tiger"] - optionsLabels: ["Cat", "Dog", "Bird", "Monkey", "Fish", "Turtle", "Elephant", "Tiger"] - currentKey: "cat" - onSelected: function (value) { - console.log("[DemoPanel] NComboBox: selected ", value) + description: "What's your favorite?" + model: ListModel { + ListElement { + key: "cat" + name: "Cat" + } + ListElement { + key: "dog" + name: "Dog" + } + ListElement { + key: "bird" + name: "Bird" + } + ListElement { + key: "fish" + name: "Fish" + } + ListElement { + key: "turtle" + name: "Turtle" + } + ListElement { + key: "elephant" + name: "Elephant" + } + ListElement { + key: "tiger" + name: "Tiger" + } + } + currentKey: "dog" + onSelected: function (key) { + console.log("[DemoPanel] NComboBox: selected ", key) } } diff --git a/Modules/Settings/Tabs/AudioTab.qml b/Modules/Settings/Tabs/AudioTab.qml index 39aaa64..3314c4e 100644 --- a/Modules/Settings/Tabs/AudioTab.qml +++ b/Modules/Settings/Tabs/AudioTab.qml @@ -256,8 +256,16 @@ ColumnLayout { id: audioVisualizerCombo label: "Visualization Type" description: "Choose a visualization type for media playback" - optionsKeys: ["none", "linear"] - optionsLabels: ["None", "Linear"] + model: ListModel { + ListElement { + key: "none" + name: "None" + } + ListElement { + key: "linear" + name: "Linear" + } + } currentKey: Settings.data.audio.visualizerType onSelected: function (key) { Settings.data.audio.visualizerType = key diff --git a/Modules/Settings/Tabs/ScreenRecorderTab.qml b/Modules/Settings/Tabs/ScreenRecorderTab.qml index 6240ae1..2ecce19 100644 --- a/Modules/Settings/Tabs/ScreenRecorderTab.qml +++ b/Modules/Settings/Tabs/ScreenRecorderTab.qml @@ -95,8 +95,24 @@ ColumnLayout { NComboBox { label: "Frame Rate" description: "Target frame rate for screen recordings (default: 60)" - optionsKeys: ["30", "60", "120", "240"] - optionsLabels: ["30 FPS", "60 FPS", "120 FPS", "240 FPS"] + model: ListModel { + ListElement { + key: "30" + name: "30 FPS" + } + ListElement { + key: "60" + name: "60 FPS" + } + ListElement { + key: "120" + name: "120 FPS" + } + ListElement { + key: "240" + name: "240 FPS" + } + } currentKey: Settings.data.screenRecorder.frameRate onSelected: function (key) { Settings.data.screenRecorder.frameRate = key @@ -107,8 +123,24 @@ ColumnLayout { NComboBox { label: "Video Quality" description: "Higher quality results in larger file sizes" - optionsKeys: ["medium", "high", "very_high", "ultra"] - optionsLabels: ["Medium", "High", "Very High", "Ultra"] + model: ListModel { + ListElement { + key: "medium" + name: "Medium" + } + ListElement { + key: "high" + name: "High" + } + ListElement { + key: "very_high" + name: "Very High" + } + ListElement { + key: "ultra" + name: "Ultra" + } + } currentKey: Settings.data.screenRecorder.quality onSelected: function (key) { Settings.data.screenRecorder.quality = key @@ -119,8 +151,28 @@ ColumnLayout { NComboBox { label: "Video Codec" description: "Different codecs offer different compression and compatibility" - optionsKeys: ["h264", "hevc", "av1", "vp8", "vp9"] - optionsLabels: ["H264", "HEVC", "AV1", "VP8", "VP9"] + model: ListModel { + ListElement { + key: "h264" + name: "H264" + } + ListElement { + key: "hevc" + name: "HEVC" + } + ListElement { + key: "av1" + name: "AV1" + } + ListElement { + key: "vp8" + name: "VP8" + } + ListElement { + key: "vp9" + name: "VP9" + } + } currentKey: Settings.data.screenRecorder.videoCodec onSelected: function (key) { Settings.data.screenRecorder.videoCodec = key @@ -131,8 +183,16 @@ ColumnLayout { NComboBox { label: "Color Range" description: "Limited is recommended for better compatibility" - optionsKeys: ["limited", "full"] - optionsLabels: ["Limited", "Full"] + model: ListModel { + ListElement { + key: "limited" + name: "Limited" + } + ListElement { + key: "full" + name: "Full" + } + } currentKey: Settings.data.screenRecorder.colorRange onSelected: function (key) { Settings.data.screenRecorder.colorRange = key @@ -163,8 +223,20 @@ ColumnLayout { NComboBox { label: "Audio Source" description: "Audio source to capture during recording" - optionsKeys: ["default_output", "default_input", "both"] - optionsLabels: ["System Audio", "Microphone", "System Audio + Microphone"] + model: ListModel { + ListElement { + key: "default_output" + name: "System Output" + } + ListElement { + key: "default_input" + name: "Microphone Input" + } + ListElement { + key: "both" + name: "System Output + Microphone Input" + } + } currentKey: Settings.data.screenRecorder.audioSource onSelected: function (key) { Settings.data.screenRecorder.audioSource = key @@ -175,8 +247,17 @@ ColumnLayout { NComboBox { label: "Audio Codec" description: "Opus is recommended for best performance and smallest audio size" - optionsKeys: ["opus", "aac"] - optionsLabels: ["OPUS", "AAC"] + model: ListModel { + ListElement { + key: "opus" + name: "Opus" + } + ListElement { + key: "aac" + name: "AAC" + } + } + currentKey: Settings.data.screenRecorder.audioCodec onSelected: function (key) { Settings.data.screenRecorder.audioCodec = key diff --git a/Modules/Settings/Tabs/WallpaperTab.qml b/Modules/Settings/Tabs/WallpaperTab.qml index d657b1b..41288d9 100644 --- a/Modules/Settings/Tabs/WallpaperTab.qml +++ b/Modules/Settings/Tabs/WallpaperTab.qml @@ -183,8 +183,24 @@ ColumnLayout { NComboBox { label: "Resize Mode" description: "How SWWW should resize wallpapers to fit the screen" - optionsKeys: ["no", "crop", "fit", "stretch"] - optionsLabels: ["No", "Crop", "Fit", "Stretch"] + model: ListModel { + ListElement { + key: "no" + name: "No" + } + ListElement { + key: "crop" + name: "Crop" + } + ListElement { + key: "fit" + name: "Fit" + } + ListElement { + key: "stretch" + name: "Stretch" + } + } currentKey: Settings.data.wallpaper.swww.resizeMethod onSelected: function (key) { Settings.data.wallpaper.swww.resizeMethod = key @@ -195,8 +211,64 @@ ColumnLayout { NComboBox { label: "Transition Type" description: "Animation type when switching between wallpapers" - optionsKeys: ["none", "simple", "fade", "left", "right", "top", "bottom", "wipe", "wave", "grow", "center", "any", "outer", "random"] - optionsLabels: ["None", "Simple", "Fade", "Left", "Right", "Top", "Bottom", "Wipe", "Wave", "Grow", "Center", "Any", "Outer", "Random"] + model: ListModel { + ListElement { + key: "none" + name: "None" + } + ListElement { + key: "simple" + name: "Simple" + } + ListElement { + key: "fade" + name: "Fade" + } + ListElement { + key: "left" + name: "Left" + } + ListElement { + key: "right" + name: "Right" + } + ListElement { + key: "top" + name: "Top" + } + ListElement { + key: "bottom" + name: "Bottom" + } + ListElement { + key: "wipe" + name: "Wipe" + } + ListElement { + key: "wave" + name: "Wave" + } + ListElement { + key: "grow" + name: "Grow" + } + ListElement { + key: "center" + name: "Center" + } + ListElement { + key: "any" + name: "Any" + } + ListElement { + key: "outer" + name: "Outer" + } + ListElement { + key: "random" + name: "Random" + } + } currentKey: Settings.data.wallpaper.swww.transitionType onSelected: function (key) { Settings.data.wallpaper.swww.transitionType = key diff --git a/Services/ColorSchemes.qml b/Services/ColorSchemes.qml index d8c3638..2075510 100644 --- a/Services/ColorSchemes.qml +++ b/Services/ColorSchemes.qml @@ -9,7 +9,7 @@ Singleton { id: root Component.onCompleted: { - console.log("[ColorSchemes] Service initialized") + console.log("[ColorSchemes] Service started") loadColorSchemes() } diff --git a/Services/Settings.qml b/Services/Settings.qml index 5e2d4f4..5f79da6 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -70,7 +70,6 @@ Singleton { property bool showActiveWindowIcon: false property bool showSystemInfo: false property bool showMedia: false - // New: optional taskbar visibility in bar property bool showTaskbar: false property list monitors: [] } @@ -107,7 +106,6 @@ Singleton { property string quality: "very_high" property string colorRange: "limited" property bool showCursor: true - // New: optional audio source selection (default: system output) property string audioSource: "default_output" } diff --git a/Services/Wallpapers.qml b/Services/Wallpapers.qml index 82e9e38..a8d9847 100644 --- a/Services/Wallpapers.qml +++ b/Services/Wallpapers.qml @@ -9,7 +9,7 @@ Singleton { id: root Component.onCompleted: { - console.log("[Wallpapers] Service initialized") + console.log("[Wallpapers] Service started") loadWallpapers() // Wallpaper is set when the settings are loaded. @@ -23,7 +23,7 @@ Singleton { // SWWW property string transitionType: Settings.data.wallpaper.swww.transitionType property var randomChoices: ["simple", "fade", "left", "right", "top", "bottom", "wipe", "wave", "grow", "center", "any", "outer"] - + function loadWallpapers() { console.log("[Wallpapers] Load Wallpapers") scanning = true diff --git a/Widgets/NComboBox.qml b/Widgets/NComboBox.qml index f489011..0c36869 100644 --- a/Widgets/NComboBox.qml +++ b/Widgets/NComboBox.qml @@ -12,8 +12,9 @@ ColumnLayout { property string label: "" property string description: "" - property list optionsKeys: [] - property list optionsLabels: [] + property ListModel model: { + + } property string currentKey: '' signal selected(string key) @@ -24,14 +25,12 @@ ColumnLayout { ColumnLayout { spacing: Style.marginTiniest * scaling Layout.fillWidth: true - NText { text: label font.pointSize: Style.fontSizeMedium * scaling font.weight: Style.fontWeightBold color: Colors.mOnSurface } - NText { text: description font.pointSize: Style.fontSizeSmall * scaling @@ -40,19 +39,25 @@ ColumnLayout { } } + function findIndexByKey(key) { + for (var i = 0; i < root.model.count; i++) { + if (root.model.get(i).key === key) { + return i + } + } + return -1 + } + ComboBox { id: combo - Layout.fillWidth: true Layout.preferredHeight: height - - model: optionsKeys - currentIndex: model.indexOf(currentKey) + model: model + currentIndex: findIndexByKey(currentKey) onActivated: { - root.selected(model[combo.currentIndex]) + root.selected(model.get(combo.currentIndex).key) } - // Rounded background background: Rectangle { implicitWidth: 120 * scaling implicitHeight: preferredHeight @@ -62,18 +67,16 @@ ColumnLayout { radius: Style.radiusMedium * scaling } - // Label (currently selected) contentItem: NText { leftPadding: Style.marginLarge * scaling rightPadding: combo.indicator.width + Style.marginLarge * scaling font.pointSize: Style.fontSizeMedium * scaling verticalAlignment: Text.AlignVCenter elide: Text.ElideRight - text: (combo.currentIndex >= 0 - && combo.currentIndex < root.optionsLabels.length) ? root.optionsLabels[combo.currentIndex] : "" + text: (combo.currentIndex >= 0 && combo.currentIndex < root.model.count) ? root.model.get( + combo.currentIndex).name : "" } - // Drop down indicator indicator: NText { x: combo.width - width - Style.marginMedium * scaling y: combo.topPadding + (combo.availableHeight - height) / 2 @@ -89,11 +92,43 @@ ColumnLayout { padding: Style.marginMedium * scaling contentItem: ListView { + property var comboBoxRoot: root clip: true implicitHeight: contentHeight - model: combo.popup.visible ? combo.delegateModel : null - currentIndex: combo.highlightedIndex + model: combo.popup.visible ? root.model : null ScrollIndicator.vertical: ScrollIndicator {} + + delegate: ItemDelegate { + width: combo.width + hoverEnabled: true + highlighted: ListView.view.currentIndex === index + + onHoveredChanged: { + if (hovered) { + ListView.view.currentIndex = index + } + } + + onClicked: { + ListView.view.comboBoxRoot.selected(ListView.view.comboBoxRoot.model.get(index).key) + combo.currentIndex = index + combo.popup.close() + } + + contentItem: NText { + text: name + font.pointSize: Style.fontSizeMedium * scaling + color: highlighted ? Colors.mSurface : Colors.mOnSurface + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + background: Rectangle { + width: combo.width - Style.marginMedium * scaling * 3 + color: highlighted ? Colors.mTertiary : "transparent" + radius: Style.radiusSmall * scaling + } + } } background: Rectangle { @@ -103,25 +138,5 @@ ColumnLayout { radius: Style.radiusMedium * scaling } } - - delegate: ItemDelegate { - width: combo.width - highlighted: combo.highlightedIndex === index - - contentItem: NText { - text: (combo.model.indexOf(modelData) >= 0 && combo.model.indexOf( - modelData) < root.optionsLabels.length) ? root.optionsLabels[combo.model.indexOf(modelData)] : "" - font.pointSize: Style.fontSizeMedium * scaling - color: highlighted ? Colors.mSurface : Colors.mOnSurface - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - } - - background: Rectangle { - width: combo.width - Style.marginMedium * scaling * 3 - color: highlighted ? Colors.mTertiary : "transparent" - radius: Style.radiusSmall * scaling - } - } } } From 25e4dbcfc1196478114a652d1f6604704bed21e0 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Fri, 15 Aug 2025 14:33:57 +0200 Subject: [PATCH 300/394] Fix Brightness (doesn't regenerate colors anymore) --- Modules/Settings/Tabs/DisplayTab.qml | 6 ++++-- Services/BrightnessService.qml | 4 ++++ Services/Settings.qml | 7 ++++++- Services/Wallpapers.qml | 9 ++++++++- 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/Modules/Settings/Tabs/DisplayTab.qml b/Modules/Settings/Tabs/DisplayTab.qml index c4694bd..3d1211a 100644 --- a/Modules/Settings/Tabs/DisplayTab.qml +++ b/Modules/Settings/Tabs/DisplayTab.qml @@ -77,8 +77,10 @@ Item { to: 50 value: Settings.data.brightness.brightnessStep stepSize: 1 - onMoved: { - Settings.data.brightness.brightnessStep = value + onPressedChanged: { + if (!pressed) { + Settings.data.brightness.brightnessStep = value + } } } diff --git a/Services/BrightnessService.qml b/Services/BrightnessService.qml index ce9e130..dc7104b 100644 --- a/Services/BrightnessService.qml +++ b/Services/BrightnessService.qml @@ -204,6 +204,8 @@ Singleton { } } + + function getStoredBrightness(): real { // Try to get stored brightness for this specific monitor var stored = Settings.data.brightness.monitorBrightness.find(m => m.name === modelData.name) @@ -283,6 +285,8 @@ Singleton { initProc.running = true } + + onBusNumChanged: initBrightness() Component.onCompleted: initBrightness() } diff --git a/Services/Settings.qml b/Services/Settings.qml index bc4568c..100ac64 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -24,6 +24,9 @@ Singleton { // Used to access via Settings.data.xxx.yyy property var data: adapter + + // Flag to prevent unnecessary wallpaper calls during reloads + property bool isInitialLoad: true // Needed to only have one NPanel loaded at a time. <--- VERY BROKEN //property var openPanel: null @@ -47,10 +50,12 @@ Singleton { onLoaded: function () { console.log("[Settings] Loaded") Qt.callLater(function () { - if (adapter.wallpaper.current !== "") { + // Only set wallpaper on initial load, not on reloads + if (isInitialLoad && adapter.wallpaper.current !== "") { console.log("[Settings] Set current wallpaper", adapter.wallpaper.current) Wallpapers.setCurrentWallpaper(adapter.wallpaper.current, true) } + isInitialLoad = false }) } onLoadFailed: function (error) { diff --git a/Services/Wallpapers.qml b/Services/Wallpapers.qml index a8d9847..301ab50 100644 --- a/Services/Wallpapers.qml +++ b/Services/Wallpapers.qml @@ -39,6 +39,8 @@ Singleton { } function setCurrentWallpaper(path, isInitial) { + // Only generate colors if the wallpaper actually changed + var wallpaperChanged = currentWallpaper !== path currentWallpaper = path if (!isInitial) { @@ -62,7 +64,10 @@ Singleton { randomWallpaperTimer.restart() } - generateColors() + // Only generate colors if the wallpaper actually changed + if (wallpaperChanged) { + generateColors() + } } function setRandomWallpaper() { @@ -91,7 +96,9 @@ Singleton { } function generateColors() { + console.log("[Wallpapers] generateColors() called, generateColors setting:", Settings.data.wallpaper.generateColors) if (Settings.data.wallpaper.generateColors) { + console.log("[Wallpapers] Starting color generation process") generateThemeProcess.running = true } } From c352997dd9300323cfa4f47ea6ac550baf8e3c52 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Fri, 15 Aug 2025 15:04:51 +0200 Subject: [PATCH 301/394] Make bar wifi symbol reflect signal strength --- Modules/Bar/WiFi.qml | 4 +++- Services/Network.qml | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Modules/Bar/WiFi.qml b/Modules/Bar/WiFi.qml index 4148a4a..5bd5c89 100644 --- a/Modules/Bar/WiFi.qml +++ b/Modules/Bar/WiFi.qml @@ -15,13 +15,15 @@ NIconButton { showBorder: false icon: { let connected = false + let signalStrength = 0 for (const net in network.networks) { if (network.networks[net].connected) { connected = true + signalStrength = network.networks[net].signal break } } - return connected ? network.signalIcon(parent.currentSignal) : "wifi_off" + return connected ? network.signalIcon(signalStrength) : "wifi_off" } tooltipText: "WiFi Networks" onClicked: { diff --git a/Services/Network.qml b/Services/Network.qml index 92527b8..f064790 100644 --- a/Services/Network.qml +++ b/Services/Network.qml @@ -214,6 +214,8 @@ QtObject { scanProcess.existingNetwork = {} } } + + } property Process connectProcess: Process { From e86d7e5beb335bfcac08314fc4ce345b9990ff9e Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 09:08:05 -0400 Subject: [PATCH 302/394] SettingsPanel: finally made the panel ordering easier and also saved ram --- Modules/Settings/SettingsPanel.qml | 174 ++++++++++++-------- Modules/Settings/Tabs/ScreenRecorderTab.qml | 1 - 2 files changed, 102 insertions(+), 73 deletions(-) diff --git a/Modules/Settings/SettingsPanel.qml b/Modules/Settings/SettingsPanel.qml index 61387d6..6db4056 100644 --- a/Modules/Settings/SettingsPanel.qml +++ b/Modules/Settings/SettingsPanel.qml @@ -39,7 +39,6 @@ NLoader { // Start hide animation bgRect.scaleValue = 0.8 bgRect.opacityValue = 0.0 - // Hide after animation completes hideTimer.start() } @@ -48,12 +47,7 @@ NLoader { Connections { target: panel function onDismissed() { - // Start hide animation - bgRect.scaleValue = 0.8 - bgRect.opacityValue = 0.0 - - // Hide after animation completes - hideTimer.start() + hide() } } @@ -70,79 +64,126 @@ NLoader { WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand - // Order is NOT relevant, and should match the Tabs.xxxx {} in the StackLayout + Component { + id: generalTab + Tabs.GeneralTab {} + } + Component { + id: barTab + Tabs.BarTab {} + } + Component { + id: audioTab + Tabs.AudioTab {} + } + Component { + id: displayTab + Tabs.DisplayTab {} + } + Component { + id: networkTab + Tabs.NetworkTab {} + } + Component { + id: timeWeatherTab + Tabs.TimeWeatherTab {} + } + Component { + id: colorSchemeTab + Tabs.ColorSchemeTab {} + } + Component { + id: wallpaperTab + Tabs.WallpaperTab {} + } + Component { + id: wallpaperSelectorTab + Tabs.WallpaperSelectorTab {} + } + Component { + id: screenRecorderTab + Tabs.ScreenRecorderTab {} + } + Component { + id: aboutTab + Tabs.AboutTab {} + } + property var tabsModel: [{ "id": SettingsPanel.Tab.General, "label": "General", - "icon": "tune" + "icon": "tune", + "source": generalTab }, { "id": SettingsPanel.Tab.Bar, "label": "Bar", - "icon": "web_asset" + "icon": "web_asset", + "source": barTab }, { "id": SettingsPanel.Tab.Audio, "label": "Audio", - "icon": "volume_up" + "icon": "volume_up", + "source": audioTab }, { "id": SettingsPanel.Tab.Display, "label": "Display", - "icon": "monitor" + "icon": "monitor", + "source": displayTab }, { "id": SettingsPanel.Tab.Network, "label": "Network", - "icon": "lan" + "icon": "lan", + "source": networkTab }, { "id": SettingsPanel.Tab.TimeWeather, "label": "Time & Weather", - "icon": "schedule" + "icon": "schedule", + "source": timeWeatherTab }, { "id": SettingsPanel.Tab.ColorScheme, "label": "Color Scheme", - "icon": "palette" + "icon": "palette", + "source": colorSchemeTab }, { "id": SettingsPanel.Tab.Wallpaper, "label": "Wallpaper", - "icon": "image" + "icon": "image", + "source": wallpaperTab }, { "id": SettingsPanel.Tab.WallpaperSelector, "label": "Wallpaper Selector", - "icon": "wallpaper_slideshow" + "icon": "wallpaper_slideshow", + "source": wallpaperSelectorTab }, { "id": SettingsPanel.Tab.ScreenRecorder, "label": "Screen Recorder", - "icon": "videocam" + "icon": "videocam", + "source": screenRecorderTab }, { "id": SettingsPanel.Tab.About, "label": "About", - "icon": "info" + "icon": "info", + "source": aboutTab }] Component.onCompleted: { + var initialIndex = 0 + if (root.requestedTab !== null) { + for (var i = 0; i < panel.tabsModel.length; i++) { + if (panel.tabsModel[i].id === root.requestedTab) { + initialIndex = i + break + } + } + } + // Now that the UI is settled, set the current tab index. + panel.currentTabIndex = initialIndex show() } - // Combined visibility change handler onVisibleChanged: { - if (visible) { - // Default to first tab - currentTabIndex = 0 - - // Find the request tab if necessary - if (requestedTab != null) { - for (var i = 0; i < tabsModel.length; i++) { - if (tabsModel[i].id == requestedTab) { - currentTabIndex = i - break - } - } - } - } else if (bgRect.opacityValue > 0) { - // Start hide animation - bgRect.scaleValue = 0.8 - bgRect.opacityValue = 0.0 - - // Hide after animation completes - hideTimer.start() + if (!visible && (bgRect.opacityValue > 0)) { + hide() } } @@ -174,14 +215,12 @@ NLoader { anchors.fill: parent } - // Animation behaviors Behavior on scale { NumberAnimation { duration: Style.animationSlow easing.type: Easing.OutExpo } } - Behavior on opacity { NumberAnimation { duration: Style.animationNormal @@ -194,7 +233,6 @@ NLoader { anchors.margins: Style.marginLarge * scaling spacing: Style.marginLarge * scaling - // Sidebar with tighter spacing Rectangle { id: sidebar Layout.preferredWidth: 260 * scaling @@ -207,29 +245,20 @@ NLoader { Column { anchors.fill: parent anchors.margins: Style.marginSmall * scaling - spacing: Style.marginTiny * 1.5 * scaling // Minimal spacing between tabs + spacing: Style.marginTiny * 1.5 * scaling Repeater { id: sections model: panel.tabsModel - delegate: Rectangle { id: tabItem - width: parent.width - height: 32 * scaling // Back to original height + height: 32 * scaling radius: Style.radiusSmall * scaling color: selected ? Colors.mPrimary : (tabItem.hovering ? Colors.mTertiary : "transparent") - border.color: "transparent" - border.width: 0 - readonly property bool selected: index === currentTabIndex - - // Subtle hover effect: only icon/text color tint on hover property bool hovering: false - property color tabTextColor: selected ? Colors.mOnPrimary : (tabItem.hovering ? Colors.mOnTertiary : Colors.mOnSurface) - RowLayout { anchors.fill: parent anchors.leftMargin: Style.marginSmall * scaling @@ -290,7 +319,7 @@ NLoader { Layout.fillWidth: true spacing: Style.marginSmall * scaling - // Tab label on the main right + // Tab label on the main right side NText { text: panel.tabsModel[currentTabIndex].label font.pointSize: Style.fontSizeLarge * scaling @@ -302,9 +331,7 @@ NLoader { icon: "close" tooltipText: "Close" Layout.alignment: Qt.AlignVCenter - onClicked: { - panel.hide() - } + onClicked: panel.hide() } } @@ -312,23 +339,26 @@ NLoader { Layout.fillWidth: true } - StackLayout { - id: stack + Item { Layout.fillWidth: true Layout.fillHeight: true - currentIndex: currentTabIndex + clip: true - Tabs.GeneralTab {} - Tabs.BarTab {} - Tabs.AudioTab {} - Tabs.DisplayTab {} - Tabs.NetworkTab {} - Tabs.TimeWeatherTab {} - Tabs.ColorSchemeTab {} - Tabs.WallpaperTab {} - Tabs.WallpaperSelectorTab {} - Tabs.ScreenRecorderTab {} - Tabs.AboutTab {} + Repeater { + model: panel.tabsModel + + onItemAdded: function (index, item) { + item.sourceComponent = panel.tabsModel[index].source + } + + delegate: Loader { + // All loaders will occupy the same space, stacked on top of each other. + anchors.fill: parent + visible: index === panel.currentTabIndex + // The loader is only active (and uses memory) when its page is visible. + active: visible + } + } } } } diff --git a/Modules/Settings/Tabs/ScreenRecorderTab.qml b/Modules/Settings/Tabs/ScreenRecorderTab.qml index 2ecce19..eb14a4c 100644 --- a/Modules/Settings/Tabs/ScreenRecorderTab.qml +++ b/Modules/Settings/Tabs/ScreenRecorderTab.qml @@ -257,7 +257,6 @@ ColumnLayout { name: "AAC" } } - currentKey: Settings.data.screenRecorder.audioCodec onSelected: function (key) { Settings.data.screenRecorder.audioCodec = key From f0ef9ac7b0d19ed9f0429f654d8efeabba2c6c7a Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 09:53:28 -0400 Subject: [PATCH 303/394] MediaPlayer: more robust display --- Modules/SidePanel/Cards/MediaCard.qml | 10 +++++----- Services/MediaPlayer.qml | 7 +++++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Modules/SidePanel/Cards/MediaCard.qml b/Modules/SidePanel/Cards/MediaCard.qml index d1d7312..b7c1a9b 100644 --- a/Modules/SidePanel/Cards/MediaCard.qml +++ b/Modules/SidePanel/Cards/MediaCard.qml @@ -58,7 +58,7 @@ NBox { ColumnLayout { id: main - visible: MediaPlayer.currentPlayer + visible: MediaPlayer.currentPlayer && MediaPlayer.canPlay spacing: Style.marginMedium * scaling // Player selector @@ -229,12 +229,14 @@ NBox { // Progress bar Rectangle { id: progressBarBackground + visible: (MediaPlayer.currentPlayer && MediaPlayer.trackLength > 0) width: parent.width height: 4 * scaling radius: Style.radiusSmall * scaling - color: Colors.mSurfaceVariant + color: Colors.mSurface Layout.fillWidth: true + property real progressRatio: { if (!MediaPlayer.currentPlayer || !MediaPlayer.isPlaying || MediaPlayer.trackLength <= 0) { return 0 @@ -259,17 +261,15 @@ NBox { // Interactive progress handle Rectangle { id: progressHandle + visible: (MediaPlayer.currentPlayer && MediaPlayer.trackLength > 0) width: 16 * scaling height: 16 * scaling radius: width * 0.5 color: Colors.mPrimary border.color: Colors.mSurface border.width: Math.max(1 * Style.borderMedium * scaling) - x: Math.max(0, Math.min(parent.width - width, progressFill.width - width / 2)) anchors.verticalCenter: parent.verticalCenter - - visible: MediaPlayer.trackLength > 0 scale: progressMouseArea.containsMouse || progressMouseArea.pressed ? 1.2 : 1.0 Behavior on scale { diff --git a/Services/MediaPlayer.qml b/Services/MediaPlayer.qml index f809c1c..92b1880 100644 --- a/Services/MediaPlayer.qml +++ b/Services/MediaPlayer.qml @@ -16,13 +16,13 @@ Singleton { property string trackArtist: currentPlayer ? (currentPlayer.trackArtist || "") : "" property string trackAlbum: currentPlayer ? (currentPlayer.trackAlbum || "") : "" property string trackArtUrl: currentPlayer ? (currentPlayer.trackArtUrl || "") : "" - property real trackLength: currentPlayer ? currentPlayer.length : 0 + property real trackLength: currentPlayer ? ((currentPlayer.length < infiniteTrackLength) ? currentPlayer.length : 0) : 0 property bool canPlay: currentPlayer ? currentPlayer.canPlay : false property bool canPause: currentPlayer ? currentPlayer.canPause : false property bool canGoNext: currentPlayer ? currentPlayer.canGoNext : false property bool canGoPrevious: currentPlayer ? currentPlayer.canGoPrevious : false property bool canSeek: currentPlayer ? currentPlayer.canSeek : false - property bool hasPlayer: getAvailablePlayers().length > 0 + property real infiniteTrackLength: 922337203685 Component.onCompleted: { updateCurrentPlayer() @@ -49,6 +49,7 @@ Singleton { function findActivePlayer() { let availablePlayers = getAvailablePlayers() if (availablePlayers.length === 0) { + console.log("[MediaPlayer] No active player found") return null } @@ -66,6 +67,7 @@ Singleton { if (newPlayer !== currentPlayer) { currentPlayer = newPlayer currentPosition = currentPlayer ? currentPlayer.position : 0 + console.log("[MediaPlayer] Switching player") } } @@ -146,6 +148,7 @@ Singleton { Connections { target: Mpris.players function onValuesChanged() { + console.log("[MediaPlayer] Players changed") updateCurrentPlayer() } } From 82e42ad5c220f9c3ea91520e2c2ab71628adcff2 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 10:06:49 -0400 Subject: [PATCH 304/394] formatting --- Modules/SidePanel/Cards/MediaCard.qml | 1 - Services/MediaPlayer.qml | 2 +- Services/Network.qml | 2 -- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/Modules/SidePanel/Cards/MediaCard.qml b/Modules/SidePanel/Cards/MediaCard.qml index b7c1a9b..c0eb9fc 100644 --- a/Modules/SidePanel/Cards/MediaCard.qml +++ b/Modules/SidePanel/Cards/MediaCard.qml @@ -236,7 +236,6 @@ NBox { color: Colors.mSurface Layout.fillWidth: true - property real progressRatio: { if (!MediaPlayer.currentPlayer || !MediaPlayer.isPlaying || MediaPlayer.trackLength <= 0) { return 0 diff --git a/Services/MediaPlayer.qml b/Services/MediaPlayer.qml index 92b1880..4617145 100644 --- a/Services/MediaPlayer.qml +++ b/Services/MediaPlayer.qml @@ -16,7 +16,7 @@ Singleton { property string trackArtist: currentPlayer ? (currentPlayer.trackArtist || "") : "" property string trackAlbum: currentPlayer ? (currentPlayer.trackAlbum || "") : "" property string trackArtUrl: currentPlayer ? (currentPlayer.trackArtUrl || "") : "" - property real trackLength: currentPlayer ? ((currentPlayer.length < infiniteTrackLength) ? currentPlayer.length : 0) : 0 + property real trackLength: currentPlayer ? ((currentPlayer.length < infiniteTrackLength) ? currentPlayer.length : 0) : 0 property bool canPlay: currentPlayer ? currentPlayer.canPlay : false property bool canPause: currentPlayer ? currentPlayer.canPause : false property bool canGoNext: currentPlayer ? currentPlayer.canGoNext : false diff --git a/Services/Network.qml b/Services/Network.qml index f064790..92527b8 100644 --- a/Services/Network.qml +++ b/Services/Network.qml @@ -214,8 +214,6 @@ QtObject { scanProcess.existingNetwork = {} } } - - } property Process connectProcess: Process { From 04f12876908148e851cfa6dd306051b5942160dc Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Fri, 15 Aug 2025 16:13:14 +0200 Subject: [PATCH 305/394] More wifi fixes --- Modules/Bar/WiFiMenu.qml | 469 +++++++++++++++++++++++---------------- Services/Network.qml | 142 +++++++++++- 2 files changed, 405 insertions(+), 206 deletions(-) diff --git a/Modules/Bar/WiFiMenu.qml b/Modules/Bar/WiFiMenu.qml index 21d5389..3ba335d 100644 --- a/Modules/Bar/WiFiMenu.qml +++ b/Modules/Bar/WiFiMenu.qml @@ -41,7 +41,9 @@ NLoader { // Also handle visibility changes from external sources onVisibleChanged: { - if (!visible && wifiMenuRect.opacityValue > 0) { + if (visible && Settings.data.network.wifiEnabled) { + network.refreshNetworks() + } else if (wifiMenuRect.opacityValue > 0) { // Start hide animation wifiMenuRect.scaleValue = 0.8 wifiMenuRect.opacityValue = 0.0 @@ -65,6 +67,22 @@ NLoader { WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + Network { + id: network + } + + // Timer to refresh networks when WiFi is enabled while menu is open + Timer { + id: wifiEnableRefreshTimer + interval: 3000 // Wait 3 seconds for WiFi to be fully ready + repeat: false + onTriggered: { + if (Settings.data.network.wifiEnabled && wifiPanel.visible) { + network.refreshNetworks() + } + } + } + Rectangle { id: wifiMenuRect color: Colors.mSurface @@ -135,14 +153,19 @@ NLoader { value: Settings.data.network.wifiEnabled onToggled: function (value) { Settings.data.network.wifiEnabled = value - // TBC: This should be done in a service - Quickshell.execDetached(["nmcli", "radio", "wifi", Settings.data.network.wifiEnabled ? "on" : "off"]) + network.setWifiEnabled(value) + + // If enabling WiFi while menu is open, refresh after a delay + if (value) { + wifiEnableRefreshTimer.start() + } } } NIconButton { icon: "refresh" sizeMultiplier: 0.8 + enabled: Settings.data.network.wifiEnabled && !network.isLoading onClicked: { network.refreshNetworks() } @@ -159,226 +182,282 @@ NLoader { NDivider {} - ListView { - id: networkList + Item { Layout.fillWidth: true Layout.fillHeight: true - model: Object.values(network.networks) - spacing: Style.marginMedium * scaling - clip: true - delegate: Item { - width: parent.width - height: modelData.ssid === passwordPromptSsid - && showPasswordPrompt ? 108 * scaling : Style.baseWidgetSize * 1.5 * scaling + // Loading indicator + ColumnLayout { + anchors.centerIn: parent + visible: Settings.data.network.wifiEnabled && network.isLoading + spacing: Style.marginMedium * scaling - ColumnLayout { - anchors.fill: parent - spacing: 0 + NBusyIndicator { + running: network.isLoading + color: Colors.mPrimary + size: Style.baseWidgetSize * scaling + Layout.alignment: Qt.AlignHCenter + } - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: Style.baseWidgetSize * 1.5 * scaling - radius: Style.radiusMedium * scaling - color: modelData.connected ? Colors.mPrimary : (networkMouseArea.containsMouse ? Colors.mTertiary : "transparent") + NText { + text: "Scanning for networks..." + font.pointSize: Style.fontSizeNormal * scaling + color: Colors.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + } - RowLayout { - anchors.fill: parent - anchors.margins: Style.marginSmall * scaling - spacing: Style.marginSmall * scaling + // WiFi disabled message + ColumnLayout { + anchors.centerIn: parent + visible: !Settings.data.network.wifiEnabled + spacing: Style.marginMedium * scaling - NText { - text: network.signalIcon(modelData.signal) - font.family: "Material Symbols Outlined" - font.pointSize: Style.fontSizeXL * scaling - color: modelData.connected ? Colors.mSurface : (networkMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurface) - } + NText { + text: "wifi_off" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXXL * scaling + color: Colors.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } - ColumnLayout { - Layout.fillWidth: true - spacing: Style.marginTiny * scaling + NText { + text: "WiFi is disabled" + font.pointSize: Style.fontSizeLarge * scaling + color: Colors.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "Enable WiFi to see available networks" + font.pointSize: Style.fontSizeNormal * scaling + color: Colors.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + } + + // Network list + ListView { + id: networkList + anchors.fill: parent + visible: Settings.data.network.wifiEnabled && !network.isLoading + model: Object.values(network.networks) + spacing: Style.marginMedium * scaling + clip: true + + delegate: Item { + width: parent.width + height: modelData.ssid === passwordPromptSsid + && showPasswordPrompt ? 108 * scaling : Style.baseWidgetSize * 1.5 * scaling + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: Style.baseWidgetSize * 1.5 * scaling + radius: Style.radiusMedium * scaling + color: modelData.connected ? Colors.mPrimary : (networkMouseArea.containsMouse ? Colors.mTertiary : "transparent") + + RowLayout { + anchors.fill: parent + anchors.margins: Style.marginSmall * scaling + spacing: Style.marginSmall * scaling - // SSID NText { - text: modelData.ssid || "Unknown Network" - font.pointSize: Style.fontSizeNormal * scaling - elide: Text.ElideRight - Layout.fillWidth: true + text: network.signalIcon(modelData.signal) + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXL * scaling color: modelData.connected ? Colors.mSurface : (networkMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurface) } - // Security Protocol - NText { - text: modelData.security && modelData.security !== "--" ? modelData.security : "Open" - font.pointSize: Style.fontSizeTiny * scaling - elide: Text.ElideRight + ColumnLayout { Layout.fillWidth: true + spacing: Style.marginTiny * scaling + + // SSID + NText { + text: modelData.ssid || "Unknown Network" + font.pointSize: Style.fontSizeNormal * scaling + elide: Text.ElideRight + Layout.fillWidth: true + color: modelData.connected ? Colors.mSurface : (networkMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurface) + } + + // Security Protocol + NText { + text: modelData.security && modelData.security !== "--" ? modelData.security : "Open" + font.pointSize: Style.fontSizeTiny * scaling + elide: Text.ElideRight + Layout.fillWidth: true + color: modelData.connected ? Colors.mSurface : (networkMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurface) + } + + NText { + visible: network.connectStatusSsid === modelData.ssid && network.connectStatus === "error" + && network.connectError.length > 0 + text: network.connectError + color: Colors.mError + font.pointSize: Style.fontSizeSmall * scaling + elide: Text.ElideRight + Layout.fillWidth: true + } + } + + Item { + Layout.preferredWidth: 22 + Layout.preferredHeight: 22 + visible: network.connectStatusSsid === modelData.ssid + && (network.connectStatus !== "" || network.connectingSsid === modelData.ssid) + + NBusyIndicator { + visible: network.connectingSsid === modelData.ssid + running: network.connectingSsid === modelData.ssid + color: Colors.mPrimary + anchors.centerIn: parent + size: Style.baseWidgetSize * 0.7 * scaling + } + + // TBC: Does nothing on my setup + NText { + visible: network.connectStatus === "success" && !network.connectingSsid + text: "check_circle" + font.family: "Material Symbols Outlined" + font.pointSize: 18 * scaling + color: "#43a047" // TBC: No! + anchors.centerIn: parent + } + + // TBC: Does nothing on my setup + NText { + visible: network.connectStatus === "error" && !network.connectingSsid + text: "error" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeSmall * scaling + color: Colors.mError + anchors.centerIn: parent + } + } + + NText { + visible: modelData.connected + text: "connected" + font.pointSize: Style.fontSizeSmall * scaling color: modelData.connected ? Colors.mSurface : (networkMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurface) } + } - NText { - visible: network.connectStatusSsid === modelData.ssid && network.connectStatus === "error" - && network.connectError.length > 0 - text: network.connectError - color: Colors.mError - font.pointSize: Style.fontSizeSmall * scaling - elide: Text.ElideRight + MouseArea { + id: networkMouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + if (modelData.connected) { + network.disconnectNetwork(modelData.ssid) + } else if (network.isSecured(modelData.security) && !modelData.existing) { + passwordPromptSsid = modelData.ssid + showPasswordPrompt = true + passwordInput = "" // Clear previous input + Qt.callLater(function () { + passwordInputField.forceActiveFocus() + }) + } else { + network.connectNetwork(modelData.ssid, modelData.security) + } + } + } + } + + // Password prompt section + Rectangle { + id: passwordPromptSection + Layout.fillWidth: true + Layout.preferredHeight: modelData.ssid === passwordPromptSsid && showPasswordPrompt ? 60 : 0 + Layout.margins: 8 + visible: modelData.ssid === passwordPromptSsid && showPasswordPrompt + color: Colors.mSurfaceVariant + radius: Style.radiusSmall * scaling + + RowLayout { + anchors.fill: parent + anchors.margins: Style.marginSmall * scaling + spacing: Style.marginSmall * scaling + + Item { Layout.fillWidth: true + Layout.preferredHeight: 36 + + Rectangle { + anchors.fill: parent + radius: 8 + color: "transparent" + border.color: passwordInputField.activeFocus ? Colors.mPrimary : Colors.mOutline + border.width: 1 + + TextInput { + id: passwordInputField + anchors.fill: parent + anchors.margins: Style.marginMedium * scaling + text: passwordInput + font.pointSize: Style.fontSizeMedium * scaling + color: Colors.mOnSurface + verticalAlignment: TextInput.AlignVCenter + clip: true + focus: true + selectByMouse: true + activeFocusOnTab: true + inputMethodHints: Qt.ImhNone + echoMode: TextInput.Password + onTextChanged: passwordInput = text + onAccepted: { + network.submitPassword(passwordPromptSsid, passwordInput) + showPasswordPrompt = false + } + + MouseArea { + id: passwordInputMouseArea + anchors.fill: parent + onClicked: passwordInputField.forceActiveFocus() + } + } + } } - } - - Item { - Layout.preferredWidth: 22 - Layout.preferredHeight: 22 - visible: network.connectStatusSsid === modelData.ssid - && (network.connectStatus !== "" || network.connectingSsid === modelData.ssid) - - NBusyIndicator { - visible: network.connectingSsid === modelData.ssid - running: network.connectingSsid === modelData.ssid - color: Colors.mPrimary - anchors.centerIn: parent - size: Style.baseWidgetSize * 0.7 * scaling - } - - // TBC: Does nothing on my setup - NText { - visible: network.connectStatus === "success" && !network.connectingSsid - text: "check_circle" - font.family: "Material Symbols Outlined" - font.pointSize: 18 * scaling - color: "#43a047" // TBC: No! - anchors.centerIn: parent - } - - // TBC: Does nothing on my setup - NText { - visible: network.connectStatus === "error" && !network.connectingSsid - text: "error" - font.family: "Material Symbols Outlined" - font.pointSize: Style.fontSizeSmall * scaling - color: Colors.mError - anchors.centerIn: parent - } - } - - NText { - visible: modelData.connected - text: "connected" - font.pointSize: Style.fontSizeSmall * scaling - color: modelData.connected ? Colors.mSurface : (networkMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurface) - } - } - - MouseArea { - id: networkMouseArea - anchors.fill: parent - hoverEnabled: true - onClicked: { - if (modelData.connected) { - network.disconnectNetwork(modelData.ssid) - } else if (network.isSecured(modelData.security) && !modelData.existing) { - passwordPromptSsid = modelData.ssid - showPasswordPrompt = true - passwordInput = "" // Clear previous input - Qt.callLater(function () { - passwordInputField.forceActiveFocus() - }) - } else { - network.connectNetwork(modelData.ssid, modelData.security) - } - } - } - } - - // Password prompt section - Rectangle { - id: passwordPromptSection - Layout.fillWidth: true - Layout.preferredHeight: modelData.ssid === passwordPromptSsid && showPasswordPrompt ? 60 : 0 - Layout.margins: 8 - visible: modelData.ssid === passwordPromptSsid && showPasswordPrompt - color: Colors.mSurfaceVariant - radius: Style.radiusSmall * scaling - - RowLayout { - anchors.fill: parent - anchors.margins: Style.marginSmall * scaling - spacing: Style.marginSmall * scaling - - Item { - Layout.fillWidth: true - Layout.preferredHeight: 36 Rectangle { - anchors.fill: parent - radius: 8 - color: "transparent" - border.color: passwordInputField.activeFocus ? Colors.mPrimary : Colors.mOutline - border.width: 1 + Layout.preferredWidth: 80 + Layout.preferredHeight: 36 + radius: Style.radiusMedium * scaling + color: Colors.mPrimary + border.color: Colors.mPrimary + border.width: 0 - TextInput { - id: passwordInputField + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + + NText { + anchors.centerIn: parent + text: "Connect" + color: Colors.mSurface + font.pointSize: Style.fontSizeSmall * scaling + } + + MouseArea { anchors.fill: parent - anchors.margins: Style.marginMedium * scaling - text: passwordInput - font.pointSize: Style.fontSizeMedium * scaling - color: Colors.mOnSurface - verticalAlignment: TextInput.AlignVCenter - clip: true - focus: true - selectByMouse: true - activeFocusOnTab: true - inputMethodHints: Qt.ImhNone - echoMode: TextInput.Password - onTextChanged: passwordInput = text - onAccepted: { + onClicked: { network.submitPassword(passwordPromptSsid, passwordInput) showPasswordPrompt = false } - - MouseArea { - id: passwordInputMouseArea - anchors.fill: parent - onClicked: passwordInputField.forceActiveFocus() - } + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onEntered: parent.color = Qt.darker(Colors.mPrimary, 1.1) + onExited: parent.color = Colors.mPrimary } } } - - Rectangle { - Layout.preferredWidth: 80 - Layout.preferredHeight: 36 - radius: Style.radiusMedium * scaling - color: Colors.mPrimary - border.color: Colors.mPrimary - border.width: 0 - - Behavior on color { - ColorAnimation { - duration: Style.animationFast - } - } - - NText { - anchors.centerIn: parent - text: "Connect" - color: Colors.mSurface - font.pointSize: Style.fontSizeSmall * scaling - } - - MouseArea { - anchors.fill: parent - onClicked: { - network.submitPassword(passwordPromptSsid, passwordInput) - showPasswordPrompt = false - } - cursorShape: Qt.PointingHandCursor - hoverEnabled: true - onEntered: parent.color = Qt.darker(Colors.mPrimary, 1.1) - onExited: parent.color = Colors.mPrimary - } - } } } } @@ -388,4 +467,4 @@ NLoader { } } } -} +} \ No newline at end of file diff --git a/Services/Network.qml b/Services/Network.qml index f064790..bd8c70f 100644 --- a/Services/Network.qml +++ b/Services/Network.qml @@ -1,4 +1,5 @@ import QtQuick +import Quickshell import Quickshell.Io QtObject { @@ -10,6 +11,8 @@ QtObject { property string connectStatusSsid: "" property string connectError: "" property string detectedInterface: "" + property string lastConnectedNetwork: "" + property bool isLoading: false function signalIcon(signal) { if (signal >= 80) @@ -28,9 +31,29 @@ QtObject { } function refreshNetworks() { + isLoading = true existingNetwork.running = true } + function setWifiEnabled(enabled) { + if (enabled) { + // Enable WiFi radio + isLoading = true + enableWifiProcess.running = true + } else { + // Store the currently connected network before disabling + for (const ssid in networks) { + if (networks[ssid].connected) { + lastConnectedNetwork = ssid + break + } + } + + // Disable WiFi radio + disableWifiProcess.running = true + } + } + function connectNetwork(ssid, security) { pendingConnect = { "ssid": ssid, @@ -87,7 +110,7 @@ QtObject { property int refreshInterval: 25000 - // Only refresh when we have an active connection + // Only refresh when we have an active connection and WiFi is enabled property bool hasActiveConnection: { for (const net in networks) { if (networks[net].connected) { @@ -99,18 +122,111 @@ QtObject { property Timer refreshTimer: Timer { interval: root.refreshInterval - // Only run timer when we're connected to a network - running: root.hasActiveConnection + // Only run timer when we're connected to a network and WiFi is enabled + running: root.hasActiveConnection && Settings.data.network.wifiEnabled repeat: true onTriggered: root.refreshNetworks() } // Force a refresh when menu is opened function onMenuOpened() { - refreshNetworks() + if (Settings.data.network.wifiEnabled) { + refreshNetworks() + } } - function onMenuClosed() {// No need to do anything special on close + function onMenuClosed() { + // No need to do anything special on close + } + + // Process to enable WiFi radio + property Process enableWifiProcess: Process { + id: enableWifiProcess + running: false + command: ["nmcli", "radio", "wifi", "on"] + onRunningChanged: { + if (!running) { + // Wait a moment for the radio to be enabled, then refresh networks + enableWifiDelayTimer.start() + } + } + stderr: StdioCollector { + onStreamFinished: { + if (text.trim() !== "") { + console.warn("Error enabling WiFi:", text) + } + } + } + } + + // Timer to delay network refresh after enabling WiFi + property Timer enableWifiDelayTimer: Timer { + id: enableWifiDelayTimer + interval: 2000 // Wait 2 seconds for radio to be ready + repeat: false + onTriggered: { + // Force refresh networks multiple times to ensure UI updates + root.refreshNetworks() + + // Try to auto-reconnect to the last connected network if it exists + if (lastConnectedNetwork) { + autoReconnectTimer.start() + } + + // Set up additional refresh to ensure UI is populated + postEnableRefreshTimer.start() + } + } + + // Additional timer to ensure networks are populated after enabling + property Timer postEnableRefreshTimer: Timer { + id: postEnableRefreshTimer + interval: 1000 + repeat: false + onTriggered: { + root.refreshNetworks() + } + } + + // Timer to attempt auto-reconnection to the last connected network + property Timer autoReconnectTimer: Timer { + id: autoReconnectTimer + interval: 3000 // Wait 3 seconds after scan for networks to be available + repeat: false + onTriggered: { + if (lastConnectedNetwork && networks[lastConnectedNetwork]) { + const network = networks[lastConnectedNetwork] + if (network.existing && !network.connected) { + upConnectionProcess.profileName = lastConnectedNetwork + upConnectionProcess.running = true + } + } + } + } + + // Process to disable WiFi radio + property Process disableWifiProcess: Process { + id: disableWifiProcess + running: false + command: ["nmcli", "radio", "wifi", "off"] + onRunningChanged: { + if (!running) { + // Clear networks when WiFi is disabled + root.networks = ({}) + root.connectingSsid = "" + root.connectStatus = "" + root.connectStatusSsid = "" + root.connectError = "" + root.isLoading = false + } + } + stderr: StdioCollector { + onStreamFinished: { + if (text.trim() !== "") { + console.warn("Error disabling WiFi:", text) + } + } + } } property Process disconnectProfileProcess: Process { @@ -211,11 +327,10 @@ QtObject { } root.networks = networksMap + root.isLoading = false scanProcess.existingNetwork = {} } } - - } property Process connectProcess: Process { @@ -237,6 +352,7 @@ QtObject { root.connectStatus = "success" root.connectStatusSsid = connectProcess.ssid root.connectError = "" + root.lastConnectedNetwork = connectProcess.ssid root.refreshNetworks() } } @@ -324,8 +440,9 @@ QtObject { onStreamFinished: { root.connectingSsid = "" root.connectStatus = "success" - root.connectStatusSsid = root.pendingConnect ? root.pendingConnect.ssid : "" + root.connectStatusSsid = root.pendingConnect ? root.pendingConnect.ssid : profileName root.connectError = "" + root.lastConnectedNetwork = profileName root.pendingConnect = null root.refreshNetworks() } @@ -334,7 +451,7 @@ QtObject { onStreamFinished: { root.connectingSsid = "" root.connectStatus = "error" - root.connectStatusSsid = root.pendingConnect ? root.pendingConnect.ssid : "" + root.connectStatusSsid = root.pendingConnect ? root.pendingConnect.ssid : profileName root.connectError = text root.pendingConnect = null } @@ -342,6 +459,9 @@ QtObject { } Component.onCompleted: { - refreshNetworks() + // Only refresh networks if WiFi is enabled + if (Settings.data.network.wifiEnabled) { + refreshNetworks() + } } -} +} \ No newline at end of file From af20c7b9eb4b6226a05792a8b086329d91d7569e Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 10:23:16 -0400 Subject: [PATCH 306/394] Dock: Settings - Removed master toggle again, as we have it per display - moved autoHide setting into the dock section - conditional dock per display working --- Modules/Bar/Bar.qml | 1 + Modules/Dock/Dock.qml | 9 ++++++--- Modules/Settings/Tabs/GeneralTab.qml | 13 ++----------- Services/Settings.qml | 5 ++--- 4 files changed, 11 insertions(+), 17 deletions(-) diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index a7db3ab..420b67e 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -18,6 +18,7 @@ Variants { screen: modelData implicitHeight: Style.barHeight * scaling color: "transparent" + visible: modelData ? (Settings.data.bar.monitors.includes(modelData.name) || (Settings.data.bar.monitors.length === 0)) : false diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml index dc10c9f..bb0f9c8 100644 --- a/Modules/Dock/Dock.qml +++ b/Modules/Dock/Dock.qml @@ -9,7 +9,7 @@ import qs.Services import qs.Widgets NLoader { - isLoaded: Settings.data.general.showDock + isLoaded: (Settings.data.dock.monitors.length > 0) content: Component { Variants { model: Quickshell.screens @@ -19,7 +19,7 @@ NLoader { readonly property real scaling: Scaling.scale(modelData) // Auto-hide properties - property bool autoHide: Settings.data.general.dockAutoHide + property bool autoHide: Settings.data.dock.autoHide property bool hidden: autoHide // Start hidden only if auto-hide is enabled property int hideDelay: 500 property int showDelay: 100 @@ -39,7 +39,10 @@ NLoader { PanelWindow { id: dockWindow - visible: true + + // Dock works differently from bar, it is show only if toggled in Settings/Display + visible: modelData ? Settings.data.dock.monitors.includes(modelData.name) : false + screen: modelData exclusionMode: ExclusionMode.Ignore anchors.bottom: true diff --git a/Modules/Settings/Tabs/GeneralTab.qml b/Modules/Settings/Tabs/GeneralTab.qml index c161442..b3bf2a9 100644 --- a/Modules/Settings/Tabs/GeneralTab.qml +++ b/Modules/Settings/Tabs/GeneralTab.qml @@ -109,21 +109,12 @@ ColumnLayout { } } - NToggle { - label: "Show Dock" - description: "Enable the dock at the bottom of the screen" - value: Settings.data.general.showDock - onToggled: function (v) { - Settings.data.general.showDock = v - } - } - NToggle { label: "Auto-hide Dock" description: "Automatically hide the dock when not in use" - value: Settings.data.general.dockAutoHide + value: Settings.data.dock.autoHide onToggled: function (v) { - Settings.data.general.dockAutoHide = v + Settings.data.dock.autoHide = v } } } diff --git a/Services/Settings.qml b/Services/Settings.qml index 100ac64..5295302 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -24,7 +24,7 @@ Singleton { // Used to access via Settings.data.xxx.yyy property var data: adapter - + // Flag to prevent unnecessary wallpaper calls during reloads property bool isInitialLoad: true @@ -86,8 +86,6 @@ Singleton { property string avatarImage: defaultAvatar property bool dimDesktop: true property bool showScreenCorners: false - property bool showDock: false - property bool dockAutoHide: false } // location @@ -149,6 +147,7 @@ Singleton { property JsonObject dock dock: JsonObject { + property bool autoHide: false property bool exclusive: false property list monitors: [] } From 838962e448cc54c9892e740e877c6ea37369e97e Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 10:23:40 -0400 Subject: [PATCH 307/394] Formatting: Brightness, wifi, network --- Modules/Bar/Brightness.qml | 23 ++++---- Modules/Bar/WiFiMenu.qml | 4 +- Services/BrightnessService.qml | 97 +++++++++++++++++----------------- Services/Network.qml | 11 ++-- 4 files changed, 66 insertions(+), 69 deletions(-) diff --git a/Modules/Bar/Brightness.qml b/Modules/Bar/Brightness.qml index aa754b7..486611e 100644 --- a/Modules/Bar/Brightness.qml +++ b/Modules/Bar/Brightness.qml @@ -19,9 +19,7 @@ Item { return "brightness_auto" } var brightness = BrightnessService.brightness - return brightness <= 0 ? "brightness_1" : - brightness < 33 ? "brightness_low" : - brightness < 66 ? "brightness_medium" : "brightness_high" + return brightness <= 0 ? "brightness_1" : brightness < 33 ? "brightness_low" : brightness < 66 ? "brightness_medium" : "brightness_high" } // Connection used to open the pill when brightness changes @@ -29,25 +27,23 @@ Item { target: BrightnessService.focusedMonitor function onBrightnessUpdated() { var currentBrightness = BrightnessService.brightness - + // Ignore if this is the first time or if brightness hasn't actually changed if (!firstBrightnessReceived) { firstBrightnessReceived = true lastBrightness = currentBrightness return } - + // Only show pill if brightness actually changed (not just loaded from settings) if (Math.abs(currentBrightness - lastBrightness) > 0.1) { pill.show() } - + lastBrightness = currentBrightness } } - - NPill { id: pill icon: getIcon() @@ -55,11 +51,14 @@ Item { collapsedIconColor: Colors.mOnSurface autoHide: true text: Math.round(BrightnessService.brightness) + "%" - tooltipText: "Brightness: " + Math.round(BrightnessService.brightness) + "%\nMethod: " + BrightnessService.currentMethod + "\nLeft click for advanced settings.\nScroll up/down to change brightness." + tooltipText: "Brightness: " + Math.round( + BrightnessService.brightness) + "%\nMethod: " + BrightnessService.currentMethod + + "\nLeft click for advanced settings.\nScroll up/down to change brightness." onWheel: function (angle) { - if (!BrightnessService.available) return - + if (!BrightnessService.available) + return + if (angle > 0) { BrightnessService.increaseBrightness() } else if (angle < 0) { @@ -71,4 +70,4 @@ Item { settingsPanel.isLoaded = true } } -} \ No newline at end of file +} diff --git a/Modules/Bar/WiFiMenu.qml b/Modules/Bar/WiFiMenu.qml index 3ba335d..58754f3 100644 --- a/Modules/Bar/WiFiMenu.qml +++ b/Modules/Bar/WiFiMenu.qml @@ -154,7 +154,7 @@ NLoader { onToggled: function (value) { Settings.data.network.wifiEnabled = value network.setWifiEnabled(value) - + // If enabling WiFi while menu is open, refresh after a delay if (value) { wifiEnableRefreshTimer.start() @@ -467,4 +467,4 @@ NLoader { } } } -} \ No newline at end of file +} diff --git a/Services/BrightnessService.qml b/Services/BrightnessService.qml index dc7104b..72f5e4f 100644 --- a/Services/BrightnessService.qml +++ b/Services/BrightnessService.qml @@ -1,5 +1,6 @@ pragma Singleton -pragma ComponentBehavior: Bound + +pragma ComponentBehavior import QtQuick import Quickshell @@ -17,15 +18,16 @@ Singleton { readonly property bool available: focusedMonitor !== null readonly property string currentMethod: focusedMonitor ? focusedMonitor.method : Settings.data.brightness.lastMethod readonly property var detectedDisplays: monitors.map(m => ({ - name: m.modelData.name, - type: m.isDdc ? "external" : "internal", - method: m.method, - index: m.busNum - })) + "name": m.modelData.name, + "type": m.isDdc ? "external" : "internal", + "method": m.method, + "index": m.busNum + })) // Get the currently focused monitor readonly property Monitor focusedMonitor: { - if (monitors.length === 0) return null + if (monitors.length === 0) + return null // For now, return the first monitor. Could be enhanced to detect focused monitor return monitors[0] } @@ -61,8 +63,7 @@ Singleton { } // Backward compatibility functions - function updateBrightness(): void { - // No longer needed with the new architecture + function updateBrightness(): void {// No longer needed with the new architecture } function setDisplay(displayIndex: int): bool { @@ -72,18 +73,21 @@ Singleton { function getDisplayInfo(): var { return focusedMonitor ? { - name: focusedMonitor.modelData.name, - type: focusedMonitor.isDdc ? "external" : "internal", - method: focusedMonitor.method, - index: focusedMonitor.busNum - } : null + "name": focusedMonitor.modelData.name, + "type": focusedMonitor.isDdc ? "external" : "internal", + "method": focusedMonitor.method, + "index": focusedMonitor.busNum + } : null } function getAvailableMethods(): list { var methods = [] - if (monitors.some(m => m.isDdc)) methods.push("ddcutil") - if (monitors.some(m => !m.isDdc)) methods.push("internal") - if (appleDisplayPresent) methods.push("apple") + if (monitors.some(m => m.isDdc)) + methods.push("ddcutil") + if (monitors.some(m => !m.isDdc)) + methods.push("internal") + if (appleDisplayPresent) + methods.push("apple") return methods } @@ -121,19 +125,17 @@ Singleton { onStreamFinished: { var displays = text.trim().split("\n\n").filter(d => d.startsWith("Display ")) root.ddcMonitors = displays.map(d => { - var modelMatch = d.match(/Monitor:.*:(.*):.*/) - var busMatch = d.match(/I2C bus:[ ]*\/dev\/i2c-([0-9]+)/) - return { - model: modelMatch ? modelMatch[1] : "", - busNum: busMatch ? busMatch[1] : "" - } - }) + var modelMatch = d.match(/Monitor:.*:(.*):.*/) + var busMatch = d.match(/I2C bus:[ ]*\/dev\/i2c-([0-9]+)/) + return { + "model": modelMatch ? modelMatch[1] : "", + "busNum": busMatch ? busMatch[1] : "" + } + }) } } } - - component Monitor: QtObject { id: monitor @@ -142,19 +144,19 @@ Singleton { readonly property string busNum: root.ddcMonitors.find(m => m.model === modelData.model)?.busNum ?? "" readonly property bool isAppleDisplay: root.appleDisplayPresent && modelData.model.startsWith("StudioDisplay") readonly property string method: isAppleDisplay ? "apple" : (isDdc ? "ddcutil" : "internal") - + property real brightness: getStoredBrightness() property real queuedBrightness: NaN // Signal for brightness changes signal brightnessUpdated(real newBrightness) - // Initialize brightness + // Initialize brightness readonly property Process initProc: Process { stdout: StdioCollector { onStreamFinished: { console.log("[BrightnessService] Raw brightness data for", monitor.modelData.name + ":", text.trim()) - + if (monitor.isAppleDisplay) { var val = parseInt(text.trim()) if (!isNaN(val)) { @@ -183,7 +185,7 @@ Singleton { } } } - + if (monitor.brightness > 0) { // Save the detected brightness to settings monitor.saveBrightness(monitor.brightness) @@ -204,9 +206,7 @@ Singleton { } } - - - function getStoredBrightness(): real { + function getStoredBrightness(): real { // Try to get stored brightness for this specific monitor var stored = Settings.data.brightness.monitorBrightness.find(m => m.name === modelData.name) if (stored) { @@ -218,19 +218,19 @@ Singleton { function saveBrightness(value: real): void { var brightnessPercent = Math.round(value * 100) - + // Update general last brightness Settings.data.brightness.lastBrightness = brightnessPercent Settings.data.brightness.lastMethod = method - + // Update monitor-specific brightness var monitorIndex = Settings.data.brightness.monitorBrightness.findIndex(m => m.name === modelData.name) var monitorData = { - name: modelData.name, - brightness: brightnessPercent, - method: method + "name": modelData.name, + "brightness": brightnessPercent, + "method": method } - + if (monitorIndex >= 0) { Settings.data.brightness.monitorBrightness[monitorIndex] = monitorData } else { @@ -241,8 +241,9 @@ Singleton { function setBrightness(value: real): void { value = Math.max(0, Math.min(1, value)) var rounded = Math.round(value * 100) - - if (Math.round(brightness * 100) === rounded) return + + if (Math.round(brightness * 100) === rounded) + return if (isDdc && timer.running) { queuedBrightness = value @@ -251,7 +252,7 @@ Singleton { brightness = value brightnessUpdated(brightness) - + // Save to settings saveBrightness(value) @@ -278,16 +279,14 @@ Singleton { initProc.command = ["asdbctl", "get"] } else if (isDdc) { initProc.command = ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"] - } else { - // Internal backlight - try to find the first available backlight device - initProc.command = ["sh", "-c", "for dev in /sys/class/backlight/*; do if [ -f \"$dev/brightness\" ] && [ -f \"$dev/max_brightness\" ]; then echo \"$(cat $dev/brightness) $(cat $dev/max_brightness)\"; break; fi; done"] - } + } else { + // Internal backlight - try to find the first available backlight device + initProc.command = ["sh", "-c", "for dev in /sys/class/backlight/*; do if [ -f \"$dev/brightness\" ] && [ -f \"$dev/max_brightness\" ]; then echo \"$(cat $dev/brightness) $(cat $dev/max_brightness)\"; break; fi; done"] + } initProc.running = true } - - onBusNumChanged: initBrightness() Component.onCompleted: initBrightness() } -} \ No newline at end of file +} diff --git a/Services/Network.qml b/Services/Network.qml index bd8c70f..c25c646 100644 --- a/Services/Network.qml +++ b/Services/Network.qml @@ -48,7 +48,7 @@ QtObject { break } } - + // Disable WiFi radio disableWifiProcess.running = true } @@ -135,8 +135,7 @@ QtObject { } } - function onMenuClosed() { - // No need to do anything special on close + function onMenuClosed() {// No need to do anything special on close } // Process to enable WiFi radio @@ -167,12 +166,12 @@ QtObject { onTriggered: { // Force refresh networks multiple times to ensure UI updates root.refreshNetworks() - + // Try to auto-reconnect to the last connected network if it exists if (lastConnectedNetwork) { autoReconnectTimer.start() } - + // Set up additional refresh to ensure UI is populated postEnableRefreshTimer.start() } @@ -464,4 +463,4 @@ QtObject { refreshNetworks() } } -} \ No newline at end of file +} From 8afb71b1142f9038027ae28ef5fad2a42e0de73c Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 10:43:22 -0400 Subject: [PATCH 308/394] Brightness: Hiding a couple warnings, does not work at all with 3 monitors --- Services/BrightnessService.qml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Services/BrightnessService.qml b/Services/BrightnessService.qml index 72f5e4f..cd6cabd 100644 --- a/Services/BrightnessService.qml +++ b/Services/BrightnessService.qml @@ -208,7 +208,12 @@ Singleton { function getStoredBrightness(): real { // Try to get stored brightness for this specific monitor - var stored = Settings.data.brightness.monitorBrightness.find(m => m.name === modelData.name) + var stored = Settings.data.brightness.monitorBrightness.find(m => { + if (m !== null) { + return m.name === modelData.name + } + return false + }) if (stored) { return stored.brightness / 100 } @@ -224,7 +229,12 @@ Singleton { Settings.data.brightness.lastMethod = method // Update monitor-specific brightness - var monitorIndex = Settings.data.brightness.monitorBrightness.findIndex(m => m.name === modelData.name) + var monitorIndex = Settings.data.brightness.monitorBrightness.findIndex(m => { + if (m !== null) { + return m.name === modelData.name + } + return -1 + }) var monitorData = { "name": modelData.name, "brightness": brightnessPercent, From 2abe7c9bd1aaf379e2ae6395efb5724ce2ddc516 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 10:49:09 -0400 Subject: [PATCH 309/394] Notifications: proper display on selected screen --- Modules/Bar/Bar.qml | 1 + Modules/Dock/Dock.qml | 2 +- Modules/Notification/Notification.qml | 310 +++++++++++++------------- Modules/Settings/Tabs/DisplayTab.qml | 172 +++++++------- Modules/SidePanel/Cards/MediaCard.qml | 10 - Services/BrightnessService.qml | 20 +- 6 files changed, 260 insertions(+), 255 deletions(-) diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 420b67e..22d41fd 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -19,6 +19,7 @@ Variants { implicitHeight: Style.barHeight * scaling color: "transparent" + // If no bar display activated in settings, then show them all visible: modelData ? (Settings.data.bar.monitors.includes(modelData.name) || (Settings.data.bar.monitors.length === 0)) : false diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml index bb0f9c8..701ac7d 100644 --- a/Modules/Dock/Dock.qml +++ b/Modules/Dock/Dock.qml @@ -40,7 +40,7 @@ NLoader { PanelWindow { id: dockWindow - // Dock works differently from bar, it is show only if toggled in Settings/Display + // Dock is only shown if explicitely toggled visible: modelData ? Settings.data.dock.monitors.includes(modelData.name) : false screen: modelData diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index 99cc20e..5858e4e 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -8,182 +8,190 @@ import qs.Services import qs.Widgets // Simple notification popup - displays multiple notifications -PanelWindow { - id: root +Variants { + model: Quickshell.screens - readonly property real scaling: Scaling.scale(screen) + PanelWindow { + id: root - color: "transparent" - visible: notificationService.notificationModel.count > 0 - anchors.top: true - anchors.right: true - margins.top: (Style.barHeight + Style.marginMedium) * scaling - margins.right: Style.marginMedium * scaling - implicitWidth: 360 * scaling - implicitHeight: Math.min(notificationStack.implicitHeight, (notificationService.maxVisible * 120) * scaling) - WlrLayershell.layer: WlrLayer.Overlay - WlrLayershell.exclusionMode: ExclusionMode.Ignore + required property ShellScreen modelData + readonly property real scaling: Scaling.scale(screen) - // Use the notification service singleton - property var notificationService: NotificationService + // Access the notification model from the service + property ListModel notificationModel: NotificationService.notificationModel - // Access the notification model from the service - property ListModel notificationModel: notificationService.notificationModel + // Track notifications being removed for animation + property var removingNotifications: ({}) - // Track notifications being removed for animation - property var removingNotifications: ({}) + screen: modelData + color: "transparent" - // Connect to animation signal from service - Component.onCompleted: { - notificationService.animateAndRemove.connect(function (notification, index) { - // Find the delegate and trigger its animation - if (notificationStack.children && notificationStack.children[index]) { - let delegate = notificationStack.children[index] - if (delegate && delegate.animateOut) { - delegate.animateOut() - } - } - }) - } + // If no notification display activated in settings, then show them all + visible: modelData ? (Settings.data.notifications.monitors.includes(modelData.name) + || (Settings.data.notifications.monitors.length === 0)) + && (NotificationService.notificationModel.count > 0) : false - // Main notification container - Column { - id: notificationStack - anchors.top: parent.top - anchors.right: parent.right - spacing: 8 * scaling - width: 360 * scaling - visible: true + anchors.top: true + anchors.right: true + margins.top: (Style.barHeight + Style.marginMedium) * scaling + margins.right: Style.marginMedium * scaling + implicitWidth: 360 * scaling + implicitHeight: Math.min(notificationStack.implicitHeight, (NotificationService.maxVisible * 120) * scaling) + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.exclusionMode: ExclusionMode.Ignore - // Multiple notifications display - Repeater { - model: notificationModel - delegate: Rectangle { - width: 360 * scaling - height: Math.max(80 * scaling, contentColumn.implicitHeight + (Style.marginMedium * 2 * scaling)) - clip: true - radius: Style.radiusMedium * scaling - border.color: Colors.mPrimary - border.width: Math.max(1, Style.borderThin * scaling) - color: Colors.mSurface - - // Animation properties - property real scaleValue: 0.8 - property real opacityValue: 0.0 - property bool isRemoving: false - - // Scale and fade-in animation - scale: scaleValue - opacity: opacityValue - - // Animate in when the item is created - Component.onCompleted: { - scaleValue = 1.0 - opacityValue = 1.0 - } - - // Animate out when being removed - function animateOut() { - isRemoving = true - scaleValue = 0.8 - opacityValue = 0.0 - } - - // Timer for delayed removal after animation - Timer { - id: removalTimer - interval: Style.animationSlow - repeat: false - onTriggered: { - notificationService.forceRemoveNotification(model.rawNotification) + // Connect to animation signal from service + Component.onCompleted: { + NotificationService.animateAndRemove.connect(function (notification, index) { + // Find the delegate and trigger its animation + if (notificationStack.children && notificationStack.children[index]) { + let delegate = notificationStack.children[index] + if (delegate && delegate.animateOut) { + delegate.animateOut() } } + }) + } - // Check if this notification is being removed - onIsRemovingChanged: { - if (isRemoving) { - // Remove from model after animation completes - removalTimer.start() + // Main notification container + Column { + id: notificationStack + anchors.top: parent.top + anchors.right: parent.right + spacing: 8 * scaling + width: 360 * scaling + visible: true + + // Multiple notifications display + Repeater { + model: notificationModel + delegate: Rectangle { + width: 360 * scaling + height: Math.max(80 * scaling, contentColumn.implicitHeight + (Style.marginMedium * 2 * scaling)) + clip: true + radius: Style.radiusMedium * scaling + border.color: Colors.mPrimary + border.width: Math.max(1, Style.borderThin * scaling) + color: Colors.mSurface + + // Animation properties + property real scaleValue: 0.8 + property real opacityValue: 0.0 + property bool isRemoving: false + + // Scale and fade-in animation + scale: scaleValue + opacity: opacityValue + + // Animate in when the item is created + Component.onCompleted: { + scaleValue = 1.0 + opacityValue = 1.0 } - } - // Animation behaviors - Behavior on scale { - NumberAnimation { - duration: Style.animationSlow - easing.type: Easing.OutExpo - //easing.type: Easing.OutBack looks better but notification get clipped on all sides + // Animate out when being removed + function animateOut() { + isRemoving = true + scaleValue = 0.8 + opacityValue = 0.0 } - } - Behavior on opacity { - NumberAnimation { - duration: Style.animationNormal - easing.type: Easing.OutQuad + // Timer for delayed removal after animation + Timer { + id: removalTimer + interval: Style.animationSlow + repeat: false + onTriggered: { + NotificationService.forceRemoveNotification(model.rawNotification) + } } - } - Column { - id: contentColumn - anchors.fill: parent - anchors.margins: Style.marginMedium * scaling - spacing: Style.marginSmall * scaling + // Check if this notification is being removed + onIsRemovingChanged: { + if (isRemoving) { + // Remove from model after animation completes + removalTimer.start() + } + } - RowLayout { + // Animation behaviors + Behavior on scale { + NumberAnimation { + duration: Style.animationSlow + easing.type: Easing.OutExpo + //easing.type: Easing.OutBack looks better but notification get clipped on all sides + } + } + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutQuad + } + } + + Column { + id: contentColumn + anchors.fill: parent + anchors.margins: Style.marginMedium * scaling spacing: Style.marginSmall * scaling + + RowLayout { + spacing: Style.marginSmall * scaling + NText { + text: (model.appName || model.desktopEntry) || "Unknown App" + color: Colors.mSecondary + font.pointSize: Style.fontSizeSmall * scaling + } + Rectangle { + width: 6 * scaling + height: 6 * scaling + radius: 3 * scaling + color: (model.urgency === NotificationUrgency.Critical) ? Colors.mError : (model.urgency === NotificationUrgency.Low) ? Colors.mOnSurface : Colors.mPrimary + Layout.alignment: Qt.AlignVCenter + } + Item { + Layout.fillWidth: true + } + NText { + text: NotificationService.formatTimestamp(model.timestamp) + color: Colors.mOnSurface + font.pointSize: Style.fontSizeSmall * scaling + } + } + NText { - text: (model.appName || model.desktopEntry) || "Unknown App" - color: Colors.mSecondary - font.pointSize: Style.fontSizeSmall * scaling - } - Rectangle { - width: 6 * scaling - height: 6 * scaling - radius: 3 * scaling - color: (model.urgency === NotificationUrgency.Critical) ? Colors.mError : (model.urgency === NotificationUrgency.Low) ? Colors.mOnSurface : Colors.mPrimary - Layout.alignment: Qt.AlignVCenter - } - Item { - Layout.fillWidth: true - } - NText { - text: notificationService.formatTimestamp(model.timestamp) + text: model.summary || "No summary" + font.pointSize: Style.fontSizeLarge * scaling + font.weight: Style.fontWeightBold color: Colors.mOnSurface + wrapMode: Text.Wrap + width: 300 * scaling + maximumLineCount: 3 + elide: Text.ElideRight + } + + NText { + text: model.body || "" font.pointSize: Style.fontSizeSmall * scaling + color: Colors.mOnSurface + wrapMode: Text.Wrap + width: 300 * scaling + maximumLineCount: 5 + elide: Text.ElideRight } } - NText { - text: model.summary || "No summary" - font.pointSize: Style.fontSizeLarge * scaling - font.weight: Style.fontWeightBold - color: Colors.mOnSurface - wrapMode: Text.Wrap - width: 300 * scaling - maximumLineCount: 3 - elide: Text.ElideRight - } - - NText { - text: model.body || "" - font.pointSize: Style.fontSizeSmall * scaling - color: Colors.mOnSurface - wrapMode: Text.Wrap - width: 300 * scaling - maximumLineCount: 5 - elide: Text.ElideRight - } - } - - NIconButton { - sizeMultiplier: 0.8 - showBorder: false - anchors.top: parent.top - anchors.right: parent.right - anchors.margins: Style.marginSmall * scaling - icon: "close" - onClicked: { - animateOut() + NIconButton { + sizeMultiplier: 0.8 + showBorder: false + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: Style.marginSmall * scaling + icon: "close" + onClicked: { + animateOut() + } } } } diff --git a/Modules/Settings/Tabs/DisplayTab.qml b/Modules/Settings/Tabs/DisplayTab.qml index 3d1211a..3282813 100644 --- a/Modules/Settings/Tabs/DisplayTab.qml +++ b/Modules/Settings/Tabs/DisplayTab.qml @@ -47,6 +47,95 @@ Item { color: Colors.mOnSurface } + NText { + text: "By default, all bars are displayed. Select one or more below to narrow your view." + font.pointSize: Style.fontSize * scaling + color: Colors.mOnSurfaceVariant + } + + Repeater { + model: Quickshell.screens || [] + delegate: Rectangle { + Layout.fillWidth: true + radius: Style.radiusMedium * scaling + color: Colors.mSurface + border.color: Colors.mOutline + border.width: Math.max(1, Style.borderThin * scaling) + implicitHeight: contentCol.implicitHeight + Style.marginXL * 2 * scaling + + ColumnLayout { + id: contentCol + anchors.fill: parent + anchors.margins: Style.marginLarge * scaling + spacing: Style.marginTiniest * scaling + + NText { + text: (modelData.name || "Unknown") + font.pointSize: Style.fontSizeLarge * scaling + font.weight: Style.fontWeightBold + color: Colors.mSecondary + } + + NText { + text: `Resolution: ${modelData.width}x${modelData.height} - Position: (${modelData.x}, ${modelData.y})` + font.pointSize: Style.fontSizeSmall * scaling + color: Colors.mOnSurface + } + + ColumnLayout { + spacing: Style.marginLarge * scaling + + NToggle { + label: "Bar" + description: "Enable the top bar on this monitor" + value: (Settings.data.bar.monitors || []).indexOf(modelData.name) !== -1 + onToggled: function (newValue) { + if (newValue) { + Settings.data.bar.monitors = addMonitor(Settings.data.bar.monitors, modelData.name) + } else { + Settings.data.bar.monitors = removeMonitor(Settings.data.bar.monitors, modelData.name) + } + } + } + + NToggle { + label: "Dock" + description: "Enable the dock on this monitor" + value: (Settings.data.dock.monitors || []).indexOf(modelData.name) !== -1 + onToggled: function (newValue) { + if (newValue) { + Settings.data.dock.monitors = addMonitor(Settings.data.dock.monitors, modelData.name) + } else { + Settings.data.dock.monitors = removeMonitor(Settings.data.dock.monitors, modelData.name) + } + } + } + + NToggle { + label: "Notifications" + description: "Enable notifications on this monitor" + value: (Settings.data.notifications.monitors || []).indexOf(modelData.name) !== -1 + onToggled: function (newValue) { + if (newValue) { + Settings.data.notifications.monitors = addMonitor(Settings.data.notifications.monitors, + modelData.name) + } else { + Settings.data.notifications.monitors = removeMonitor(Settings.data.notifications.monitors, + modelData.name) + } + } + } + } + } + } + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginLarge * 2 * scaling + Layout.bottomMargin: Style.marginLarge * scaling + } + // Brightness Section ColumnLayout { spacing: Style.marginSmall * scaling @@ -92,89 +181,6 @@ Item { } } - NDivider { - Layout.fillWidth: true - Layout.topMargin: Style.marginLarge * 2 * scaling - Layout.bottomMargin: Style.marginLarge * scaling - } - - Repeater { - model: Quickshell.screens || [] - delegate: Rectangle { - Layout.fillWidth: true - radius: Style.radiusMedium * scaling - color: Colors.mSurface - border.color: Colors.mOutline - border.width: Math.max(1, Style.borderThin * scaling) - implicitHeight: contentCol.implicitHeight + Style.marginXL * 2 * scaling - - ColumnLayout { - id: contentCol - anchors.fill: parent - anchors.margins: Style.marginLarge * scaling - spacing: Style.marginTiniest * scaling - - NText { - text: (modelData.name || "Unknown") - font.pointSize: Style.fontSizeLarge * scaling - font.weight: Style.fontWeightBold - color: Colors.mSecondary - } - - NText { - text: `Resolution: ${modelData.width}x${modelData.height} - Position: (${modelData.x}, ${modelData.y})` - font.pointSize: Style.fontSizeSmall * scaling - color: Colors.mOnSurface - } - - ColumnLayout { - spacing: Style.marginLarge * scaling - - NToggle { - label: "Bar" - description: "Display the top bar on this monitor" - value: (Settings.data.bar.monitors || []).indexOf(modelData.name) !== -1 - onToggled: function (newValue) { - if (newValue) { - Settings.data.bar.monitors = addMonitor(Settings.data.bar.monitors, modelData.name) - } else { - Settings.data.bar.monitors = removeMonitor(Settings.data.bar.monitors, modelData.name) - } - } - } - - NToggle { - label: "Dock" - description: "Display the dock on this monitor" - value: (Settings.data.dock.monitors || []).indexOf(modelData.name) !== -1 - onToggled: function (newValue) { - if (newValue) { - Settings.data.dock.monitors = addMonitor(Settings.data.dock.monitors, modelData.name) - } else { - Settings.data.dock.monitors = removeMonitor(Settings.data.dock.monitors, modelData.name) - } - } - } - - NToggle { - label: "Notifications" - description: "Display notifications on this monitor" - value: (Settings.data.notifications.monitors || []).indexOf(modelData.name) !== -1 - onToggled: function (newValue) { - if (newValue) { - Settings.data.notifications.monitors = addMonitor(Settings.data.notifications.monitors, - modelData.name) - } else { - Settings.data.notifications.monitors = removeMonitor(Settings.data.notifications.monitors, - modelData.name) - } - } - } - } - } - } - } - Item { Layout.fillHeight: true } diff --git a/Modules/SidePanel/Cards/MediaCard.qml b/Modules/SidePanel/Cards/MediaCard.qml index c0eb9fc..9437c62 100644 --- a/Modules/SidePanel/Cards/MediaCard.qml +++ b/Modules/SidePanel/Cards/MediaCard.qml @@ -339,15 +339,5 @@ NBox { Layout.alignment: Qt.AlignHCenter } } - - // CircularSpectrum { - // visible: Settings.data.audio.visualizerType == "radial" - // values: Cava.values - // innerRadius: 30 * scaling // Position just outside 60x60 album art - // outerRadius: 48 * scaling // Extend bars outward from album art - // fillColor: Colors.mPrimary - // strokeColor: Colors.mPrimary - // strokeWidth: 0 * scaling - // } } } diff --git a/Services/BrightnessService.qml b/Services/BrightnessService.qml index cd6cabd..9dd9e15 100644 --- a/Services/BrightnessService.qml +++ b/Services/BrightnessService.qml @@ -209,11 +209,11 @@ Singleton { function getStoredBrightness(): real { // Try to get stored brightness for this specific monitor var stored = Settings.data.brightness.monitorBrightness.find(m => { - if (m !== null) { - return m.name === modelData.name - } - return false - }) + if (m !== null) { + return m.name === modelData.name + } + return false + }) if (stored) { return stored.brightness / 100 } @@ -230,11 +230,11 @@ Singleton { // Update monitor-specific brightness var monitorIndex = Settings.data.brightness.monitorBrightness.findIndex(m => { - if (m !== null) { - return m.name === modelData.name - } - return -1 - }) + if (m !== null) { + return m.name === modelData.name + } + return -1 + }) var monitorData = { "name": modelData.name, "brightness": brightnessPercent, From f21364781a84ab8d886718c1c8df869672fa79bf Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 10:53:29 -0400 Subject: [PATCH 310/394] Better explanations in Settings/Display --- Modules/Settings/Tabs/DisplayTab.qml | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Modules/Settings/Tabs/DisplayTab.qml b/Modules/Settings/Tabs/DisplayTab.qml index 3282813..b9b39d0 100644 --- a/Modules/Settings/Tabs/DisplayTab.qml +++ b/Modules/Settings/Tabs/DisplayTab.qml @@ -48,7 +48,7 @@ Item { } NText { - text: "By default, all bars are displayed. Select one or more below to narrow your view." + text: "By default, bars and notifications are shown on all displays. Select one or more below to narrow your view." font.pointSize: Style.fontSize * scaling color: Colors.mOnSurfaceVariant } @@ -98,19 +98,6 @@ Item { } } - NToggle { - label: "Dock" - description: "Enable the dock on this monitor" - value: (Settings.data.dock.monitors || []).indexOf(modelData.name) !== -1 - onToggled: function (newValue) { - if (newValue) { - Settings.data.dock.monitors = addMonitor(Settings.data.dock.monitors, modelData.name) - } else { - Settings.data.dock.monitors = removeMonitor(Settings.data.dock.monitors, modelData.name) - } - } - } - NToggle { label: "Notifications" description: "Enable notifications on this monitor" @@ -125,6 +112,19 @@ Item { } } } + + NToggle { + label: "Dock" + description: "Enable the dock on this monitor" + value: (Settings.data.dock.monitors || []).indexOf(modelData.name) !== -1 + onToggled: function (newValue) { + if (newValue) { + Settings.data.dock.monitors = addMonitor(Settings.data.dock.monitors, modelData.name) + } else { + Settings.data.dock.monitors = removeMonitor(Settings.data.dock.monitors, modelData.name) + } + } + } } } } From f67c48032f76df1f0d23f95077ca31b22cbbbdb2 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 11:01:17 -0400 Subject: [PATCH 311/394] LockScreen: Using a property for scaling --- Modules/LockScreen/LockScreen.qml | 113 +++++++++++++++--------------- 1 file changed, 58 insertions(+), 55 deletions(-) diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index 04693c2..9efce8e 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -13,6 +13,9 @@ import qs.Widgets WlSessionLock { id: lock + // Lockscreen is a different beast, needs a capital 'S' in 'Screen' to get the current screen + readonly property real scaling: Scaling.scale(Screen) + property string errorMessage: "" property bool authenticating: false property string password: "" @@ -166,12 +169,12 @@ WlSessionLock { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - anchors.topMargin: 80 * Scaling.scale(screen) - spacing: 40 * Scaling.scale(screen) + anchors.topMargin: 80 * scaling + spacing: 40 * scaling // Time display - Large and prominent with pulse animation Column { - spacing: 8 * Scaling.scale(screen) + spacing: 8 * scaling Layout.alignment: Qt.AlignHCenter Text { @@ -213,28 +216,28 @@ WlSessionLock { // User section with animated avatar Column { - spacing: 16 * Scaling.scale(screen) + spacing: 16 * scaling Layout.alignment: Qt.AlignHCenter // Animated avatar with glow effect Rectangle { - width: 120 * Scaling.scale(screen) - height: 120 * Scaling.scale(screen) + width: 120 * scaling + height: 120 * scaling radius: width * 0.5 color: "transparent" border.color: Colors.mPrimary - border.width: 3 * Scaling.scale(screen) + border.width: 3 * scaling anchors.horizontalCenter: parent.horizontalCenter // Glow effect Rectangle { anchors.centerIn: parent - width: parent.width + 24 * Scaling.scale(screen) - height: parent.height + 24 * Scaling.scale(screen) + width: parent.width + 24 * scaling + height: parent.height + 24 * scaling radius: width * 0.5 color: "transparent" border.color: Qt.rgba(Colors.mPrimary.r, Colors.mPrimary.g, Colors.mPrimary.b, 0.3) - border.width: 2 * Scaling.scale(screen) + border.width: 2 * scaling z: -1 SequentialAnimation on scale { @@ -254,8 +257,8 @@ WlSessionLock { NImageRounded { anchors.centerIn: parent - width: 100 * Scaling.scale(screen) - height: 100 * Scaling.scale(screen) + width: 100 * scaling + height: 100 * scaling imagePath: Quickshell.env("HOME") + "/.face" fallbackIcon: "person" imageRadius: width * 0.5 @@ -281,19 +284,19 @@ WlSessionLock { // Centered terminal section Item { - width: 720 * Scaling.scale(screen) - height: 280 * Scaling.scale(screen) + width: 720 * scaling + height: 280 * scaling anchors.centerIn: parent ColumnLayout { anchors.centerIn: parent - spacing: 20 * Scaling.scale(screen) + spacing: 20 * scaling width: parent.width // Futuristic Terminal-Style Input Item { width: parent.width - height: 280 * Scaling.scale(screen) + height: 280 * scaling Layout.fillWidth: true // Terminal background with scanlines @@ -303,7 +306,7 @@ WlSessionLock { radius: 16 color: Colors.applyOpacity(Colors.mSurface, "E6") border.color: Colors.mPrimary - border.width: 2 * Scaling.scale(screen) + border.width: 2 * scaling // Scanline effect Repeater { @@ -332,15 +335,15 @@ WlSessionLock { // Terminal header Rectangle { width: parent.width - height: 40 * Scaling.scale(screen) + height: 40 * scaling color: Colors.applyOpacity(Colors.mPrimary, "33") topLeftRadius: 14 topRightRadius: 14 RowLayout { anchors.fill: parent - anchors.margins: 12 * Scaling.scale(screen) - spacing: 12 * Scaling.scale(screen) + anchors.margins: 12 * scaling + spacing: 12 * scaling Text { text: "SECURE TERMINAL" @@ -359,14 +362,14 @@ WlSessionLock { anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom - anchors.topMargin: 70 * Scaling.scale(screen) - anchors.margins: 12 * Scaling.scale(screen) - spacing: 12 * Scaling.scale(screen) + anchors.topMargin: 70 * scaling + anchors.margins: 12 * scaling + spacing: 12 * scaling // Welcome back typing effect RowLayout { Layout.fillWidth: true - spacing: 12 * Scaling.scale(screen) + spacing: 12 * scaling Text { text: "root@noctalia:~$" @@ -404,7 +407,7 @@ WlSessionLock { // Command line with integrated password input RowLayout { Layout.fillWidth: true - spacing: 12 * Scaling.scale(screen) + spacing: 12 * scaling Text { text: "root@noctalia:~$" @@ -415,7 +418,7 @@ WlSessionLock { } Text { - text: "sudo unlock_session" + text: "sudo unlock-session" color: Colors.mOnSurface font.family: "DejaVu Sans Mono" font.pointSize: Style.fontSizeLarge @@ -481,12 +484,12 @@ WlSessionLock { // Blinking cursor positioned right after the asterisks Rectangle { - width: 8 * Scaling.scale(screen) - height: 20 * Scaling.scale(screen) + width: 8 * scaling + height: 20 * scaling color: Colors.mPrimary visible: passwordInput.activeFocus anchors.left: asterisksText.right - anchors.leftMargin: 2 * Scaling.scale(screen) + anchors.leftMargin: 2 * scaling anchors.verticalCenter: asterisksText.verticalCenter SequentialAnimation on opacity { @@ -527,15 +530,15 @@ WlSessionLock { // Execute button Rectangle { - width: 120 * Scaling.scale(screen) - height: 40 * Scaling.scale(screen) + width: 120 * scaling + height: 40 * scaling radius: 12 color: executeButtonArea.containsMouse ? Colors.mPrimary : Colors.applyOpacity(Colors.mPrimary, "33") border.color: Colors.mPrimary border.width: 1 enabled: !lock.authenticating Layout.alignment: Qt.AlignRight - Layout.bottomMargin: -12 * Scaling.scale(screen) + Layout.bottomMargin: -12 * scaling Text { anchors.centerIn: parent @@ -622,27 +625,27 @@ WlSessionLock { Row { anchors.right: parent.right anchors.bottom: parent.bottom - anchors.margins: 50 * Scaling.scale(screen) - spacing: 20 * Scaling.scale(screen) + anchors.margins: 50 * scaling + spacing: 20 * scaling // Shutdown with enhanced styling Rectangle { - width: 64 * Scaling.scale(screen) - height: 64 * Scaling.scale(screen) + width: 64 * scaling + height: 64 * scaling radius: 32 color: Qt.rgba(Colors.mError.r, Colors.mError.g, Colors.mError.b, shutdownArea.containsMouse ? 0.9 : 0.2) border.color: Colors.mError - border.width: 2 * Scaling.scale(screen) + border.width: 2 * scaling // Glow effect Rectangle { anchors.centerIn: parent - width: parent.width + 10 * Scaling.scale(screen) - height: parent.height + 10 * Scaling.scale(screen) + width: parent.width + 10 * scaling + height: parent.height + 10 * scaling radius: width * 0.5 color: "transparent" border.color: Qt.rgba(Colors.mError.r, Colors.mError.g, Colors.mError.b, 0.3) - border.width: 2 * Scaling.scale(screen) + border.width: 2 * scaling opacity: shutdownArea.containsMouse ? 1 : 0 z: -1 @@ -669,7 +672,7 @@ WlSessionLock { anchors.centerIn: parent text: "power_settings_new" font.family: "Material Symbols Outlined" - font.pixelSize: 28 * Scaling.scale(screen) + font.pixelSize: 28 * scaling color: shutdownArea.containsMouse ? Colors.onAccent : Colors.mError } @@ -684,22 +687,22 @@ WlSessionLock { // Reboot with enhanced styling Rectangle { - width: 64 * Scaling.scale(screen) - height: 64 * Scaling.scale(screen) + width: 64 * scaling + height: 64 * scaling radius: 32 color: Qt.rgba(Colors.mPrimary.r, Colors.mPrimary.g, Colors.mPrimary.b, rebootArea.containsMouse ? 0.9 : 0.2) border.color: Colors.mPrimary - border.width: 2 * Scaling.scale(screen) + border.width: 2 * scaling // Glow effect Rectangle { anchors.centerIn: parent - width: parent.width + 10 * Scaling.scale(screen) - height: parent.height + 10 * Scaling.scale(screen) + width: parent.width + 10 * scaling + height: parent.height + 10 * scaling radius: width * 0.5 color: "transparent" border.color: Qt.rgba(Colors.mPrimary.r, Colors.mPrimary.g, Colors.mPrimary.b, 0.3) - border.width: 2 * Scaling.scale(screen) + border.width: 2 * scaling opacity: rebootArea.containsMouse ? 1 : 0 z: -1 @@ -725,7 +728,7 @@ WlSessionLock { anchors.centerIn: parent text: "refresh" font.family: "Material Symbols Outlined" - font.pixelSize: 28 * Scaling.scale(screen) + font.pixelSize: 28 * scaling color: rebootArea.containsMouse ? Colors.onAccent : Colors.mPrimary } @@ -740,23 +743,23 @@ WlSessionLock { // Logout with enhanced styling Rectangle { - width: 64 * Scaling.scale(screen) - height: 64 * Scaling.scale(screen) + width: 64 * scaling + height: 64 * scaling radius: 32 color: Qt.rgba(Colors.mSecondary.r, Colors.mSecondary.g, Colors.mSecondary.b, logoutArea.containsMouse ? 0.9 : 0.2) border.color: Colors.mSecondary - border.width: 2 * Scaling.scale(screen) + border.width: 2 * scaling // Glow effect Rectangle { anchors.centerIn: parent - width: parent.width + 10 * Scaling.scale(screen) - height: parent.height + 10 * Scaling.scale(screen) + width: parent.width + 10 * scaling + height: parent.height + 10 * scaling radius: width * 0.5 color: "transparent" border.color: Qt.rgba(Colors.mSecondary.r, Colors.mSecondary.g, Colors.mSecondary.b, 0.3) - border.width: 2 * Scaling.scale(screen) + border.width: 2 * scaling opacity: logoutArea.containsMouse ? 1 : 0 z: -1 @@ -784,7 +787,7 @@ WlSessionLock { anchors.centerIn: parent text: "exit_to_app" font.family: "Material Symbols Outlined" - font.pixelSize: 28 * Scaling.scale(screen) + font.pixelSize: 28 * scaling color: logoutArea.containsMouse ? Colors.onAccent : Colors.mSecondary } From 33bec01e98f5d9f37173034438522c3454616aaf Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 11:24:05 -0400 Subject: [PATCH 312/394] Removed all creeping: readonly property real scaling: Scaling.scale(screen) --- Modules/Background/ScreenCorners.qml | 6 +- Modules/Bar/Bar.qml | 4 +- Modules/Bar/NotificationHistory.qml | 1 - Modules/Bar/Tray.qml | 1 - Modules/Bar/TrayMenu.qml | 1 - Modules/Bar/WiFi.qml | 1 - Modules/Bar/Workspace.qml | 37 +- Modules/Calendar/Calendar.qml | 2 - Modules/Demo/DemoPanel.qml | 16 +- Modules/Dock/Dock.qml | 525 +++++++++--------- Modules/Notification/Notification.qml | 2 +- Modules/Settings/SettingsPanel.qml | 1 - Modules/SidePanel/Cards/ProfileCard.qml | 1 - Modules/SidePanel/Cards/SystemMonitorCard.qml | 2 - Modules/SidePanel/Cards/WeatherCard.qml | 1 - Modules/SidePanel/SidePanel.qml | 1 - Widgets/NBox.qml | 2 - Widgets/NBusyIndicator.qml | 2 - Widgets/NCard.qml | 2 - Widgets/NCircleStat.qml | 1 - Widgets/NClock.qml | 2 - Widgets/NComboBox.qml | 1 - Widgets/NIconButton.qml | 1 - Widgets/NImageRounded.qml | 1 - Widgets/NPanel.qml | 4 +- Widgets/NPill.qml | 2 - Widgets/NSlider.qml | 2 - Widgets/NText.qml | 2 - Widgets/NTextInput.qml | 1 - Widgets/NToggle.qml | 1 - Widgets/NTooltip.qml | 1 - 31 files changed, 294 insertions(+), 333 deletions(-) diff --git a/Modules/Background/ScreenCorners.qml b/Modules/Background/ScreenCorners.qml index 7dc5076..59e88de 100644 --- a/Modules/Background/ScreenCorners.qml +++ b/Modules/Background/ScreenCorners.qml @@ -15,6 +15,8 @@ NLoader { id: root required property ShellScreen modelData + readonly property real scaling: Scaling.scale(screen) + screen: modelData // Visible ring color property color ringColor: Colors.mSurface @@ -25,7 +27,7 @@ NLoader { property int innerRadius: 20 color: "transparent" - screen: modelData + WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.namespace: "quickshell-corner" // Do not take keyboard focus and make the surface click-through @@ -39,7 +41,7 @@ NLoader { } margins { - top: Math.round(Style.barHeight * Scaling.scale(screen)) + top: Math.floor(Style.barHeight * scaling) } // Source we want to show only as a ring diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 22d41fd..cc1b5be 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -14,12 +14,12 @@ Variants { required property ShellScreen modelData readonly property real scaling: Scaling.scale(screen) - screen: modelData + implicitHeight: Style.barHeight * scaling color: "transparent" - // If no bar display activated in settings, then show them all + // If no bar activated in settings, then show them all visible: modelData ? (Settings.data.bar.monitors.includes(modelData.name) || (Settings.data.bar.monitors.length === 0)) : false diff --git a/Modules/Bar/NotificationHistory.qml b/Modules/Bar/NotificationHistory.qml index d4c3b47..f6ae033 100644 --- a/Modules/Bar/NotificationHistory.qml +++ b/Modules/Bar/NotificationHistory.qml @@ -9,7 +9,6 @@ import qs.Widgets NIconButton { id: root - readonly property real scaling: Scaling.scale(screen) sizeMultiplier: 0.8 showBorder: false icon: "notifications" diff --git a/Modules/Bar/Tray.qml b/Modules/Bar/Tray.qml index e35a7ac..615749f 100644 --- a/Modules/Bar/Tray.qml +++ b/Modules/Bar/Tray.qml @@ -9,7 +9,6 @@ import qs.Services import qs.Widgets Item { - readonly property real scaling: Scaling.scale(screen) readonly property real itemSize: 24 * scaling width: tray.width diff --git a/Modules/Bar/TrayMenu.qml b/Modules/Bar/TrayMenu.qml index da52e96..dd94d8d 100644 --- a/Modules/Bar/TrayMenu.qml +++ b/Modules/Bar/TrayMenu.qml @@ -8,7 +8,6 @@ import qs.Widgets PopupWindow { id: trayMenu - readonly property real scaling: Scaling.scale(screen) property QsMenuHandle menu property var anchorItem: null property real anchorX diff --git a/Modules/Bar/WiFi.qml b/Modules/Bar/WiFi.qml index 5bd5c89..ef28f39 100644 --- a/Modules/Bar/WiFi.qml +++ b/Modules/Bar/WiFi.qml @@ -9,7 +9,6 @@ import qs.Widgets NIconButton { id: root - readonly property real scaling: Scaling.scale(screen) readonly property bool wifiEnabled: Settings.data.network.wifiEnabled sizeMultiplier: 0.8 showBorder: false diff --git a/Modules/Bar/Workspace.qml b/Modules/Bar/Workspace.qml index 79ef0b9..669192b 100644 --- a/Modules/Bar/Workspace.qml +++ b/Modules/Bar/Workspace.qml @@ -12,16 +12,13 @@ Item { property bool isDestroying: false property bool hovered: false - // Unified scale - readonly property real s: Scaling.scale(screen) - property ListModel localWorkspaces: ListModel {} property real masterProgress: 0.0 property bool effectsActive: false property color effectColor: Colors.mPrimary - property int horizontalPadding: Math.round(16 * s) - property int spacingBetweenPills: Math.round(8 * s) + property int horizontalPadding: Math.round(16 * scaling) + property int spacingBetweenPills: Math.round(8 * scaling) signal workspaceChanged(int workspaceId, color accentColor) @@ -30,18 +27,18 @@ Item { for (var i = 0; i < localWorkspaces.count; i++) { const ws = localWorkspaces.get(i) if (ws.isFocused) - total += Math.round(44 * s) + total += Math.round(44 * scaling) else if (ws.isActive) - total += Math.round(28 * s) + total += Math.round(28 * scaling) else - total += Math.round(16 * s) + total += Math.round(16 * scaling) } total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills total += horizontalPadding * 2 return total } - height: Math.round(36 * s) + height: Math.round(36 * scaling) Component.onCompleted: { localWorkspaces.clear() @@ -116,14 +113,14 @@ Item { Rectangle { id: workspaceBackground - width: parent.width - Math.round(15 * s) - height: Math.round(26 * s) + width: parent.width - Math.round(15 * scaling) + height: Math.round(26 * scaling) anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter - radius: Math.round(12 * s) + radius: Math.round(12 * scaling) color: Colors.mSurfaceVariant border.color: Colors.mOutlineVariant - border.width: Math.max(1, Math.round(1 * s)) + border.width: Math.max(1, Math.round(1 * scaling)) layer.enabled: true layer.effect: MultiEffect { shadowColor: Colors.mShadow @@ -144,14 +141,14 @@ Item { model: localWorkspaces Item { id: workspacePillContainer - height: Math.round(12 * s) + height: Math.round(12 * scaling) width: { if (model.isFocused) - return Math.round(44 * s) + return Math.round(44 * scaling) else if (model.isActive) - return Math.round(28 * s) + return Math.round(28 * scaling) else - return Math.round(16 * s) + return Math.round(16 * scaling) } Rectangle { @@ -159,10 +156,10 @@ Item { anchors.fill: parent radius: { if (model.isFocused) - return Math.round(12 * s) + return Math.round(12 * scaling) else // half of focused height (if you want to animate this too) - return Math.round(6 * s) + return Math.round(6 * scaling) } color: { if (model.isFocused) @@ -248,7 +245,7 @@ Item { radius: width / 2 color: "transparent" border.color: root.effectColor - border.width: Math.max(1, Math.round((2 + 6 * (1.0 - root.masterProgress)) * s)) + border.width: Math.max(1, Math.round((2 + 6 * (1.0 - root.masterProgress)) * scaling)) opacity: root.effectsActive && model.isFocused ? (1.0 - root.masterProgress) * 0.7 : 0 visible: root.effectsActive && model.isFocused z: 1 diff --git a/Modules/Calendar/Calendar.qml b/Modules/Calendar/Calendar.qml index d1dd99a..d433718 100644 --- a/Modules/Calendar/Calendar.qml +++ b/Modules/Calendar/Calendar.qml @@ -13,8 +13,6 @@ NLoader { NPanel { id: calendarPanel - readonly property real scaling: Scaling.scale(screen) - // Override hide function to animate first function hide() { // Start hide animation diff --git a/Modules/Demo/DemoPanel.qml b/Modules/Demo/DemoPanel.qml index fa238f1..cd02fd7 100644 --- a/Modules/Demo/DemoPanel.qml +++ b/Modules/Demo/DemoPanel.qml @@ -13,8 +13,6 @@ NLoader { NPanel { id: demoPanel - readonly property real scaling: Scaling.scale(screen) - // Override hide function to animate first function hide() { // Start hide animation @@ -73,7 +71,7 @@ NLoader { border.color: Colors.mOutlineVariant border.width: Math.max(1, Style.borderThin * scaling) width: 500 * scaling - height: 700 * scaling + height: 900 * scaling anchors.centerIn: parent // Animation properties @@ -312,7 +310,7 @@ NLoader { } NText { - text: `Brightness: ${Math.round(Brightness.brightness)}%` + text: `Brightness: ${Math.round(BrightnessService.brightness)}%` Layout.alignment: Qt.AlignVCenter } @@ -322,30 +320,30 @@ NLoader { icon: "brightness_low" fontPointSize: Style.fontSizeLarge * scaling onClicked: { - Brightness.decreaseBrightness() + BrightnessService.decreaseBrightness() } } NSlider { from: 0 to: 100 stepSize: 1 - value: Brightness.brightness + value: BrightnessService.brightness implicitWidth: bgRect.width * 0.5 onMoved: { - Brightness.setBrightnessDebounced(value) + BrightnessService.setBrightnessDebounced(value) } } NIconButton { icon: "brightness_high" fontPointSize: Style.fontSizeLarge * scaling onClicked: { - Brightness.increaseBrightness() + BrightnessService.increaseBrightness() } } } NText { - text: `Method: ${Brightness.currentMethod} | Available: ${Brightness.available}` + text: `Method: ${BrightnessService.currentMethod} | Available: ${BrightnessService.available}` color: Colors.mOnSurfaceVariant font.pointSize: Style.fontSizeSmall * scaling Layout.alignment: Qt.AlignHCenter diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml index 701ac7d..a22e490 100644 --- a/Modules/Dock/Dock.qml +++ b/Modules/Dock/Dock.qml @@ -14,9 +14,12 @@ NLoader { Variants { model: Quickshell.screens - Item { - property var modelData - readonly property real scaling: Scaling.scale(modelData) + PanelWindow { + id: dockWindow + + required property ShellScreen modelData + readonly property real scaling: Scaling.scale(screen) + screen: modelData // Auto-hide properties property bool autoHide: Settings.data.dock.autoHide @@ -37,320 +40,314 @@ NLoader { property var contextMenuTarget: null property var contextMenuToplevel: null - PanelWindow { - id: dockWindow + // Dock is only shown if explicitely toggled + visible: modelData ? Settings.data.dock.monitors.includes(modelData.name) : false - // Dock is only shown if explicitely toggled - visible: modelData ? Settings.data.dock.monitors.includes(modelData.name) : false + exclusionMode: ExclusionMode.Ignore + anchors.bottom: true + anchors.left: true + anchors.right: true + focusable: false + color: "transparent" + implicitHeight: 60 - screen: modelData - exclusionMode: ExclusionMode.Ignore - anchors.bottom: true - anchors.left: true - anchors.right: true - focusable: false - color: "transparent" - implicitHeight: 60 + // Timer for auto-hide delay + Timer { + id: hideTimer + interval: hideDelay + onTriggered: if (autoHide && !dockHovered && !anyAppHovered) + hidden = true + } - // Timer for auto-hide delay - Timer { - id: hideTimer - interval: hideDelay - onTriggered: if (autoHide && !dockHovered && !anyAppHovered) - hidden = true + // Timer for show delay + Timer { + id: showTimer + interval: showDelay + onTriggered: hidden = false + } + + // Behavior for smooth hide/show animations + Behavior on margins.bottom { + NumberAnimation { + duration: hidden ? hideAnimationDuration : showAnimationDuration + easing.type: Easing.InOutQuad } + } - // Timer for show delay - Timer { - id: showTimer - interval: showDelay - onTriggered: hidden = false + // Mouse area at screen bottom to detect entry and keep dock visible + MouseArea { + id: screenEdgeMouseArea + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: 10 + hoverEnabled: true + propagateComposedEvents: true + + onEntered: if (autoHide && hidden) + showTimer.start() + onExited: if (autoHide && !hidden && !dockHovered && !anyAppHovered) + hideTimer.start() + } + + margins.bottom: hidden ? -(fullHeight - peekHeight) : 0 + + MouseArea { + anchors.fill: parent + enabled: contextMenuVisible + onClicked: { + contextMenuVisible = false + contextMenuTarget = null + contextMenuToplevel = null } + } - // Behavior for smooth hide/show animations - Behavior on margins.bottom { - NumberAnimation { - duration: hidden ? hideAnimationDuration : showAnimationDuration - easing.type: Easing.InOutQuad - } - } + Rectangle { + id: dockContainer + width: dock.width + 40 + height: 50 + color: Colors.mSurface + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + topLeftRadius: 20 + topRightRadius: 20 - // Mouse area at screen bottom to detect entry and keep dock visible MouseArea { - id: screenEdgeMouseArea - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - height: 10 + id: dockMouseArea + anchors.fill: parent hoverEnabled: true propagateComposedEvents: true - onEntered: if (autoHide && hidden) - showTimer.start() - onExited: if (autoHide && !hidden && !dockHovered && !anyAppHovered) - hideTimer.start() - } - - margins.bottom: hidden ? -(fullHeight - peekHeight) : 0 - - MouseArea { - anchors.fill: parent - enabled: contextMenuVisible - onClicked: { - contextMenuVisible = false - contextMenuTarget = null - contextMenuToplevel = null + onEntered: { + dockHovered = true + if (autoHide) { + showTimer.stop() + hideTimer.stop() + hidden = false + } + } + onExited: { + dockHovered = false + if (autoHide && !anyAppHovered && !contextMenuVisible) + hideTimer.start() } } - Rectangle { - id: dockContainer - width: dock.width + 40 - height: 50 - color: Colors.mSurface - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: parent.bottom - topLeftRadius: 20 - topRightRadius: 20 + Item { + id: dock + width: runningAppsRow.width + height: parent.height - 10 + anchors.centerIn: parent - MouseArea { - id: dockMouseArea - anchors.fill: parent - hoverEnabled: true - propagateComposedEvents: true - - onEntered: { - dockHovered = true - if (autoHide) { - showTimer.stop() - hideTimer.stop() - hidden = false - } - } - onExited: { - dockHovered = false - if (autoHide && !anyAppHovered && !contextMenuVisible) - hideTimer.start() - } + NTooltip { + id: appTooltip + visible: false + positionAbove: true } - Item { - id: dock - width: runningAppsRow.width - height: parent.height - 10 + function getAppIcon(toplevel: Toplevel): string { + if (!toplevel) + return "" + let icon = Quickshell.iconPath(toplevel.appId?.toLowerCase(), true) + if (!icon) + icon = Quickshell.iconPath(toplevel.appId, true) + if (!icon) + icon = Quickshell.iconPath(toplevel.title?.toLowerCase(), true) + if (!icon) + icon = Quickshell.iconPath(toplevel.title, true) + return icon || Quickshell.iconPath("application-x-executable", true) + } + + Row { + id: runningAppsRow + spacing: 8 + height: parent.height anchors.centerIn: parent - NTooltip { - id: appTooltip - visible: false - positionAbove: true - } + Repeater { + model: ToplevelManager ? ToplevelManager.toplevels : null - function getAppIcon(toplevel: Toplevel): string { - if (!toplevel) - return "" - let icon = Quickshell.iconPath(toplevel.appId?.toLowerCase(), true) - if (!icon) - icon = Quickshell.iconPath(toplevel.appId, true) - if (!icon) - icon = Quickshell.iconPath(toplevel.title?.toLowerCase(), true) - if (!icon) - icon = Quickshell.iconPath(toplevel.title, true) - return icon || Quickshell.iconPath("application-x-executable", true) - } + delegate: Rectangle { + id: appButton + width: 36 + height: 36 + radius: 18 + color: "transparent" - Row { - id: runningAppsRow - spacing: 8 - height: parent.height - anchors.centerIn: parent + property bool isActive: ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData + property bool hovered: appMouseArea.containsMouse + property string appId: modelData ? modelData.appId : "" + property string appTitle: modelData ? modelData.title : "" - Repeater { - model: ToplevelManager ? ToplevelManager.toplevels : null + Behavior on color { + ColorAnimation { + duration: 150 + } + } - delegate: Rectangle { - id: appButton - width: 36 - height: 36 - radius: 18 - color: "transparent" + Image { + id: appIcon + width: 28 + height: 28 + anchors.centerIn: parent + source: dock.getAppIcon(modelData) + visible: source.toString() !== "" + smooth: false + mipmap: false + antialiasing: false + fillMode: Image.PreserveAspectFit + } - property bool isActive: ToplevelManager.activeToplevel - && ToplevelManager.activeToplevel === modelData - property bool hovered: appMouseArea.containsMouse - property string appId: modelData ? modelData.appId : "" - property string appTitle: modelData ? modelData.title : "" + Text { + anchors.centerIn: parent + visible: !appIcon.visible + text: appButton.appId ? appButton.appId.charAt(0).toUpperCase() : "?" + font.pixelSize: 14 + font.bold: true + color: appButton.isActive ? Colors.mPrimary : Colors.mOnSurface + } - Behavior on color { - ColorAnimation { - duration: 150 + MouseArea { + id: appMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + + onEntered: { + anyAppHovered = true + const appName = appButton.appTitle || appButton.appId || "Unknown" + appTooltip.text = appName.length > 40 ? appName.substring(0, 37) + "..." : appName + appTooltip.target = appButton + appTooltip.isVisible = true + if (autoHide) { + showTimer.stop() + hideTimer.stop() + hidden = false } } - Image { - id: appIcon - width: 28 - height: 28 - anchors.centerIn: parent - source: dock.getAppIcon(modelData) - visible: source.toString() !== "" - smooth: false - mipmap: false - antialiasing: false - fillMode: Image.PreserveAspectFit + onExited: { + anyAppHovered = false + appTooltip.hide() + if (autoHide && !dockHovered && !contextMenuVisible) + hideTimer.start() } - Text { - anchors.centerIn: parent - visible: !appIcon.visible - text: appButton.appId ? appButton.appId.charAt(0).toUpperCase() : "?" - font.pixelSize: 14 - font.bold: true - color: appButton.isActive ? Colors.mPrimary : Colors.mOnSurface - } - - MouseArea { - id: appMouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - - onEntered: { - anyAppHovered = true - const appName = appButton.appTitle || appButton.appId || "Unknown" - appTooltip.text = appName.length > 40 ? appName.substring(0, 37) + "..." : appName - appTooltip.target = appButton - appTooltip.isVisible = true - if (autoHide) { - showTimer.stop() - hideTimer.stop() - hidden = false - } + onClicked: function (mouse) { + if (mouse.button === Qt.MiddleButton && modelData?.close) { + modelData.close() } - - onExited: { - anyAppHovered = false + if (mouse.button === Qt.LeftButton && modelData?.activate) { + modelData.activate() + } + if (mouse.button === Qt.RightButton) { appTooltip.hide() - if (autoHide && !dockHovered && !contextMenuVisible) - hideTimer.start() - } - - onClicked: function (mouse) { - if (mouse.button === Qt.MiddleButton && modelData?.close) { - modelData.close() - } - if (mouse.button === Qt.LeftButton && modelData?.activate) { - modelData.activate() - } - if (mouse.button === Qt.RightButton) { - appTooltip.hide() - contextMenuTarget = appButton - contextMenuToplevel = modelData - contextMenuVisible = true - } + contextMenuTarget = appButton + contextMenuToplevel = modelData + contextMenuVisible = true } } + } - Rectangle { - visible: isActive - width: 20 - height: 3 - color: Colors.mPrimary - radius: 1.5 - anchors.bottom: parent.bottom - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottomMargin: 2 - } + Rectangle { + visible: isActive + width: 20 + height: 3 + color: Colors.mPrimary + radius: 1.5 + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottomMargin: 2 } } } } } + } - // Context Menu - PanelWindow { - id: contextMenuWindow - visible: contextMenuVisible - screen: dockWindow.screen - exclusionMode: ExclusionMode.Ignore - anchors.bottom: true - anchors.left: true - anchors.right: true - color: "transparent" - focusable: false + // Context Menu + PanelWindow { + id: contextMenuWindow + visible: contextMenuVisible + screen: dockWindow.screen + exclusionMode: ExclusionMode.Ignore + anchors.bottom: true + anchors.left: true + anchors.right: true + color: "transparent" + focusable: false + + MouseArea { + anchors.fill: parent + onClicked: { + contextMenuVisible = false + contextMenuTarget = null + contextMenuToplevel = null + hidden = true // Hide dock when context menu closes + } + } + + Rectangle { + id: contextMenuContainer + width: 80 + height: 32 + radius: 8 + color: closeMouseArea.containsMouse ? Colors.mTertiary : Colors.mSurface + border.color: Colors.mOutline + border.width: 1 + + x: { + if (!contextMenuTarget) + return 0 + const pos = contextMenuTarget.mapToItem(null, 0, 0) + let xPos = pos.x + (contextMenuTarget.width - width) / 2 + return Math.max(0, Math.min(xPos, dockWindow.width - width)) + } + + y: { + if (!contextMenuTarget) + return 0 + const pos = contextMenuTarget.mapToItem(null, 0, 0) + return pos.y - height + 32 + } + + Text { + anchors.centerIn: parent + text: "Close" + font.pixelSize: 14 + color: Colors.mOnSurface + } MouseArea { + id: closeMouseArea anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (contextMenuToplevel?.close) + contextMenuToplevel.close() contextMenuVisible = false - contextMenuTarget = null - contextMenuToplevel = null - hidden = true // Hide dock when context menu closes + hidden = true } } - Rectangle { - id: contextMenuContainer - width: 80 - height: 32 - radius: 8 - color: closeMouseArea.containsMouse ? Colors.mTertiary : Colors.mSurface - border.color: Colors.mOutline - border.width: 1 + // Animation + scale: contextMenuVisible ? 1 : 0.9 + opacity: contextMenuVisible ? 1 : 0 + transformOrigin: Item.Bottom - x: { - if (!contextMenuTarget) - return 0 - const pos = contextMenuTarget.mapToItem(null, 0, 0) - let xPos = pos.x + (contextMenuTarget.width - width) / 2 - return Math.max(0, Math.min(xPos, dockWindow.width - width)) + Behavior on scale { + NumberAnimation { + duration: 150 + easing.type: Easing.OutBack } + } - y: { - if (!contextMenuTarget) - return 0 - const pos = contextMenuTarget.mapToItem(null, 0, 0) - return pos.y - height + 32 - } - - Text { - anchors.centerIn: parent - text: "Close" - font.pixelSize: 14 - color: Colors.mOnSurface - } - - MouseArea { - id: closeMouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - - onClicked: { - if (contextMenuToplevel?.close) - contextMenuToplevel.close() - contextMenuVisible = false - hidden = true - } - } - - // Animation - scale: contextMenuVisible ? 1 : 0.9 - opacity: contextMenuVisible ? 1 : 0 - transformOrigin: Item.Bottom - - Behavior on scale { - NumberAnimation { - duration: 150 - easing.type: Easing.OutBack - } - } - - Behavior on opacity { - NumberAnimation { - duration: 100 - } + Behavior on opacity { + NumberAnimation { + duration: 100 } } } diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index 5858e4e..9ce4234 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -16,6 +16,7 @@ Variants { required property ShellScreen modelData readonly property real scaling: Scaling.scale(screen) + screen: modelData // Access the notification model from the service property ListModel notificationModel: NotificationService.notificationModel @@ -23,7 +24,6 @@ Variants { // Track notifications being removed for animation property var removingNotifications: ({}) - screen: modelData color: "transparent" // If no notification display activated in settings, then show them all diff --git a/Modules/Settings/SettingsPanel.qml b/Modules/Settings/SettingsPanel.qml index 6db4056..06ea2df 100644 --- a/Modules/Settings/SettingsPanel.qml +++ b/Modules/Settings/SettingsPanel.qml @@ -31,7 +31,6 @@ NLoader { NPanel { id: panel - readonly property real scaling: Scaling.scale(screen) property int currentTabIndex: 0 // Override hide function to animate first diff --git a/Modules/SidePanel/Cards/ProfileCard.qml b/Modules/SidePanel/Cards/ProfileCard.qml index b1b1bbf..d8921d3 100644 --- a/Modules/SidePanel/Cards/ProfileCard.qml +++ b/Modules/SidePanel/Cards/ProfileCard.qml @@ -13,7 +13,6 @@ import qs.Widgets NBox { id: root - readonly property real scaling: Scaling.scale(screen) property string uptimeText: "--" Layout.fillWidth: true diff --git a/Modules/SidePanel/Cards/SystemMonitorCard.qml b/Modules/SidePanel/Cards/SystemMonitorCard.qml index 90d4345..a4633b1 100644 --- a/Modules/SidePanel/Cards/SystemMonitorCard.qml +++ b/Modules/SidePanel/Cards/SystemMonitorCard.qml @@ -7,8 +7,6 @@ import qs.Widgets NBox { id: root - readonly property real scaling: Scaling.scale(screen) - Layout.preferredWidth: 84 * scaling implicitHeight: content.implicitHeight + Style.marginTiny * 2 * scaling diff --git a/Modules/SidePanel/Cards/WeatherCard.qml b/Modules/SidePanel/Cards/WeatherCard.qml index 9a3a84e..5063108 100644 --- a/Modules/SidePanel/Cards/WeatherCard.qml +++ b/Modules/SidePanel/Cards/WeatherCard.qml @@ -8,7 +8,6 @@ import qs.Widgets NBox { id: root - readonly property real scaling: Scaling.scale(screen) readonly property bool weatherReady: (Location.data.weather !== null) // TBC weatherReady is not turning to false when we reset weather... diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml index e9d89cb..8f187ac 100644 --- a/Modules/SidePanel/SidePanel.qml +++ b/Modules/SidePanel/SidePanel.qml @@ -32,7 +32,6 @@ NLoader { NPanel { id: sidePanel - readonly property real scaling: Scaling.scale(screen) // Single source of truth for spacing between cards (both axes) property real cardSpacing: Style.marginLarge * scaling // X coordinate from the bar to align this panel under diff --git a/Widgets/NBox.qml b/Widgets/NBox.qml index d418638..a29d07d 100644 --- a/Widgets/NBox.qml +++ b/Widgets/NBox.qml @@ -6,8 +6,6 @@ import qs.Services Rectangle { id: root - readonly property real scaling: Scaling.scale(screen) - implicitWidth: childrenRect.width implicitHeight: childrenRect.height diff --git a/Widgets/NBusyIndicator.qml b/Widgets/NBusyIndicator.qml index 7c2b2da..8098fde 100644 --- a/Widgets/NBusyIndicator.qml +++ b/Widgets/NBusyIndicator.qml @@ -4,8 +4,6 @@ import qs.Services Item { id: root - readonly property real scaling: Scaling.scale(screen) - property bool running: true property color color: Colors.mPrimary property int size: Style.baseWidgetSize * scaling diff --git a/Widgets/NCard.qml b/Widgets/NCard.qml index f468091..c173a8a 100644 --- a/Widgets/NCard.qml +++ b/Widgets/NCard.qml @@ -5,8 +5,6 @@ import qs.Services Rectangle { id: root - readonly property real scaling: Scaling.scale(screen) - implicitWidth: childrenRect.width implicitHeight: childrenRect.height diff --git a/Widgets/NCircleStat.qml b/Widgets/NCircleStat.qml index 46fc207..acf7243 100644 --- a/Widgets/NCircleStat.qml +++ b/Widgets/NCircleStat.qml @@ -5,7 +5,6 @@ import qs.Services Rectangle { id: root - readonly property real scaling: Scaling.scale(screen) property real value: 0 // 0..100 (or any range visually mapped) property string icon: "" property string suffix: "%" diff --git a/Widgets/NClock.qml b/Widgets/NClock.qml index c249c01..25ab5c4 100644 --- a/Widgets/NClock.qml +++ b/Widgets/NClock.qml @@ -5,8 +5,6 @@ import qs.Widgets Rectangle { id: root - readonly property real scaling: Scaling.scale(screen) - signal entered signal exited signal clicked diff --git a/Widgets/NComboBox.qml b/Widgets/NComboBox.qml index 0c36869..a89d6bc 100644 --- a/Widgets/NComboBox.qml +++ b/Widgets/NComboBox.qml @@ -7,7 +7,6 @@ import qs.Widgets ColumnLayout { id: root - readonly property real scaling: Scaling.scale(screen) readonly property real preferredHeight: Style.baseWidgetSize * 1.25 * scaling property string label: "" diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml index d3ebf16..933411d 100644 --- a/Widgets/NIconButton.qml +++ b/Widgets/NIconButton.qml @@ -6,7 +6,6 @@ import qs.Services Rectangle { id: root - readonly property real scaling: Scaling.scale(screen) // Multiplier to control how large the button container is relative to Style.baseWidgetSize property real sizeMultiplier: 1.0 property real size: Style.baseWidgetSize * sizeMultiplier * scaling diff --git a/Widgets/NImageRounded.qml b/Widgets/NImageRounded.qml index f1a37d9..6fb2ba8 100644 --- a/Widgets/NImageRounded.qml +++ b/Widgets/NImageRounded.qml @@ -10,7 +10,6 @@ Rectangle { property real imageRadius: width * 0.5 radius: imageRadius - readonly property real scaling: Scaling.scale(screen) property string imagePath: "" property string fallbackIcon: "" property color borderColor: "transparent" diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index a3d76e3..0c6878b 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -7,10 +7,10 @@ PanelWindow { id: root readonly property real scaling: Scaling.scale(screen) + property bool showOverlay: Settings.data.general.dimDesktop property int topMargin: Style.barHeight * scaling property color overlayColor: showOverlay ? Colors.applyOpacity(Colors.mShadow, "AA") : "transparent" - signal dismissed function hide() { @@ -37,7 +37,7 @@ PanelWindow { color: visible ? overlayColor : "transparent" visible: false WlrLayershell.exclusionMode: ExclusionMode.Ignore - screen: (typeof modelData !== 'undefined' ? modelData : null) + anchors.top: true anchors.left: true anchors.right: true diff --git a/Widgets/NPill.qml b/Widgets/NPill.qml index 58bbef3..07b9c70 100644 --- a/Widgets/NPill.qml +++ b/Widgets/NPill.qml @@ -5,8 +5,6 @@ import qs.Services Item { id: root - readonly property real scaling: Scaling.scale(screen) - property string icon: "" property string text: "" property string tooltipText: "" diff --git a/Widgets/NSlider.qml b/Widgets/NSlider.qml index 13d748f..875f988 100644 --- a/Widgets/NSlider.qml +++ b/Widgets/NSlider.qml @@ -6,14 +6,12 @@ import qs.Services Slider { id: root - readonly property real scaling: Scaling.scale(screen) readonly property real knobDiameter: Style.baseWidgetSize * 0.75 * scaling readonly property real trackHeight: knobDiameter * 0.5 readonly property real cutoutExtra: Style.baseWidgetSize * 0.1 * scaling // Optional color to cut the track beneath the knob (should match surrounding background) property var cutoutColor - property var screen property bool snapAlways: true snapMode: snapAlways ? Slider.SnapAlways : Slider.SnapOnRelease diff --git a/Widgets/NText.qml b/Widgets/NText.qml index 7e8ddb3..ca0f1fb 100644 --- a/Widgets/NText.qml +++ b/Widgets/NText.qml @@ -5,8 +5,6 @@ import qs.Widgets Text { id: root - readonly property real scaling: Scaling.scale(screen) - font.family: Settings.data.ui.fontFamily font.pointSize: Style.fontSizeMedium * scaling font.weight: Style.fontWeightRegular diff --git a/Widgets/NTextInput.qml b/Widgets/NTextInput.qml index ccfd832..db61e2d 100644 --- a/Widgets/NTextInput.qml +++ b/Widgets/NTextInput.qml @@ -6,7 +6,6 @@ import qs.Services Item { id: root - readonly property real scaling: Scaling.scale(screen) property string label: "" property string description: "" property bool readOnly: false diff --git a/Widgets/NToggle.qml b/Widgets/NToggle.qml index 8974340..fd42e39 100644 --- a/Widgets/NToggle.qml +++ b/Widgets/NToggle.qml @@ -6,7 +6,6 @@ import qs.Services RowLayout { id: root - readonly property real scaling: Scaling.scale(screen) property string label: "" property string description: "" property bool value: false diff --git a/Widgets/NTooltip.qml b/Widgets/NTooltip.qml index 70d3079..28bfd55 100644 --- a/Widgets/NTooltip.qml +++ b/Widgets/NTooltip.qml @@ -4,7 +4,6 @@ import qs.Services Window { id: root - readonly property real scaling: Scaling.scale(screen) property bool isVisible: false property string text: "Placeholder" property Item target: null From 5bf6659ba73ee3776a829a2602fc38eab75a132a Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 12:14:16 -0400 Subject: [PATCH 313/394] Bar: MediaMini player (Play/Pause + title) --- Modules/Bar/Bar.qml | 8 ++---- Modules/Bar/MediaMini.qml | 36 +++++++++++++++++++++++++++ Modules/SidePanel/Cards/MediaCard.qml | 2 +- 3 files changed, 39 insertions(+), 7 deletions(-) create mode 100644 Modules/Bar/MediaMini.qml diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index cc1b5be..2959a5e 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -52,13 +52,9 @@ Variants { anchors.verticalCenter: parent.verticalCenter spacing: Style.marginSmall * scaling - // Debug show monitor name - // NText { - // text: screen.name - // anchors.verticalCenter: parent.verticalCenter - // font.weight: Style.fontWeightBold - // } SystemMonitor {} + + MediaMini {} } // Center diff --git a/Modules/Bar/MediaMini.qml b/Modules/Bar/MediaMini.qml new file mode 100644 index 0000000..2a54498 --- /dev/null +++ b/Modules/Bar/MediaMini.qml @@ -0,0 +1,36 @@ +import QtQuick +import QtQuick.Layouts +import qs.Services +import qs.Widgets + +Item { + id: root + + width: visible ? mediaRow.width : 0 + height: Style.barHeight * scaling + visible: Settings.data.bar.showMedia && (MediaPlayer.canPlay || MediaPlayer.canPause) + + RowLayout { + id: mediaRow + height: parent.height + spacing: Style.spacingTiniest * scaling + + NIconButton { + icon: MediaPlayer.isPlaying ? "pause" : "play_arrow" + tooltipText: "Play/pause media" + sizeMultiplier: 0.8 + showBorder: false + onClicked: MediaPlayer.playPause() + } + + // Track info + NText { + text: MediaPlayer.trackTitle + " - " + MediaPlayer.trackArtist + color: Colors.mOnSurface + font.pointSize: Style.fontSizeSmall * scaling + elide: Text.ElideRight + Layout.maximumWidth: 200 * scaling + Layout.alignment: Qt.AlignVCenter + } + } +} diff --git a/Modules/SidePanel/Cards/MediaCard.qml b/Modules/SidePanel/Cards/MediaCard.qml index 9437c62..f2bd12e 100644 --- a/Modules/SidePanel/Cards/MediaCard.qml +++ b/Modules/SidePanel/Cards/MediaCard.qml @@ -172,7 +172,7 @@ NBox { anchors.fill: parent anchors.margins: Style.marginTiny * scaling imagePath: MediaPlayer.trackArtUrl - fallbackIcon: "image" + fallbackIcon: "music_note" borderColor: Colors.mOutline borderWidth: Math.max(1, Style.borderThin * scaling) imageRadius: width * 0.5 From 8ee5c3454322a37ea25e1c1afee495eee429b822 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 13:39:23 -0400 Subject: [PATCH 314/394] Settings: removed showActiveWindowIcon --- Modules/Settings/Tabs/BarTab.qml | 9 --------- Services/Settings.qml | 1 - 2 files changed, 10 deletions(-) diff --git a/Modules/Settings/Tabs/BarTab.qml b/Modules/Settings/Tabs/BarTab.qml index cd49a77..56b8e02 100644 --- a/Modules/Settings/Tabs/BarTab.qml +++ b/Modules/Settings/Tabs/BarTab.qml @@ -48,15 +48,6 @@ ColumnLayout { } } - NToggle { - label: "Show Active Window Icon" - description: "Display the icon of the currently focused window" - value: Settings.data.bar.showActiveWindowIcon - onToggled: function (newValue) { - Settings.data.bar.showActiveWindowIcon = newValue - } - } - NToggle { label: "Show System Info" description: "Display system information (CPU, RAM, Temperature)" diff --git a/Services/Settings.qml b/Services/Settings.qml index 5295302..c2f77af 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -72,7 +72,6 @@ Singleton { bar: JsonObject { property bool showActiveWindow: true - property bool showActiveWindowIcon: false property bool showSystemInfo: false property bool showMedia: false property bool showTaskbar: false From f80e6a02947e05ff9f1ad9a4eac876a4db7e3f75 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 14:35:54 -0400 Subject: [PATCH 315/394] Improved the dock, still needs some love --- Modules/Dock/Dock.qml | 67 ++++++++++++++++++++++--------------------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml index a22e490..83263b3 100644 --- a/Modules/Dock/Dock.qml +++ b/Modules/Dock/Dock.qml @@ -30,6 +30,7 @@ NLoader { property int showAnimationDuration: 150 property int peekHeight: 2 property int fullHeight: dockContainer.height + property int iconSize: 48 // Track hover state property bool dockHovered: false @@ -44,19 +45,24 @@ NLoader { visible: modelData ? Settings.data.dock.monitors.includes(modelData.name) : false exclusionMode: ExclusionMode.Ignore + anchors.bottom: true anchors.left: true anchors.right: true focusable: false color: "transparent" - implicitHeight: 60 + implicitHeight: iconSize * 1.5 * scaling // Timer for auto-hide delay Timer { id: hideTimer interval: hideDelay - onTriggered: if (autoHide && !dockHovered && !anyAppHovered) - hidden = true + onTriggered: { + if (autoHide && !dockHovered && !anyAppHovered) { + hidden = true + } + } + } // Timer for show delay @@ -80,7 +86,7 @@ NLoader { anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom - height: 10 + height: 10 * scaling hoverEnabled: true propagateComposedEvents: true @@ -104,13 +110,13 @@ NLoader { Rectangle { id: dockContainer - width: dock.width + 40 - height: 50 - color: Colors.mSurface + width: dock.width + 48 * scaling + height: iconSize * 1.5 * scaling + color: Colors.mSurfaceVariant anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom - topLeftRadius: 20 - topRightRadius: 20 + topLeftRadius: Style.radiusLarge * scaling + topRightRadius: Style.radiusLarge * scaling MouseArea { id: dockMouseArea @@ -136,7 +142,7 @@ NLoader { Item { id: dock width: runningAppsRow.width - height: parent.height - 10 + height: parent.height - (20 * scaling) anchors.centerIn: parent NTooltip { @@ -160,7 +166,7 @@ NLoader { Row { id: runningAppsRow - spacing: 8 + spacing: Style.marginLarge * scaling height: parent.height anchors.centerIn: parent @@ -169,9 +175,8 @@ NLoader { delegate: Rectangle { id: appButton - width: 36 - height: 36 - radius: 18 + width: iconSize * scaling + height: iconSize * scaling color: "transparent" property bool isActive: ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData @@ -179,32 +184,28 @@ NLoader { property string appId: modelData ? modelData.appId : "" property string appTitle: modelData ? modelData.title : "" - Behavior on color { - ColorAnimation { - duration: 150 - } - } - + // The icon Image { id: appIcon - width: 28 - height: 28 + width: iconSize * scaling + height: iconSize * scaling anchors.centerIn: parent source: dock.getAppIcon(modelData) visible: source.toString() !== "" - smooth: false + smooth: true mipmap: false antialiasing: false fillMode: Image.PreserveAspectFit } - Text { + // Fall back if no icon + NText { anchors.centerIn: parent visible: !appIcon.visible - text: appButton.appId ? appButton.appId.charAt(0).toUpperCase() : "?" - font.pixelSize: 14 - font.bold: true - color: appButton.isActive ? Colors.mPrimary : Colors.mOnSurface + text: "question_mark" + font.family: "Material Symbols Rounded" + font.pointSize: iconSize * 0.7 * scaling + color: appButton.isActive ? Colors.mPrimary : Colors.mOnSurfaceVariant } MouseArea { @@ -252,13 +253,13 @@ NLoader { Rectangle { visible: isActive - width: 20 - height: 3 + width: iconSize * 0.75 + height: 4 * scaling color: Colors.mPrimary - radius: 1.5 - anchors.bottom: parent.bottom + radius: Style.radiusTiny + anchors.top: parent.bottom anchors.horizontalCenter: parent.horizontalCenter - anchors.bottomMargin: 2 + anchors.topMargin: 2* scaling } } } From a8e7f6e01d57704d048cb50bcb7082d5544fb051 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 14:48:33 -0400 Subject: [PATCH 316/394] ScreenCorners: fix when bar is not visible on a screen --- Modules/Background/ScreenCorners.qml | 3 ++- Modules/Dock/Dock.qml | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Modules/Background/ScreenCorners.qml b/Modules/Background/ScreenCorners.qml index 59e88de..8dbb353 100644 --- a/Modules/Background/ScreenCorners.qml +++ b/Modules/Background/ScreenCorners.qml @@ -41,7 +41,8 @@ NLoader { } margins { - top: Math.floor(Style.barHeight * scaling) + top: (Settings.data.bar.monitors.includes(modelData.name) + || (Settings.data.bar.monitors.length === 0)) ? Math.floor(Style.barHeight * scaling) : 0 } // Source we want to show only as a ring diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml index 83263b3..4ae7b3b 100644 --- a/Modules/Dock/Dock.qml +++ b/Modules/Dock/Dock.qml @@ -62,7 +62,6 @@ NLoader { hidden = true } } - } // Timer for show delay @@ -188,7 +187,7 @@ NLoader { Image { id: appIcon width: iconSize * scaling - height: iconSize * scaling + height: iconSize * scaling anchors.centerIn: parent source: dock.getAppIcon(modelData) visible: source.toString() !== "" @@ -259,7 +258,7 @@ NLoader { radius: Style.radiusTiny anchors.top: parent.bottom anchors.horizontalCenter: parent.horizontalCenter - anchors.topMargin: 2* scaling + anchors.topMargin: 2 * scaling } } } From f5b4cb452bf92b1d187171546422890d7642b1bd Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 15:37:00 -0400 Subject: [PATCH 317/394] Predefined ColorSchemes - added support for predefined color schemes - moved all the code regarding colors generation from wallpaper to the ColorSchemes service --- Assets/ColorSchemes/Nord.json | 19 ++++++++++++ Assets/ColorSchemes/Rosepine.json | 19 ++++++++++++ Assets/ColorSchemes/rosepine.json | 26 ---------------- Modules/Settings/Tabs/ColorSchemeTab.qml | 28 +++++++++++++++-- Modules/Settings/Tabs/WallpaperTab.qml | 10 ------ Services/ColorSchemes.qml | 39 ++++++++++++++++++++++-- Services/Colors.qml | 38 +++++++++++------------ Services/Settings.qml | 8 ++++- Services/Wallpapers.qml | 33 ++------------------ 9 files changed, 126 insertions(+), 94 deletions(-) create mode 100644 Assets/ColorSchemes/Nord.json create mode 100644 Assets/ColorSchemes/Rosepine.json delete mode 100644 Assets/ColorSchemes/rosepine.json diff --git a/Assets/ColorSchemes/Nord.json b/Assets/ColorSchemes/Nord.json new file mode 100644 index 0000000..a0dc572 --- /dev/null +++ b/Assets/ColorSchemes/Nord.json @@ -0,0 +1,19 @@ +{ + "mPrimary": "#8fbcbb", + "mOnPrimary": "#2e3440", + "mSecondary": "#88c0d0", + "mOnSecondary": "#2e3440", + "mTertiary": "#5e81ac", + "mOnTertiary": "#2e3440", + + "mError": "#bf616a", + "mOnError": "#2e3440", + + "mSurface": "#2e3440", + "mOnSurface": "#d8dee9", + "mSurfaceVariant": "#3b4252", + "mOnSurfaceVariant": "#e5e9f0", + "mOutline": "#434c5e", + "mOutlineVariant": "#4c566a", + "mShadow": "#2e3440" +} diff --git a/Assets/ColorSchemes/Rosepine.json b/Assets/ColorSchemes/Rosepine.json new file mode 100644 index 0000000..cd7a688 --- /dev/null +++ b/Assets/ColorSchemes/Rosepine.json @@ -0,0 +1,19 @@ +{ + "mPrimary": "#ebbcba", + "mOnPrimary": "#191724", + "mSecondary": "#31748f", + "mOnSecondary": "#e0def4", + "mTertiary": "#9ccfd8", + "mOnTertiary": "#191724", + + "mError": "#eb6f92", + "mOnError": "#191724", + + "mSurface": "#191724", + "mOnSurface": "#e0def4", + "mSurfaceVariant": "#26233a", + "mOnSurfaceVariant": "#908caa", + "mOutline": "#44415a", + "mOutlineVariant": "#514e6c", + "mShadow": "#191724" +} diff --git a/Assets/ColorSchemes/rosepine.json b/Assets/ColorSchemes/rosepine.json deleted file mode 100644 index 1b00997..0000000 --- a/Assets/ColorSchemes/rosepine.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "backgroundPrimary": "#191724", - "backgroundSecondary": "#1f1d2e", - "backgroundTertiary": "#26233a", - - "surface": "#1b1927", - "surfaceVariant": "#262337", - - "textPrimary": "#e0def4", - "textSecondary": "#908caa", - "textDisabled": "#6e6a86", - - "accentPrimary": "#ebbcba", - "accentSecondary": "#31748f", - "accentTertiary": "#9ccfd8", - - "error": "#eb6f92", - "warning": "#f6c177", - - "hover": "#c4a7e7", - "onAccent": "#191724", - "outline": "#44415a", - - "shadow": "#191724", - "overlay": "#191724" -} \ No newline at end of file diff --git a/Modules/Settings/Tabs/ColorSchemeTab.qml b/Modules/Settings/Tabs/ColorSchemeTab.qml index 72ba676..dcdf589 100644 --- a/Modules/Settings/Tabs/ColorSchemeTab.qml +++ b/Modules/Settings/Tabs/ColorSchemeTab.qml @@ -39,6 +39,19 @@ ColumnLayout { color: Colors.mOnSurface } + // Use Wallpaper Colors + NToggle { + label: "Use Wallpaper Colors" + description: "Automatically generate colors from you active wallpaper (requires Matugen)" + value: Settings.data.colorSchemes.useWallpaperColors + onToggled: function (newValue) { + Settings.data.colorSchemes.useWallpaperColors = newValue + if (Settings.data.colorSchemes.useWallpaperColors) { + ColorSchemes.changedWallpaper() + } + } + } + ButtonGroup { id: schemesGroup } @@ -48,9 +61,18 @@ ColumnLayout { NRadioButton { property string schemePath: modelData ButtonGroup.group: schemesGroup - //checked: Audio.sink?.id === modelData.id - //onClicked: Audio.setAudioSink(modelData) - text: schemePath + text: { + // Remove json and the full path + var chunks = schemePath.replace(".json", "").split("/") + return chunks[chunks.length - 1] + } + checked: Settings.data.colorSchemes.predefinedScheme == schemePath + onClicked: { + // Disable useWallpaperColors when picking a predefined color scheme + Settings.data.colorSchemes.useWallpaperColors = false + Settings.data.colorSchemes.predefinedScheme = schemePath + ColorSchemes.applyScheme(schemePath) + } } } } diff --git a/Modules/Settings/Tabs/WallpaperTab.qml b/Modules/Settings/Tabs/WallpaperTab.qml index 41288d9..50e6377 100644 --- a/Modules/Settings/Tabs/WallpaperTab.qml +++ b/Modules/Settings/Tabs/WallpaperTab.qml @@ -92,16 +92,6 @@ ColumnLayout { } } - // Use Wallpaper Theme - NToggle { - label: "Use Wallpaper Colors" - description: "Automatically adjust UI colors based on wallpaper using Matugen" - value: Settings.data.wallpaper.generateColors - onToggled: function (newValue) { - Settings.data.wallpaper.generateColors = newValue - } - } - // Interval ColumnLayout { RowLayout { diff --git a/Services/ColorSchemes.qml b/Services/ColorSchemes.qml index 2075510..eddd275 100644 --- a/Services/ColorSchemes.qml +++ b/Services/ColorSchemes.qml @@ -15,6 +15,8 @@ Singleton { property var schemes: [] property bool scanning: false + property string schemesDirectory: Quickshell.shellDir + "/Assets/ColorSchemes" + property string colorsJsonFilePath: Settings.configDir + "colors.json" function loadColorSchemes() { console.log("[ColorSchemes] Load ColorSchemes") @@ -22,7 +24,20 @@ Singleton { schemes = [] // Unsetting, then setting the folder will re-trigger the parsing! folderModel.folder = "" - folderModel.folder = "file://" + Quickshell.shellDir + "/Assets/ColorSchemes" + folderModel.folder = "file://" + schemesDirectory + } + + function applyScheme(filePath) { + Quickshell.execDetached(["cp", filePath, colorsJsonFilePath]) + } + + function changedWallpaper() { + if (Settings.data.colorSchemes.useWallpaperColors) { + console.log("[ColorSchemes] Starting color generation process") + generateColorsProcess.running = true + // Invalidate potential predefined scheme + Settings.data.colorSchemes.predefinedScheme = "" + } } FolderListModel { @@ -34,12 +49,30 @@ Singleton { if (status === FolderListModel.Ready) { var files = [] for (var i = 0; i < count; i++) { - var filepath = folderModel.folder + "/" + get(i, "fileName") + var filepath = schemesDirectory + "/" + get(i, "fileName") files.push(filepath) } schemes = files scanning = false - console.log(schemes) + } + } + } + + Process { + id: generateColorsProcess + command: ["matugen", "image", Wallpapers.currentWallpaper, "--config", Quickshell.shellDir + "/Assets/Matugen/matugen.toml"] + workingDirectory: Quickshell.shellDir + running: false + stdout: StdioCollector { + onStreamFinished: { + console.log("[ColorSchemes] Generated colors from wallpaper") + } + } + stderr: StdioCollector { + onStreamFinished: { + if (this.text !== "") { + console.error(this.text) + } } } } diff --git a/Services/Colors.qml b/Services/Colors.qml index d77af2d..b866ff1 100644 --- a/Services/Colors.qml +++ b/Services/Colors.qml @@ -13,29 +13,25 @@ Singleton { id: root // --- Key Colors: These are the main accent colors that define your app's style - property color mPrimary: useCustom ? customColors.mPrimary : defaultColors.mPrimary - property color mOnPrimary: useCustom ? customColors.mOnPrimary : defaultColors.mOnPrimary - property color mSecondary: useCustom ? customColors.mSecondary : defaultColors.mSecondary - property color mOnSecondary: useCustom ? customColors.mOnSecondary : defaultColors.mOnSecondary - property color mTertiary: useCustom ? customColors.mTertiary : defaultColors.mTertiary - property color mOnTertiary: useCustom ? customColors.mOnTertiary : defaultColors.mOnTertiary + property color mPrimary: customColors.mPrimary + property color mOnPrimary: customColors.mOnPrimary + property color mSecondary: customColors.mSecondary + property color mOnSecondary: customColors.mOnSecondary + property color mTertiary: customColors.mTertiary + property color mOnTertiary: customColors.mOnTertiary // --- Utility Colors: These colors serve specific, universal purposes like indicating errors - property color mError: useCustom ? customColors.mError : defaultColors.mError - property color mOnError: useCustom ? customColors.mOnError : defaultColors.mOnError + property color mError: customColors.mError + property color mOnError: customColors.mOnError // --- Surface and Variant Colors: These provide additional options for surfaces and their contents, creating visual hierarchy - property color mSurface: useCustom ? customColors.mSurface : defaultColors.mSurface - property color mOnSurface: useCustom ? customColors.mOnSurface : defaultColors.mOnSurface - property color mSurfaceVariant: useCustom ? customColors.mSurfaceVariant : defaultColors.mSurfaceVariant - property color mOnSurfaceVariant: useCustom ? customColors.mOnSurfaceVariant : defaultColors.mOnSurfaceVariant - property color mOutline: useCustom ? customColors.mOutline : defaultColors.mOutline - property color mOutlineVariant: useCustom ? customColors.mOutlineVariant : defaultColors.mOutlineVariant - property color mShadow: useCustom ? customColors.mShadow : defaultColors.mShadow - - // ----------- - // Check if we should use custom colors - property bool useCustom: (Settings.data.wallpaper.generateColors && customColorsFile.loaded) + property color mSurface: customColors.mSurface + property color mOnSurface: customColors.mOnSurface + property color mSurfaceVariant: customColors.mSurfaceVariant + property color mOnSurfaceVariant: customColors.mOnSurfaceVariant + property color mOutline: customColors.mOutline + property color mOutlineVariant: customColors.mOutlineVariant + property color mShadow: customColors.mShadow // ----------- function applyOpacity(color, opacity) { @@ -98,11 +94,11 @@ Singleton { path: Settings.configDir + "colors.json" watchChanges: true onFileChanged: { - console.log("[Colors] reloading colors file from disk") + console.log("[Colors] Reloading colors from disk") reload() } onAdapterUpdated: { - console.log("[Colors] writing colors to disk, primary color:", mPrimary) + console.log("[Colors] Writing colors to disk") writeAdapter() } onLoadFailed: function (error) { diff --git a/Services/Settings.qml b/Services/Settings.qml index c2f77af..d982699 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -119,7 +119,6 @@ Singleton { property string current: "" property bool isRandom: false property int randomInterval: 300 - property bool generateColors: false property JsonObject swww onDirectoryChanged: Wallpapers.loadWallpapers() @@ -190,6 +189,13 @@ Singleton { property list monitorBrightness: [] property int brightnessStep: 5 } + + property JsonObject colorSchemes + + colorSchemes: JsonObject { + property bool useWallpaperColors: false + property string predefinedScheme: "" + } } } } diff --git a/Services/Wallpapers.qml b/Services/Wallpapers.qml index 301ab50..448df02 100644 --- a/Services/Wallpapers.qml +++ b/Services/Wallpapers.qml @@ -39,7 +39,7 @@ Singleton { } function setCurrentWallpaper(path, isInitial) { - // Only generate colors if the wallpaper actually changed + // Only regenerate colors if the wallpaper actually changed var wallpaperChanged = currentWallpaper !== path currentWallpaper = path @@ -64,9 +64,9 @@ Singleton { randomWallpaperTimer.restart() } - // Only generate colors if the wallpaper actually changed + // Only notify ColorSchemes service if the wallpaper actually changed if (wallpaperChanged) { - generateColors() + ColorSchemes.changedWallpaper() } } @@ -95,14 +95,6 @@ Singleton { } } - function generateColors() { - console.log("[Wallpapers] generateColors() called, generateColors setting:", Settings.data.wallpaper.generateColors) - if (Settings.data.wallpaper.generateColors) { - console.log("[Wallpapers] Starting color generation process") - generateThemeProcess.running = true - } - } - function startSWWWDaemon() { if (Settings.data.wallpaper.swww.enabled) { console.log("[SWWW] Requesting swww-daemon") @@ -160,25 +152,6 @@ Singleton { } } - Process { - id: generateThemeProcess - command: ["matugen", "image", currentWallpaper, "--config", Quickshell.shellDir + "/Assets/Matugen/matugen.toml"] - workingDirectory: Quickshell.shellDir - running: false - stdout: StdioCollector { - onStreamFinished: { - console.log("[Wallpapers] generated colors from image") - } - } - stderr: StdioCollector { - onStreamFinished: { - if (this.text !== "") { - console.error(this.text) - } - } - } - } - Process { id: startDaemonProcess command: ["swww-daemon", "--format", "xrgb"] From fd890f4293243d3888225ee7e147ca17222edadc Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 15:48:09 -0400 Subject: [PATCH 318/394] NCircleStat: fixed scaling I broke earlier --- Widgets/NCircleStat.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Widgets/NCircleStat.qml b/Widgets/NCircleStat.qml index acf7243..c5c0672 100644 --- a/Widgets/NCircleStat.qml +++ b/Widgets/NCircleStat.qml @@ -49,13 +49,13 @@ Rectangle { const ctx = getContext("2d") const w = width, h = height const cx = w / 2, cy = h / 2 - const r = Math.min(w, h) / 2 - 5 * root.scaling * contentScale + const r = Math.min(w, h) / 2 - 5 * scaling * contentScale // 240° arc with a 120° gap centered on the right side // Start at 60° and end at 300° → balanced right-side opening const start = Math.PI / 3 const endBg = Math.PI * 5 / 3 ctx.reset() - ctx.lineWidth = 6 * root.scaling * contentScale + ctx.lineWidth = 6 * scaling * contentScale // Track uses surfaceVariant for stronger contrast ctx.strokeStyle = Colors.mSurface ctx.beginPath() From 9298f37b38591159e0d37a0180238d3ed8b11aa1 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 15:51:11 -0400 Subject: [PATCH 319/394] LinearSpectrum: minimum 1px height --- Modules/Audio/LinearSpectrum.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/Audio/LinearSpectrum.qml b/Modules/Audio/LinearSpectrum.qml index 116dd7e..93f8e41 100644 --- a/Modules/Audio/LinearSpectrum.qml +++ b/Modules/Audio/LinearSpectrum.qml @@ -21,7 +21,7 @@ Item { antialiasing: true width: xScale * 0.5 - height: root.height * amp + height: Math.max(1, root.height * amp) x: index * xScale y: root.height - height } @@ -38,7 +38,7 @@ Item { antialiasing: true width: xScale * 0.5 - height: root.height * amp + height: Math.max(1, root.height * amp) x: (values.length + index) * xScale y: root.height - height } From 48b6c1cf5c1537d575174cf1920e2055b5d811fc Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 16:09:20 -0400 Subject: [PATCH 320/394] Color Schemes: added Catppucin, Dracula, Gruvbox --- Assets/ColorSchemes/Catppuccin.json | 19 +++++++++++++++++++ Assets/ColorSchemes/Dracula.json | 19 +++++++++++++++++++ Assets/ColorSchemes/Gruvbox.json | 19 +++++++++++++++++++ ...{Rosepine.json => Rosepine (default).json} | 0 Modules/Audio/CircularSpectrum.qml | 1 + Modules/Settings/Tabs/ColorSchemeTab.qml | 7 ------- 6 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 Assets/ColorSchemes/Catppuccin.json create mode 100644 Assets/ColorSchemes/Dracula.json create mode 100644 Assets/ColorSchemes/Gruvbox.json rename Assets/ColorSchemes/{Rosepine.json => Rosepine (default).json} (100%) diff --git a/Assets/ColorSchemes/Catppuccin.json b/Assets/ColorSchemes/Catppuccin.json new file mode 100644 index 0000000..aebe0e3 --- /dev/null +++ b/Assets/ColorSchemes/Catppuccin.json @@ -0,0 +1,19 @@ +{ + "mPrimary": "#cba6f7", + "mOnPrimary": "#11111b", + "mSecondary": "#fab387", + "mOnSecondary": "#11111b", + "mTertiary": "#a6e3a1", + "mOnTertiary": "#11111b", + + "mError": "#f38ba8", + "mOnError": "#11111b", + + "mSurface": "#1e1e2e", + "mOnSurface": "##cdd6f4", + "mSurfaceVariant": "#313244", + "mOnSurfaceVariant": "#a3b4eb", + "mOutline": "#45475a", + "mOutlineVariant": "#585b70", + "mShadow": "#11111b" +} diff --git a/Assets/ColorSchemes/Dracula.json b/Assets/ColorSchemes/Dracula.json new file mode 100644 index 0000000..5e8aa9d --- /dev/null +++ b/Assets/ColorSchemes/Dracula.json @@ -0,0 +1,19 @@ +{ + "mPrimary": "#bd93f9", + "mOnPrimary": "#282A36", + "mSecondary": "#ff79c6", + "mOnSecondary": "#4e1d32", + "mTertiary": "#8be9fd", + "mOnTertiary": "#003543", + + "mError": "#FF5555", + "mOnError": "#282A36", + + "mSurface": "#282A36", + "mOnSurface": "#F8F8F2", + "mSurfaceVariant": "#44475A", + "mOnSurfaceVariant": "#d6d8e0", + "mOutline": "#6272A4", + "mOutlineVariant": "#4c566a", + "mShadow": "#282A36" +} diff --git a/Assets/ColorSchemes/Gruvbox.json b/Assets/ColorSchemes/Gruvbox.json new file mode 100644 index 0000000..519884e --- /dev/null +++ b/Assets/ColorSchemes/Gruvbox.json @@ -0,0 +1,19 @@ +{ + "mPrimary": "#b8bb26", + "mOnPrimary": "#282828", + "mSecondary": "#fabd2f", + "mOnSecondary": "#282828", + "mTertiary": "#83a598", + "mOnTertiary": "#282828", + + "mError": "#fb4934", + "mOnError": "#282828", + + "mSurface": "#282828", + "mOnSurface": "#fbf1c7", + "mSurfaceVariant": "#3c3836", + "mOnSurfaceVariant": "#ebdbb2", + "mOutline": "#928374", + "mOutlineVariant": "#bfb6ad", + "mShadow": "#282828" +} diff --git a/Assets/ColorSchemes/Rosepine.json b/Assets/ColorSchemes/Rosepine (default).json similarity index 100% rename from Assets/ColorSchemes/Rosepine.json rename to Assets/ColorSchemes/Rosepine (default).json diff --git a/Modules/Audio/CircularSpectrum.qml b/Modules/Audio/CircularSpectrum.qml index c3fa7e7..a58079a 100644 --- a/Modules/Audio/CircularSpectrum.qml +++ b/Modules/Audio/CircularSpectrum.qml @@ -1,6 +1,7 @@ import QtQuick import qs.Services +// Not used ATM and need rework Item { id: root property int innerRadius: 32 * scaling diff --git a/Modules/Settings/Tabs/ColorSchemeTab.qml b/Modules/Settings/Tabs/ColorSchemeTab.qml index dcdf589..5e32b8f 100644 --- a/Modules/Settings/Tabs/ColorSchemeTab.qml +++ b/Modules/Settings/Tabs/ColorSchemeTab.qml @@ -32,13 +32,6 @@ ColumnLayout { spacing: Style.marginLarge * scaling Layout.fillWidth: true - NText { - text: "TODO" - font.pointSize: Style.fontSizeXL * scaling - font.weight: Style.fontWeightBold - color: Colors.mOnSurface - } - // Use Wallpaper Colors NToggle { label: "Use Wallpaper Colors" From 67f568dac14e3eef4d36bb21debeb0dcff40e798 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 16:29:14 -0400 Subject: [PATCH 321/394] Better color schemes --- Assets/ColorSchemes/Catppuccin.json | 2 +- Assets/ColorSchemes/Dracula.json | 4 +- Assets/ColorSchemes/Gruvbox.json | 4 +- Assets/ColorSchemes/Nord.json | 2 +- Assets/ColorSchemes/Rosepine (default).json | 8 ++-- Assets/ColorSchemes/Tokyo Night.json | 19 +++++++++ Modules/Settings/Tabs/ColorSchemeTab.qml | 43 ++++++++++++--------- Services/Colors.qml | 6 +-- 8 files changed, 56 insertions(+), 32 deletions(-) create mode 100644 Assets/ColorSchemes/Tokyo Night.json diff --git a/Assets/ColorSchemes/Catppuccin.json b/Assets/ColorSchemes/Catppuccin.json index aebe0e3..ac63b64 100644 --- a/Assets/ColorSchemes/Catppuccin.json +++ b/Assets/ColorSchemes/Catppuccin.json @@ -14,6 +14,6 @@ "mSurfaceVariant": "#313244", "mOnSurfaceVariant": "#a3b4eb", "mOutline": "#45475a", - "mOutlineVariant": "#585b70", + "mOutlineVariant": "#2f303d", "mShadow": "#11111b" } diff --git a/Assets/ColorSchemes/Dracula.json b/Assets/ColorSchemes/Dracula.json index 5e8aa9d..deb173c 100644 --- a/Assets/ColorSchemes/Dracula.json +++ b/Assets/ColorSchemes/Dracula.json @@ -13,7 +13,7 @@ "mOnSurface": "#F8F8F2", "mSurfaceVariant": "#44475A", "mOnSurfaceVariant": "#d6d8e0", - "mOutline": "#6272A4", - "mOutlineVariant": "#4c566a", + "mOutline": "#4d5c86", + "mOutlineVariant": "#3a4666", "mShadow": "#282A36" } diff --git a/Assets/ColorSchemes/Gruvbox.json b/Assets/ColorSchemes/Gruvbox.json index 519884e..654223d 100644 --- a/Assets/ColorSchemes/Gruvbox.json +++ b/Assets/ColorSchemes/Gruvbox.json @@ -13,7 +13,7 @@ "mOnSurface": "#fbf1c7", "mSurfaceVariant": "#3c3836", "mOnSurfaceVariant": "#ebdbb2", - "mOutline": "#928374", - "mOutlineVariant": "#bfb6ad", + "mOutline": "#665c54", + "mOutlineVariant": "#3c3836", "mShadow": "#282828" } diff --git a/Assets/ColorSchemes/Nord.json b/Assets/ColorSchemes/Nord.json index a0dc572..1335d43 100644 --- a/Assets/ColorSchemes/Nord.json +++ b/Assets/ColorSchemes/Nord.json @@ -14,6 +14,6 @@ "mSurfaceVariant": "#3b4252", "mOnSurfaceVariant": "#e5e9f0", "mOutline": "#434c5e", - "mOutlineVariant": "#4c566a", + "mOutlineVariant": "#2e3440", "mShadow": "#2e3440" } diff --git a/Assets/ColorSchemes/Rosepine (default).json b/Assets/ColorSchemes/Rosepine (default).json index cd7a688..6712840 100644 --- a/Assets/ColorSchemes/Rosepine (default).json +++ b/Assets/ColorSchemes/Rosepine (default).json @@ -1,19 +1,19 @@ { "mPrimary": "#ebbcba", - "mOnPrimary": "#191724", + "mOnPrimary": "#1f1d2e", "mSecondary": "#31748f", "mOnSecondary": "#e0def4", "mTertiary": "#9ccfd8", "mOnTertiary": "#191724", "mError": "#eb6f92", - "mOnError": "#191724", + "mOnError": "#1f1d2e", - "mSurface": "#191724", + "mSurface": "#1f1d2e", "mOnSurface": "#e0def4", "mSurfaceVariant": "#26233a", "mOnSurfaceVariant": "#908caa", "mOutline": "#44415a", - "mOutlineVariant": "#514e6c", + "mOutlineVariant": "#2e2c3c", "mShadow": "#191724" } diff --git a/Assets/ColorSchemes/Tokyo Night.json b/Assets/ColorSchemes/Tokyo Night.json new file mode 100644 index 0000000..49a4d74 --- /dev/null +++ b/Assets/ColorSchemes/Tokyo Night.json @@ -0,0 +1,19 @@ +{ + "mPrimary": "#ff9e64", + "mOnPrimary": "#1a1b26", + "mSecondary": "#ff4499", + "mOnSecondary": "#1a1b26", + "mTertiary": "#7aa2f7", + "mOnTertiary": "#1a1b26", + + "mError": "#f7768e", + "mOnError": "#1f1d2e", + + "mSurface": "#1a1b26", + "mOnSurface": "#a9b1d6", + "mSurfaceVariant": "#292e42", + "mOnSurfaceVariant": "#787c99", + "mOutline": "#3b4261", + "mOutlineVariant": "#282c41", + "mShadow": "#1a1b26" +} diff --git a/Modules/Settings/Tabs/ColorSchemeTab.qml b/Modules/Settings/Tabs/ColorSchemeTab.qml index 5e32b8f..8e6b895 100644 --- a/Modules/Settings/Tabs/ColorSchemeTab.qml +++ b/Modules/Settings/Tabs/ColorSchemeTab.qml @@ -45,26 +45,31 @@ ColumnLayout { } } - ButtonGroup { - id: schemesGroup - } + ColumnLayout { + spacing: Style.marginTiny * scaling + Layout.fillWidth: true - Repeater { - model: ColorSchemes.schemes - NRadioButton { - property string schemePath: modelData - ButtonGroup.group: schemesGroup - text: { - // Remove json and the full path - var chunks = schemePath.replace(".json", "").split("/") - return chunks[chunks.length - 1] - } - checked: Settings.data.colorSchemes.predefinedScheme == schemePath - onClicked: { - // Disable useWallpaperColors when picking a predefined color scheme - Settings.data.colorSchemes.useWallpaperColors = false - Settings.data.colorSchemes.predefinedScheme = schemePath - ColorSchemes.applyScheme(schemePath) + ButtonGroup { + id: schemesGroup + } + + Repeater { + model: ColorSchemes.schemes + NRadioButton { + property string schemePath: modelData + ButtonGroup.group: schemesGroup + text: { + // Remove json and the full path + var chunks = schemePath.replace(".json", "").split("/") + return chunks[chunks.length - 1] + } + checked: Settings.data.colorSchemes.predefinedScheme == schemePath + onClicked: { + // Disable useWallpaperColors when picking a predefined color scheme + Settings.data.colorSchemes.useWallpaperColors = false + Settings.data.colorSchemes.predefinedScheme = schemePath + ColorSchemes.applyScheme(schemePath) + } } } } diff --git a/Services/Colors.qml b/Services/Colors.qml index b866ff1..2a20655 100644 --- a/Services/Colors.qml +++ b/Services/Colors.qml @@ -45,16 +45,16 @@ Singleton { id: defaultColors property color mPrimary: "#ebbcba" - property color mOnPrimary: "#191724" + property color mOnPrimary: "#1f1d2e" property color mSecondary: "#31748f" property color mOnSecondary: "#e0def4" property color mTertiary: "#9ccfd8" property color mOnTertiary: "#191724" property color mError: "#eb6f92" - property color mOnError: "#191724" + property color mOnError: "#1f1d2e" - property color mSurface: "#191724" + property color mSurface: "#1f1d2e" property color mOnSurface: "#e0def4" property color mSurfaceVariant: "#26233a" property color mOnSurfaceVariant: "#908caa" From 52891d1fb4a7c7a9024e0a957b72e283f1e133ad Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 16:41:33 -0400 Subject: [PATCH 322/394] Solarized theme --- Assets/ColorSchemes/Solarized.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 Assets/ColorSchemes/Solarized.json diff --git a/Assets/ColorSchemes/Solarized.json b/Assets/ColorSchemes/Solarized.json new file mode 100644 index 0000000..d81daf4 --- /dev/null +++ b/Assets/ColorSchemes/Solarized.json @@ -0,0 +1,19 @@ +{ + "mPrimary": "#b58900", + "mOnPrimary": "#002b36", + "mSecondary": "#d33682", + "mOnSecondary": "#002b36", + "mTertiary": "#cb4b16", + "mOnTertiary": "#002b36", + + "mError": "#dc322f", + "mOnError": "#002b36", + + "mSurface": "#002b36", + "mOnSurface": "#839496", + "mSurfaceVariant": "#073642", + "mOnSurfaceVariant": "#657b83", + "mOutline": "#006883", + "mOutlineVariant": "#004050", + "mShadow": "#002b36" +} From 1d7d0752ada69e9a5e1b0e3d1a71a31f2c2a9f9a Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Fri, 15 Aug 2025 23:15:41 +0200 Subject: [PATCH 323/394] Add ActiveWindow, make ColorScheme look better --- Modules/Bar/ActiveWindow.qml | 151 +++++++++++ Modules/Bar/Bar.qml | 2 + Modules/Settings/Tabs/ColorSchemeTab.qml | 309 +++++++++++++++++++++-- Services/ColorSchemes.qml | 1 + Services/Workspaces.qml | 6 - 5 files changed, 440 insertions(+), 29 deletions(-) create mode 100644 Modules/Bar/ActiveWindow.qml diff --git a/Modules/Bar/ActiveWindow.qml b/Modules/Bar/ActiveWindow.qml new file mode 100644 index 0000000..194ec21 --- /dev/null +++ b/Modules/Bar/ActiveWindow.qml @@ -0,0 +1,151 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import qs.Services +import qs.Widgets + +Row { + id: layout + anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginSmall * scaling + visible: Settings.data.bar.showActiveWindow + + property bool showingFullTitle: false + property int lastWindowIndex: -1 + + // Timer to hide full title after window switch + Timer { + id: fullTitleTimer + interval: 2000 // Show full title for 2 seconds + repeat: false + onTriggered: { + showingFullTitle = false + titleText.text = getDisplayText() + } + } + + // Update text when window changes + Connections { + target: typeof Niri !== "undefined" ? Niri : null + function onFocusedWindowIndexChanged() { + // Check if window actually changed + if (Niri.focusedWindowIndex !== lastWindowIndex) { + lastWindowIndex = Niri.focusedWindowIndex + showingFullTitle = true + fullTitleTimer.restart() + } + titleText.text = getDisplayText() + } + } + + // Window icon + NText { + id: windowIcon + text: "desktop_windows" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeLarge * scaling + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + color: Colors.mPrimary + visible: getDisplayText() !== "" + } + + // Window title container + Item { + id: titleContainer + width: titleText.width + height: titleText.height + anchors.verticalCenter: parent.verticalCenter + + Behavior on width { + NumberAnimation { + duration: 300 + easing.type: Easing.OutCubic + } + } + + NText { + id: titleText + text: getDisplayText() + font.pointSize: Style.fontSizeSmall * scaling + font.weight: Style.fontWeightBold + anchors.verticalCenter: parent.verticalCenter + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + // Mouse area for hover detection + MouseArea { + id: titleContainerMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + onEntered: { + titleText.text = getDisplayText() + } + onExited: { + titleText.text = getDisplayText() + } + } + } + + + + function getDisplayText() { + // Check if Niri service is available + if (typeof Niri === "undefined") { + return "" + } + + // Get the focused window data + const focusedWindow = Niri.focusedWindowIndex >= 0 && Niri.focusedWindowIndex < Niri.windows.length + ? Niri.windows[Niri.focusedWindowIndex] + : null + + if (!focusedWindow) { + return "" + } + + const appId = focusedWindow.appId || "" + const title = focusedWindow.title || "" + + // If no appId, fall back to title processing + if (!appId) { + if (!title || title === "(No active window)" || title === "(Unnamed window)") { + return "" + } + + // Extract program name from title (before first space or special characters) + const programName = title.split(/[\s\-_]/)[0] + + if (programName.length <= 2 || programName === title) { + return truncateTitle(title) + } + + if (showingFullTitle || titleContainerMouseArea.containsMouse || isGenericName(programName)) { + return truncateTitle(title) + } + + return programName + } + + // Use appId for program name, show full title on hover or window switch + if (showingFullTitle || titleContainerMouseArea.containsMouse) { + return truncateTitle(title || appId) + } + + return appId + } + + function truncateTitle(title) { + if (title.length > 50) { + return title.substring(0, 47) + "..." + } + return title + } + + function isGenericName(name) { + const genericNames = ["window", "application", "app", "program", "process", "unknown"] + return genericNames.includes(name.toLowerCase()) + } +} \ No newline at end of file diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 2959a5e..14907dd 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -55,6 +55,8 @@ Variants { SystemMonitor {} MediaMini {} + + ActiveWindow {} } // Center diff --git a/Modules/Settings/Tabs/ColorSchemeTab.qml b/Modules/Settings/Tabs/ColorSchemeTab.qml index 8e6b895..35a583c 100644 --- a/Modules/Settings/Tabs/ColorSchemeTab.qml +++ b/Modules/Settings/Tabs/ColorSchemeTab.qml @@ -3,11 +3,136 @@ import QtQuick.Controls import QtQuick.Layouts import qs.Services import qs.Widgets +import Quickshell.Io ColumnLayout { id: root spacing: 0 + + // Helper function to get color from scheme file + function getSchemeColor(schemePath, colorKey) { + // Extract scheme name from path + var schemeName = schemePath.split("/").pop().replace(".json", "") + + // Try to get from cached data first + if (schemeColorsCache[schemeName] && schemeColorsCache[schemeName][colorKey]) { + return schemeColorsCache[schemeName][colorKey] + } + + // Return a default color if not cached yet + return "#000000" + } + + // Cache for scheme colors + property var schemeColorsCache: ({}) + + // Array to hold FileView objects + property var fileViews: [] + + // Load color scheme data when schemes are available + Connections { + target: ColorSchemes + function onSchemesChanged() { + loadSchemeColors() + } + } + + function loadSchemeColors() { + // Clear existing cache + schemeColorsCache = {} + + // Destroy existing FileViews + for (var i = 0; i < fileViews.length; i++) { + if (fileViews[i]) { + fileViews[i].destroy() + } + } + fileViews = [] + + // Create FileViews for each scheme + for (var i = 0; i < ColorSchemes.schemes.length; i++) { + var schemePath = ColorSchemes.schemes[i] + var schemeName = schemePath.split("/").pop().replace(".json", "") + + // Create FileView component + var component = Qt.createComponent("SchemeFileView.qml") + if (component.status === Component.Ready) { + var fileView = component.createObject(root, { + "path": schemePath, + "schemeName": schemeName + }) + fileViews.push(fileView) + } else { + // Fallback: create inline FileView + createInlineFileView(schemePath, schemeName) + } + } + } + + function createInlineFileView(schemePath, schemeName) { + var fileViewQml = ` + import QtQuick + import Quickshell.Io + + FileView { + property string schemeName: "${schemeName}" + path: "${schemePath}" + blockLoading: true + + onLoaded: { + try { + var jsonData = JSON.parse(text()) + root.schemeLoaded(schemeName, jsonData) + } catch (e) { + console.warn("Failed to parse JSON for scheme:", schemeName, e) + } + } + } + ` + + try { + var fileView = Qt.createQmlObject(fileViewQml, root, "dynamicFileView_" + schemeName) + fileViews.push(fileView) + } catch (e) { + console.warn("Failed to create FileView for scheme:", schemeName, e) + } + } + + function schemeLoaded(schemeName, jsonData) { + console.log("Loading scheme colors for:", schemeName) + + var colors = {} + + // Extract colors from JSON data + if (jsonData && typeof jsonData === 'object') { + colors.mPrimary = jsonData.mPrimary || jsonData.primary || "#000000" + colors.mSecondary = jsonData.mSecondary || jsonData.secondary || "#000000" + colors.mTertiary = jsonData.mTertiary || jsonData.tertiary || "#000000" + colors.mError = jsonData.mError || jsonData.error || "#ff0000" + colors.mSurface = jsonData.mSurface || jsonData.surface || "#ffffff" + colors.mOnSurface = jsonData.mOnSurface || jsonData.onSurface || "#000000" + colors.mOutline = jsonData.mOutline || jsonData.outline || "#666666" + } else { + // Default colors + colors = { + mPrimary: "#000000", + mSecondary: "#000000", + mTertiary: "#000000", + mError: "#ff0000", + mSurface: "#ffffff", + mOnSurface: "#000000", + mOutline: "#666666" + } + } + + // Update cache + var newCache = schemeColorsCache + newCache[schemeName] = colors + schemeColorsCache = newCache + + console.log("Cached colors for", schemeName, ":", JSON.stringify(colors)) + } ScrollView { id: scrollView @@ -32,10 +157,10 @@ ColumnLayout { spacing: Style.marginLarge * scaling Layout.fillWidth: true - // Use Wallpaper Colors + // Use Matugen NToggle { - label: "Use Wallpaper Colors" - description: "Automatically generate colors from you active wallpaper (requires Matugen)" + label: "Use Matugen" + description: "Automatically generate colors based on your active wallpaper using Matugen" value: Settings.data.colorSchemes.useWallpaperColors onToggled: function (newValue) { Settings.data.colorSchemes.useWallpaperColors = newValue @@ -45,30 +170,168 @@ ColumnLayout { } } + NDivider { + Layout.fillWidth: true + } + + NText { + text: "Predefined Color Schemes" + font.pointSize: Style.fontSizeLarge * scaling + font.weight: Style.fontWeightBold + color: Colors.mOnSurface + Layout.fillWidth: true + } + + NText { + text: "These color schemes only apply when 'Use Matugen' is disabled. When enabled, Matugen will generate colors based on your wallpaper instead." + font.pointSize: Style.fontSizeSmall * scaling + color: Colors.mOnSurface + Layout.fillWidth: true + wrapMode: Text.WordWrap + Layout.topMargin: -16 * scaling + } + ColumnLayout { spacing: Style.marginTiny * scaling Layout.fillWidth: true - ButtonGroup { - id: schemesGroup - } + // Color Schemes Grid + GridLayout { + columns: 4 + rowSpacing: Style.marginLarge * scaling + columnSpacing: Style.marginLarge * scaling + Layout.fillWidth: true - Repeater { - model: ColorSchemes.schemes - NRadioButton { - property string schemePath: modelData - ButtonGroup.group: schemesGroup - text: { - // Remove json and the full path - var chunks = schemePath.replace(".json", "").split("/") - return chunks[chunks.length - 1] - } - checked: Settings.data.colorSchemes.predefinedScheme == schemePath - onClicked: { - // Disable useWallpaperColors when picking a predefined color scheme - Settings.data.colorSchemes.useWallpaperColors = false - Settings.data.colorSchemes.predefinedScheme = schemePath - ColorSchemes.applyScheme(schemePath) + Repeater { + model: ColorSchemes.schemes + + Rectangle { + id: schemeCard + Layout.fillWidth: true + Layout.preferredHeight: 120 * scaling + radius: 12 * scaling + color: getSchemeColor(modelData, "mSurface") + border.width: 2 + border.color: Settings.data.colorSchemes.predefinedScheme === modelData ? Colors.mPrimary : Colors.mOutline + + property string schemePath: modelData + + // Mouse area for selection + MouseArea { + anchors.fill: parent + onClicked: { + // Disable useWallpaperColors when picking a predefined color scheme + Settings.data.colorSchemes.useWallpaperColors = false + Settings.data.colorSchemes.predefinedScheme = schemePath + ColorSchemes.applyScheme(schemePath) + } + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onEntered: { + schemeCard.scale = 1.05 + schemeCard.border.width = 3 + } + + onExited: { + schemeCard.scale = 1.0 + schemeCard.border.width = 2 + } + } + + // Card content + ColumnLayout { + anchors.fill: parent + anchors.margins: 16 * scaling + spacing: 8 * scaling + + // Scheme name + NText { + text: { + // Remove json and the full path + var chunks = schemePath.replace(".json", "").split("/") + return chunks[chunks.length - 1] + } + font.pointSize: Style.fontSizeMedium * scaling + font.weight: Style.fontWeightBold + color: Colors.mOnSurface + Layout.fillWidth: true + elide: Text.ElideRight + horizontalAlignment: Text.AlignHCenter + } + + // Color swatches + RowLayout { + spacing: 8 * scaling + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + + // Primary color swatch + Rectangle { + width: 28 * scaling + height: 28 * scaling + radius: 14 * scaling + color: getSchemeColor(modelData, "mPrimary") + } + + // Secondary color swatch + Rectangle { + width: 28 * scaling + height: 28 * scaling + radius: 14 * scaling + color: getSchemeColor(modelData, "mSecondary") + } + + // Tertiary color swatch + Rectangle { + width: 28 * scaling + height: 28 * scaling + radius: 14 * scaling + color: getSchemeColor(modelData, "mTertiary") + } + + // Error color swatch + Rectangle { + width: 28 * scaling + height: 28 * scaling + radius: 14 * scaling + color: getSchemeColor(modelData, "mError") + } + } + } + + // Selection indicator + Rectangle { + visible: Settings.data.colorSchemes.predefinedScheme === schemePath + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: 8 * scaling + width: 24 * scaling + height: 24 * scaling + radius: 12 * scaling + color: Colors.mPrimary + + NText { + anchors.centerIn: parent + text: "✓" + font.pointSize: Style.fontSizeSmall * scaling + font.weight: Style.fontWeightBold + color: Colors.mOnPrimary + } + } + + // Smooth animations + Behavior on scale { + NumberAnimation { duration: 200; easing.type: Easing.OutCubic } + } + + Behavior on border.color { + ColorAnimation { duration: 300 } + } + + Behavior on border.width { + NumberAnimation { duration: 200 } + } } } } @@ -76,4 +339,4 @@ ColumnLayout { } } } -} +} \ No newline at end of file diff --git a/Services/ColorSchemes.qml b/Services/ColorSchemes.qml index eddd275..4ef659e 100644 --- a/Services/ColorSchemes.qml +++ b/Services/ColorSchemes.qml @@ -54,6 +54,7 @@ Singleton { } schemes = files scanning = false + console.log("[ColorSchemes] Loaded", schemes.length, "schemes") } } } diff --git a/Services/Workspaces.qml b/Services/Workspaces.qml index f513b37..574a577 100644 --- a/Services/Workspaces.qml +++ b/Services/Workspaces.qml @@ -17,7 +17,6 @@ Singleton { property var hlWorkspaces: Hyprland.workspaces.values // Detect which compositor we're using Component.onCompleted: { - console.log("[Workspaces] Initializing workspaces service") detectCompositor() } @@ -25,25 +24,20 @@ Singleton { try { try { if (Hyprland.eventSocketPath) { - console.log("[Workspaces] Detected Hyprland compositor") isHyprland = true isNiri = false initHyprland() return } } catch (e) { - console.log("[Workspaces] Hyprland not available:", e) } if (typeof Niri !== "undefined") { - console.log("[Workspaces] Detected Niri service") isHyprland = false isNiri = true initNiri() return } - - console.log("[Workspaces] Could not detect any supported compositor") } catch (e) { console.error("[Workspaces] Error detecting compositor:", e) } From c0cfdff1d98b32efec966ed718b9b0ba95212b5e Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 17:20:24 -0400 Subject: [PATCH 324/394] Formatting --- Modules/Bar/ActiveWindow.qml | 75 ++++++------- Modules/Settings/Tabs/ColorSchemeTab.qml | 137 ++++++++++++----------- Services/Workspaces.qml | 1 + 3 files changed, 109 insertions(+), 104 deletions(-) diff --git a/Modules/Bar/ActiveWindow.qml b/Modules/Bar/ActiveWindow.qml index 194ec21..921f211 100644 --- a/Modules/Bar/ActiveWindow.qml +++ b/Modules/Bar/ActiveWindow.qml @@ -9,10 +9,10 @@ Row { anchors.verticalCenter: parent.verticalCenter spacing: Style.marginSmall * scaling visible: Settings.data.bar.showActiveWindow - + property bool showingFullTitle: false property int lastWindowIndex: -1 - + // Timer to hide full title after window switch Timer { id: fullTitleTimer @@ -23,7 +23,7 @@ Row { titleText.text = getDisplayText() } } - + // Update text when window changes Connections { target: typeof Niri !== "undefined" ? Niri : null @@ -37,7 +37,7 @@ Row { titleText.text = getDisplayText() } } - + // Window icon NText { id: windowIcon @@ -49,21 +49,21 @@ Row { color: Colors.mPrimary visible: getDisplayText() !== "" } - + // Window title container Item { id: titleContainer width: titleText.width height: titleText.height anchors.verticalCenter: parent.verticalCenter - + Behavior on width { NumberAnimation { duration: 300 easing.type: Easing.OutCubic } } - + NText { id: titleText text: getDisplayText() @@ -73,79 +73,76 @@ Row { verticalAlignment: Text.AlignVCenter elide: Text.ElideRight } - - // Mouse area for hover detection - MouseArea { - id: titleContainerMouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.IBeamCursor - onEntered: { - titleText.text = getDisplayText() - } - onExited: { - titleText.text = getDisplayText() - } - } - } - + // Mouse area for hover detection + MouseArea { + id: titleContainerMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + onEntered: { + titleText.text = getDisplayText() + } + onExited: { + titleText.text = getDisplayText() + } + } + } function getDisplayText() { // Check if Niri service is available if (typeof Niri === "undefined") { return "" } - + // Get the focused window data - const focusedWindow = Niri.focusedWindowIndex >= 0 && Niri.focusedWindowIndex < Niri.windows.length - ? Niri.windows[Niri.focusedWindowIndex] - : null - + const focusedWindow = Niri.focusedWindowIndex >= 0 + && Niri.focusedWindowIndex < Niri.windows.length ? Niri.windows[Niri.focusedWindowIndex] : null + if (!focusedWindow) { return "" } - + const appId = focusedWindow.appId || "" const title = focusedWindow.title || "" - + // If no appId, fall back to title processing if (!appId) { if (!title || title === "(No active window)" || title === "(Unnamed window)") { return "" } - + // Extract program name from title (before first space or special characters) const programName = title.split(/[\s\-_]/)[0] - + if (programName.length <= 2 || programName === title) { return truncateTitle(title) } - + if (showingFullTitle || titleContainerMouseArea.containsMouse || isGenericName(programName)) { return truncateTitle(title) } - + return programName } - + // Use appId for program name, show full title on hover or window switch if (showingFullTitle || titleContainerMouseArea.containsMouse) { return truncateTitle(title || appId) } - + return appId } - + function truncateTitle(title) { if (title.length > 50) { return title.substring(0, 47) + "..." } return title } - + function isGenericName(name) { const genericNames = ["window", "application", "app", "program", "process", "unknown"] return genericNames.includes(name.toLowerCase()) } -} \ No newline at end of file +} diff --git a/Modules/Settings/Tabs/ColorSchemeTab.qml b/Modules/Settings/Tabs/ColorSchemeTab.qml index 35a583c..de3a886 100644 --- a/Modules/Settings/Tabs/ColorSchemeTab.qml +++ b/Modules/Settings/Tabs/ColorSchemeTab.qml @@ -9,27 +9,27 @@ ColumnLayout { id: root spacing: 0 - + // Helper function to get color from scheme file function getSchemeColor(schemePath, colorKey) { // Extract scheme name from path var schemeName = schemePath.split("/").pop().replace(".json", "") - + // Try to get from cached data first if (schemeColorsCache[schemeName] && schemeColorsCache[schemeName][colorKey]) { return schemeColorsCache[schemeName][colorKey] } - + // Return a default color if not cached yet return "#000000" } - + // Cache for scheme colors property var schemeColorsCache: ({}) - + // Array to hold FileView objects property var fileViews: [] - + // Load color scheme data when schemes are available Connections { target: ColorSchemes @@ -37,11 +37,11 @@ ColumnLayout { loadSchemeColors() } } - + function loadSchemeColors() { // Clear existing cache schemeColorsCache = {} - + // Destroy existing FileViews for (var i = 0; i < fileViews.length; i++) { if (fileViews[i]) { @@ -49,19 +49,19 @@ ColumnLayout { } } fileViews = [] - + // Create FileViews for each scheme for (var i = 0; i < ColorSchemes.schemes.length; i++) { var schemePath = ColorSchemes.schemes[i] var schemeName = schemePath.split("/").pop().replace(".json", "") - + // Create FileView component var component = Qt.createComponent("SchemeFileView.qml") if (component.status === Component.Ready) { var fileView = component.createObject(root, { - "path": schemePath, - "schemeName": schemeName - }) + "path": schemePath, + "schemeName": schemeName + }) fileViews.push(fileView) } else { // Fallback: create inline FileView @@ -69,28 +69,28 @@ ColumnLayout { } } } - + function createInlineFileView(schemePath, schemeName) { var fileViewQml = ` - import QtQuick - import Quickshell.Io - - FileView { - property string schemeName: "${schemeName}" - path: "${schemePath}" - blockLoading: true - - onLoaded: { - try { - var jsonData = JSON.parse(text()) - root.schemeLoaded(schemeName, jsonData) - } catch (e) { - console.warn("Failed to parse JSON for scheme:", schemeName, e) - } - } - } + import QtQuick + import Quickshell.Io + + FileView { + property string schemeName: "${schemeName}" + path: "${schemePath}" + blockLoading: true + + onLoaded: { + try { + var jsonData = JSON.parse(text()) + root.schemeLoaded(schemeName, jsonData) + } catch (e) { + console.warn("Failed to parse JSON for scheme:", schemeName, e) + } + } + } ` - + try { var fileView = Qt.createQmlObject(fileViewQml, root, "dynamicFileView_" + schemeName) fileViews.push(fileView) @@ -98,12 +98,12 @@ ColumnLayout { console.warn("Failed to create FileView for scheme:", schemeName, e) } } - + function schemeLoaded(schemeName, jsonData) { console.log("Loading scheme colors for:", schemeName) - + var colors = {} - + // Extract colors from JSON data if (jsonData && typeof jsonData === 'object') { colors.mPrimary = jsonData.mPrimary || jsonData.primary || "#000000" @@ -116,21 +116,21 @@ ColumnLayout { } else { // Default colors colors = { - mPrimary: "#000000", - mSecondary: "#000000", - mTertiary: "#000000", - mError: "#ff0000", - mSurface: "#ffffff", - mOnSurface: "#000000", - mOutline: "#666666" + "mPrimary": "#000000", + "mSecondary": "#000000", + "mTertiary": "#000000", + "mError": "#ff0000", + "mSurface": "#ffffff", + "mOnSurface": "#000000", + "mOutline": "#666666" } } - + // Update cache var newCache = schemeColorsCache newCache[schemeName] = colors schemeColorsCache = newCache - + console.log("Cached colors for", schemeName, ":", JSON.stringify(colors)) } @@ -204,7 +204,7 @@ ColumnLayout { Repeater { model: ColorSchemes.schemes - + Rectangle { id: schemeCard Layout.fillWidth: true @@ -213,9 +213,9 @@ ColumnLayout { color: getSchemeColor(modelData, "mSurface") border.width: 2 border.color: Settings.data.colorSchemes.predefinedScheme === modelData ? Colors.mPrimary : Colors.mOutline - + property string schemePath: modelData - + // Mouse area for selection MouseArea { anchors.fill: parent @@ -227,24 +227,24 @@ ColumnLayout { } hoverEnabled: true cursorShape: Qt.PointingHandCursor - + onEntered: { schemeCard.scale = 1.05 schemeCard.border.width = 3 } - + onExited: { schemeCard.scale = 1.0 schemeCard.border.width = 2 } } - + // Card content ColumnLayout { anchors.fill: parent anchors.margins: 16 * scaling spacing: 8 * scaling - + // Scheme name NText { text: { @@ -259,13 +259,13 @@ ColumnLayout { elide: Text.ElideRight horizontalAlignment: Text.AlignHCenter } - + // Color swatches RowLayout { spacing: 8 * scaling Layout.fillWidth: true Layout.alignment: Qt.AlignHCenter - + // Primary color swatch Rectangle { width: 28 * scaling @@ -273,7 +273,7 @@ ColumnLayout { radius: 14 * scaling color: getSchemeColor(modelData, "mPrimary") } - + // Secondary color swatch Rectangle { width: 28 * scaling @@ -281,7 +281,7 @@ ColumnLayout { radius: 14 * scaling color: getSchemeColor(modelData, "mSecondary") } - + // Tertiary color swatch Rectangle { width: 28 * scaling @@ -289,7 +289,7 @@ ColumnLayout { radius: 14 * scaling color: getSchemeColor(modelData, "mTertiary") } - + // Error color swatch Rectangle { width: 28 * scaling @@ -299,7 +299,7 @@ ColumnLayout { } } } - + // Selection indicator Rectangle { visible: Settings.data.colorSchemes.predefinedScheme === schemePath @@ -310,7 +310,7 @@ ColumnLayout { height: 24 * scaling radius: 12 * scaling color: Colors.mPrimary - + NText { anchors.centerIn: parent text: "✓" @@ -319,18 +319,25 @@ ColumnLayout { color: Colors.mOnPrimary } } - + // Smooth animations Behavior on scale { - NumberAnimation { duration: 200; easing.type: Easing.OutCubic } + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } } - + Behavior on border.color { - ColorAnimation { duration: 300 } + ColorAnimation { + duration: 300 + } } - + Behavior on border.width { - NumberAnimation { duration: 200 } + NumberAnimation { + duration: 200 + } } } } @@ -339,4 +346,4 @@ ColumnLayout { } } } -} \ No newline at end of file +} diff --git a/Services/Workspaces.qml b/Services/Workspaces.qml index 574a577..12acdba 100644 --- a/Services/Workspaces.qml +++ b/Services/Workspaces.qml @@ -30,6 +30,7 @@ Singleton { return } } catch (e) { + } if (typeof Niri !== "undefined") { From 9f3ebd1a40553bc53dcfecf810c7c0bda11fe7de Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Fri, 15 Aug 2025 23:27:31 +0200 Subject: [PATCH 325/394] Fix AppLauncher.qml hover --- Modules/AppLauncher/AppLauncher.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/AppLauncher/AppLauncher.qml b/Modules/AppLauncher/AppLauncher.qml index 3f90006..3158ff0 100644 --- a/Modules/AppLauncher/AppLauncher.qml +++ b/Modules/AppLauncher/AppLauncher.qml @@ -366,7 +366,7 @@ NLoader { delegate: Rectangle { width: appsList.width - Style.marginSmall * scaling - height: 56 * scaling + height: 65 * scaling radius: 16 * scaling property bool isSelected: index === selectedIndex color: (appCardArea.containsMouse || isSelected) ? Qt.darker(Colors.mPrimary, From 38f35fbd7d0fa4029054c96ccc3dc8df9fef6a8f Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 17:29:33 -0400 Subject: [PATCH 326/394] Settings: Ensure a minimum size of 1280x720 --- Modules/Settings/SettingsPanel.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/Settings/SettingsPanel.qml b/Modules/Settings/SettingsPanel.qml index 06ea2df..094a75b 100644 --- a/Modules/Settings/SettingsPanel.qml +++ b/Modules/Settings/SettingsPanel.qml @@ -193,8 +193,8 @@ NLoader { border.color: Colors.mOutlineVariant border.width: Math.max(1, Style.borderThin * scaling) layer.enabled: true - width: (screen.width * 0.5) * scaling - height: (screen.height * 0.5) * scaling + width: Math.max(screen.width * 0.5, 1280) * scaling + height: Math.max(screen.width * 0.5, 720) * scaling anchors.centerIn: parent // Animation properties From 7e7b7b019214a4f38f9d89a0f144b1c0d2332d42 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 17:32:57 -0400 Subject: [PATCH 327/394] Borders should never used mSurfaceS colors --- Modules/Bar/WiFiMenu.qml | 2 +- Modules/Calendar/Calendar.qml | 2 +- Modules/Notification/NotificationHistoryPanel.qml | 6 +++--- Modules/SidePanel/Cards/MediaCard.qml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Modules/Bar/WiFiMenu.qml b/Modules/Bar/WiFiMenu.qml index 58754f3..9163c4e 100644 --- a/Modules/Bar/WiFiMenu.qml +++ b/Modules/Bar/WiFiMenu.qml @@ -87,7 +87,7 @@ NLoader { id: wifiMenuRect color: Colors.mSurface radius: Style.radiusMedium * scaling - border.color: Colors.mSurfaceVariant + border.color: Colors.mOutline border.width: Math.max(1, Style.borderMedium * scaling) width: 340 * scaling height: 320 * scaling diff --git a/Modules/Calendar/Calendar.qml b/Modules/Calendar/Calendar.qml index d433718..a51b8d3 100644 --- a/Modules/Calendar/Calendar.qml +++ b/Modules/Calendar/Calendar.qml @@ -63,7 +63,7 @@ NLoader { id: calendarRect color: Colors.mSurface radius: Style.radiusMedium * scaling - border.color: Colors.mSurfaceVariant + border.color: Colors.mOutline border.width: Math.max(1, Style.borderMedium * scaling) width: 340 * scaling height: 320 * scaling // Reduced height to eliminate bottom space diff --git a/Modules/Notification/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml index da0c95c..65ecf6e 100644 --- a/Modules/Notification/NotificationHistoryPanel.qml +++ b/Modules/Notification/NotificationHistoryPanel.qml @@ -65,9 +65,9 @@ NLoader { Rectangle { id: notificationRect - color: Colors.mSurface + color: Colors.mSurfaceVariant radius: Style.radiusMedium * scaling - border.color: Colors.mSurfaceVariant + border.color: Colors.mOutlineVariant border.width: Math.max(1, Style.borderMedium * scaling) width: 400 * scaling height: 500 * scaling @@ -159,7 +159,7 @@ NLoader { width: notificationList ? (notificationList.width - 20) : 380 * scaling height: Math.max(80, notificationContent.height + 30) radius: Style.radiusMedium * scaling - color: notificationMouseArea.containsMouse ? Colors.mPrimary : "transparent" + color: notificationMouseArea.containsMouse ? Colors.mPrimary : Colors.mSurface RowLayout { anchors { diff --git a/Modules/SidePanel/Cards/MediaCard.qml b/Modules/SidePanel/Cards/MediaCard.qml index f2bd12e..43b1594 100644 --- a/Modules/SidePanel/Cards/MediaCard.qml +++ b/Modules/SidePanel/Cards/MediaCard.qml @@ -265,7 +265,7 @@ NBox { height: 16 * scaling radius: width * 0.5 color: Colors.mPrimary - border.color: Colors.mSurface + border.color: Colors.mOutline border.width: Math.max(1 * Style.borderMedium * scaling) x: Math.max(0, Math.min(parent.width - width, progressFill.width - width / 2)) anchors.verticalCenter: parent.verticalCenter From 55a8aa30265acf7d461d666be93f53e99ecd25e7 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 17:38:41 -0400 Subject: [PATCH 328/394] TrayMenu: fix hover --- Modules/Bar/TrayMenu.qml | 2 +- Widgets/NCard.qml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/Bar/TrayMenu.qml b/Modules/Bar/TrayMenu.qml index dd94d8d..5289a72 100644 --- a/Modules/Bar/TrayMenu.qml +++ b/Modules/Bar/TrayMenu.qml @@ -125,7 +125,7 @@ PopupWindow { id: text Layout.fillWidth: true color: (modelData?.enabled - ?? true) ? (mouseArea.containsMouse ? Colors.mOnSurface : Colors.mOnSurface) : Colors.textDisabled + ?? true) ? (mouseArea.containsMouse ? Colors.mOnTertiary : Colors.mOnSurface) : Colors.textDisabled text: modelData?.text ?? "" font.pointSize: Style.fontSizeSmall * scaling verticalAlignment: Text.AlignVCenter diff --git a/Widgets/NCard.qml b/Widgets/NCard.qml index c173a8a..0fcfd1e 100644 --- a/Widgets/NCard.qml +++ b/Widgets/NCard.qml @@ -10,6 +10,6 @@ Rectangle { color: Colors.mSurface radius: Style.radiusMedium * scaling - border.color: Colors.mSurfaceVariant + border.color: Colors.mOutline border.width: Math.max(1, Style.borderThin * scaling) } From 7ec7ac793e52a6452c9dfa94d574259b1dc060fd Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 17:40:13 -0400 Subject: [PATCH 329/394] SettingsPanel size --- Modules/Settings/SettingsPanel.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Settings/SettingsPanel.qml b/Modules/Settings/SettingsPanel.qml index 094a75b..68f5a36 100644 --- a/Modules/Settings/SettingsPanel.qml +++ b/Modules/Settings/SettingsPanel.qml @@ -194,7 +194,7 @@ NLoader { border.width: Math.max(1, Style.borderThin * scaling) layer.enabled: true width: Math.max(screen.width * 0.5, 1280) * scaling - height: Math.max(screen.width * 0.5, 720) * scaling + height: Math.max(screen.height * 0.5, 720) * scaling anchors.centerIn: parent // Animation properties From b371cb92c11720b2b57c80c856d1905b58a060ea Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Fri, 15 Aug 2025 23:41:10 +0200 Subject: [PATCH 330/394] Fix NotificationHistory --- .../Notification/NotificationHistoryPanel.qml | 51 +++++++++++-------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/Modules/Notification/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml index 65ecf6e..33be9be 100644 --- a/Modules/Notification/NotificationHistoryPanel.qml +++ b/Modules/Notification/NotificationHistoryPanel.qml @@ -63,18 +63,19 @@ NLoader { WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand - Rectangle { - id: notificationRect - color: Colors.mSurfaceVariant - radius: Style.radiusMedium * scaling - border.color: Colors.mOutlineVariant - border.width: Math.max(1, Style.borderMedium * scaling) - width: 400 * scaling - height: 500 * scaling - anchors.top: parent.top - anchors.right: parent.right - anchors.topMargin: Style.marginTiny * scaling - anchors.rightMargin: Style.marginTiny * scaling + Rectangle { + id: notificationRect + color: Colors.mSurfaceVariant + radius: Style.radiusMedium * scaling + border.color: Colors.mOutlineVariant + border.width: Math.max(1, Style.borderMedium * scaling) + width: 400 * scaling + height: 500 * scaling + anchors.top: parent.top + anchors.right: parent.right + anchors.topMargin: Style.marginTiny * scaling + anchors.rightMargin: Style.marginTiny * scaling + clip: true // Animation properties property real scaleValue: 0.8 @@ -152,7 +153,7 @@ NLoader { Layout.fillHeight: true model: NotificationService.historyModel spacing: Style.marginMedium * scaling - clip: false + clip: true boundsBehavior: Flickable.StopAtBounds delegate: Rectangle { @@ -181,7 +182,7 @@ NLoader { font.weight: Font.Medium color: notificationMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurface wrapMode: Text.Wrap - width: parent.width - 30 + width: parent.width - 60 maximumLineCount: 2 elide: Text.ElideRight } @@ -191,7 +192,7 @@ NLoader { font.pointSize: Style.fontSizeSmall * scaling color: notificationMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurface wrapMode: Text.Wrap - width: parent.width - 30 + width: parent.width - 60 maximumLineCount: 3 elide: Text.ElideRight } @@ -202,17 +203,27 @@ NLoader { color: notificationMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurface } } + + // Trash icon button + NIconButton { + icon: "delete" + sizeMultiplier: 0.7 + tooltipText: "Delete notification" + color: notificationMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurfaceVariant + onClicked: { + console.log("[NotificationHistory] Removing notification:", summary) + NotificationService.historyModel.remove(index) + NotificationService.saveHistory() + } + } } MouseArea { id: notificationMouseArea anchors.fill: parent + anchors.rightMargin: 50 * scaling hoverEnabled: true - onClicked: { - console.log("[NotificationHistory] Removing notification:", summary) - NotificationService.historyModel.remove(index) - NotificationService.saveHistory() - } + // Remove the onClicked handler since we now have a dedicated delete button } } From 23749b3696a0e5deaa5a85c427285e206e56cd83 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Fri, 15 Aug 2025 23:46:47 +0200 Subject: [PATCH 331/394] More NotificationHistory fixes --- Modules/Notification/NotificationHistoryPanel.qml | 1 - 1 file changed, 1 deletion(-) diff --git a/Modules/Notification/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml index 33be9be..146b1a7 100644 --- a/Modules/Notification/NotificationHistoryPanel.qml +++ b/Modules/Notification/NotificationHistoryPanel.qml @@ -209,7 +209,6 @@ NLoader { icon: "delete" sizeMultiplier: 0.7 tooltipText: "Delete notification" - color: notificationMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurfaceVariant onClicked: { console.log("[NotificationHistory] Removing notification:", summary) NotificationService.historyModel.remove(index) From 112bffe9846ea7e80dbc3f3abfd0f406155390a8 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 17:50:33 -0400 Subject: [PATCH 332/394] Fixed Catppuccin theme (double ##) --- Assets/ColorSchemes/Catppuccin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Assets/ColorSchemes/Catppuccin.json b/Assets/ColorSchemes/Catppuccin.json index ac63b64..5336a71 100644 --- a/Assets/ColorSchemes/Catppuccin.json +++ b/Assets/ColorSchemes/Catppuccin.json @@ -10,7 +10,7 @@ "mOnError": "#11111b", "mSurface": "#1e1e2e", - "mOnSurface": "##cdd6f4", + "mOnSurface": "#cdd6f4", "mSurfaceVariant": "#313244", "mOnSurfaceVariant": "#a3b4eb", "mOutline": "#45475a", From 2a5a3a1b7871b87c555ad65f06a373ecb2651d3a Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 18:09:06 -0400 Subject: [PATCH 333/394] ColorScheme preview cleanup (UI only) --- Modules/Settings/Tabs/ColorSchemeTab.qml | 69 +++++++++++++----------- 1 file changed, 39 insertions(+), 30 deletions(-) diff --git a/Modules/Settings/Tabs/ColorSchemeTab.qml b/Modules/Settings/Tabs/ColorSchemeTab.qml index de3a886..a8fe35c 100644 --- a/Modules/Settings/Tabs/ColorSchemeTab.qml +++ b/Modules/Settings/Tabs/ColorSchemeTab.qml @@ -172,28 +172,35 @@ ColumnLayout { NDivider { Layout.fillWidth: true + Layout.topMargin: Style.marginLarge * scaling + Layout.bottomMargin: Style.marginLarge * scaling } - NText { - text: "Predefined Color Schemes" - font.pointSize: Style.fontSizeLarge * scaling - font.weight: Style.fontWeightBold - color: Colors.mOnSurface + ColumnLayout { + spacing: Style.marginTiniest * scaling Layout.fillWidth: true - } - NText { - text: "These color schemes only apply when 'Use Matugen' is disabled. When enabled, Matugen will generate colors based on your wallpaper instead." - font.pointSize: Style.fontSizeSmall * scaling - color: Colors.mOnSurface - Layout.fillWidth: true - wrapMode: Text.WordWrap - Layout.topMargin: -16 * scaling + NText { + text: "Predefined Color Schemes" + font.pointSize: Style.fontSizeLarge * scaling + font.weight: Style.fontWeightBold + color: Colors.mOnSurface + Layout.fillWidth: true + } + + NText { + text: "These color schemes only apply when 'Use Matugen' is disabled. When enabled, Matugen will generate colors based on your wallpaper instead." + font.pointSize: Style.fontSizeSmall * scaling + color: Colors.mOnSurface + Layout.fillWidth: true + wrapMode: Text.WordWrap + } } ColumnLayout { spacing: Style.marginTiny * scaling Layout.fillWidth: true + Layout.topMargin: Style.marginLarge * scaling // Color Schemes Grid GridLayout { @@ -205,14 +212,18 @@ ColumnLayout { Repeater { model: ColorSchemes.schemes + property real cardScaleLow: 0.95 + property real cardScaleHigh: 1.00 + Rectangle { id: schemeCard Layout.fillWidth: true Layout.preferredHeight: 120 * scaling - radius: 12 * scaling + radius: Style.radiusMedium * scaling color: getSchemeColor(modelData, "mSurface") - border.width: 2 + border.width: Math.max(1, Style.borderThick * scaling) border.color: Settings.data.colorSchemes.predefinedScheme === modelData ? Colors.mPrimary : Colors.mOutline + scale: cardScaleLow property string schemePath: modelData @@ -229,21 +240,19 @@ ColumnLayout { cursorShape: Qt.PointingHandCursor onEntered: { - schemeCard.scale = 1.05 - schemeCard.border.width = 3 + schemeCard.scale = cardScaleHight } onExited: { - schemeCard.scale = 1.0 - schemeCard.border.width = 2 + schemeCard.scale = cardScaleLow } } // Card content ColumnLayout { anchors.fill: parent - anchors.margins: 16 * scaling - spacing: 8 * scaling + anchors.margins: Style.marginXL * scaling + spacing: Style.marginSmall * scaling // Scheme name NText { @@ -262,7 +271,7 @@ ColumnLayout { // Color swatches RowLayout { - spacing: 8 * scaling + spacing: Style.marginSmall * scaling Layout.fillWidth: true Layout.alignment: Qt.AlignHCenter @@ -270,7 +279,7 @@ ColumnLayout { Rectangle { width: 28 * scaling height: 28 * scaling - radius: 14 * scaling + radius: width * 0.5 color: getSchemeColor(modelData, "mPrimary") } @@ -286,7 +295,7 @@ ColumnLayout { Rectangle { width: 28 * scaling height: 28 * scaling - radius: 14 * scaling + radius: width * 0.5 color: getSchemeColor(modelData, "mTertiary") } @@ -294,7 +303,7 @@ ColumnLayout { Rectangle { width: 28 * scaling height: 28 * scaling - radius: 14 * scaling + radius: width * 0.5 color: getSchemeColor(modelData, "mError") } } @@ -305,10 +314,10 @@ ColumnLayout { visible: Settings.data.colorSchemes.predefinedScheme === schemePath anchors.right: parent.right anchors.top: parent.top - anchors.margins: 8 * scaling + anchors.margins: Style.marginSmall * scaling width: 24 * scaling height: 24 * scaling - radius: 12 * scaling + radius: width * 0.5 color: Colors.mPrimary NText { @@ -323,20 +332,20 @@ ColumnLayout { // Smooth animations Behavior on scale { NumberAnimation { - duration: 200 + duration: Style.animationNormal easing.type: Easing.OutCubic } } Behavior on border.color { ColorAnimation { - duration: 300 + duration: Style.animationNormal } } Behavior on border.width { NumberAnimation { - duration: 200 + duration: Style.animationFast } } } From e7a5f8fb809c5e865166fe873a83560e75715cc6 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 18:22:01 -0400 Subject: [PATCH 334/394] ColorSchemeTab Preview: cleaned and refactored code to not use Qt.createComponent, use a repeater instead. --- Modules/Settings/Tabs/ColorSchemeTab.qml | 133 +++++++++-------------- 1 file changed, 49 insertions(+), 84 deletions(-) diff --git a/Modules/Settings/Tabs/ColorSchemeTab.qml b/Modules/Settings/Tabs/ColorSchemeTab.qml index a8fe35c..49196a9 100644 --- a/Modules/Settings/Tabs/ColorSchemeTab.qml +++ b/Modules/Settings/Tabs/ColorSchemeTab.qml @@ -27,78 +27,7 @@ ColumnLayout { // Cache for scheme colors property var schemeColorsCache: ({}) - // Array to hold FileView objects - property var fileViews: [] - - // Load color scheme data when schemes are available - Connections { - target: ColorSchemes - function onSchemesChanged() { - loadSchemeColors() - } - } - - function loadSchemeColors() { - // Clear existing cache - schemeColorsCache = {} - - // Destroy existing FileViews - for (var i = 0; i < fileViews.length; i++) { - if (fileViews[i]) { - fileViews[i].destroy() - } - } - fileViews = [] - - // Create FileViews for each scheme - for (var i = 0; i < ColorSchemes.schemes.length; i++) { - var schemePath = ColorSchemes.schemes[i] - var schemeName = schemePath.split("/").pop().replace(".json", "") - - // Create FileView component - var component = Qt.createComponent("SchemeFileView.qml") - if (component.status === Component.Ready) { - var fileView = component.createObject(root, { - "path": schemePath, - "schemeName": schemeName - }) - fileViews.push(fileView) - } else { - // Fallback: create inline FileView - createInlineFileView(schemePath, schemeName) - } - } - } - - function createInlineFileView(schemePath, schemeName) { - var fileViewQml = ` - import QtQuick - import Quickshell.Io - - FileView { - property string schemeName: "${schemeName}" - path: "${schemePath}" - blockLoading: true - - onLoaded: { - try { - var jsonData = JSON.parse(text()) - root.schemeLoaded(schemeName, jsonData) - } catch (e) { - console.warn("Failed to parse JSON for scheme:", schemeName, e) - } - } - } - ` - - try { - var fileView = Qt.createQmlObject(fileViewQml, root, "dynamicFileView_" + schemeName) - fileViews.push(fileView) - } catch (e) { - console.warn("Failed to create FileView for scheme:", schemeName, e) - } - } - + // This function is called by the FileView Repeater when a scheme file is loaded function schemeLoaded(schemeName, jsonData) { console.log("Loading scheme colors for:", schemeName) @@ -114,7 +43,7 @@ ColumnLayout { colors.mOnSurface = jsonData.mOnSurface || jsonData.onSurface || "#000000" colors.mOutline = jsonData.mOutline || jsonData.outline || "#666666" } else { - // Default colors + // Default colors on failure colors = { "mPrimary": "#000000", "mSecondary": "#000000", @@ -126,14 +55,50 @@ ColumnLayout { } } - // Update cache + // Update the cache. This must be done by re-assigning the whole object to trigger updates. var newCache = schemeColorsCache newCache[schemeName] = colors schemeColorsCache = newCache - - console.log("Cached colors for", schemeName, ":", JSON.stringify(colors)) } + // When the list of available schemes changes, clear the cache. + // The Repeater below will automatically re-create the FileViews. + Connections { + target: ColorSchemes + function onSchemesChanged() { + schemeColorsCache = {} + } + } + + // A non-visual Item to host the Repeater that loads the color scheme files. + Item { + visible: false + id: fileLoaders + + Repeater { + model: ColorSchemes.schemes + + // The delegate is a Component, which correctly wraps the non-visual FileView + delegate: Item { + FileView { + path: modelData + blockLoading: true + onLoaded: { + var schemeName = path.split("/").pop().replace(".json", "") + try { + var jsonData = JSON.parse(text()) + root.schemeLoaded(schemeName, jsonData) + } catch (e) { + console.warn("Failed to parse JSON for scheme:", schemeName, e) + root.schemeLoaded(schemeName, null) // Load defaults on parse error + } + } + } + } + } + } + + // UI Code ScrollView { id: scrollView @@ -212,11 +177,13 @@ ColumnLayout { Repeater { model: ColorSchemes.schemes - property real cardScaleLow: 0.95 - property real cardScaleHigh: 1.00 - Rectangle { id: schemeCard + + property real cardScaleLow: 0.95 + property real cardScaleHigh: 1.00 + property string schemePath: modelData + Layout.fillWidth: true Layout.preferredHeight: 120 * scaling radius: Style.radiusMedium * scaling @@ -225,8 +192,6 @@ ColumnLayout { border.color: Settings.data.colorSchemes.predefinedScheme === modelData ? Colors.mPrimary : Colors.mOutline scale: cardScaleLow - property string schemePath: modelData - // Mouse area for selection MouseArea { anchors.fill: parent @@ -240,7 +205,7 @@ ColumnLayout { cursorShape: Qt.PointingHandCursor onEntered: { - schemeCard.scale = cardScaleHight + schemeCard.scale = cardScaleHigh } onExited: { @@ -263,7 +228,7 @@ ColumnLayout { } font.pointSize: Style.fontSizeMedium * scaling font.weight: Style.fontWeightBold - color: Colors.mOnSurface + color: getSchemeColor(modelData, "mOnSurface") Layout.fillWidth: true elide: Text.ElideRight horizontalAlignment: Text.AlignHCenter @@ -287,7 +252,7 @@ ColumnLayout { Rectangle { width: 28 * scaling height: 28 * scaling - radius: 14 * scaling + radius: width * 0.5 color: getSchemeColor(modelData, "mSecondary") } From cf9c51b444ba2f2fe02c2f3d1b7eceffeed1a366 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 18:22:14 -0400 Subject: [PATCH 335/394] NotificationsHistory formatting --- .../Notification/NotificationHistoryPanel.qml | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Modules/Notification/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml index 146b1a7..1eb0df7 100644 --- a/Modules/Notification/NotificationHistoryPanel.qml +++ b/Modules/Notification/NotificationHistoryPanel.qml @@ -63,19 +63,19 @@ NLoader { WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand - Rectangle { - id: notificationRect - color: Colors.mSurfaceVariant - radius: Style.radiusMedium * scaling - border.color: Colors.mOutlineVariant - border.width: Math.max(1, Style.borderMedium * scaling) - width: 400 * scaling - height: 500 * scaling - anchors.top: parent.top - anchors.right: parent.right - anchors.topMargin: Style.marginTiny * scaling - anchors.rightMargin: Style.marginTiny * scaling - clip: true + Rectangle { + id: notificationRect + color: Colors.mSurfaceVariant + radius: Style.radiusMedium * scaling + border.color: Colors.mOutlineVariant + border.width: Math.max(1, Style.borderMedium * scaling) + width: 400 * scaling + height: 500 * scaling + anchors.top: parent.top + anchors.right: parent.right + anchors.topMargin: Style.marginTiny * scaling + anchors.rightMargin: Style.marginTiny * scaling + clip: true // Animation properties property real scaleValue: 0.8 From c673b897cbd02b13d9e6e97d4ed4c55da9cfa849 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sat, 16 Aug 2025 01:17:44 +0200 Subject: [PATCH 336/394] Replace tons of hardcoded stuff --- Modules/AppLauncher/AppLauncher.qml | 62 +++++++------- Modules/Audio/CircularSpectrum.qml | 2 +- Modules/Bar/ActiveWindow.qml | 4 +- Modules/Bar/TrayMenu.qml | 4 +- Modules/Bar/WiFiMenu.qml | 26 +++--- Modules/Calendar/Calendar.qml | 2 +- Modules/Dock/Dock.qml | 28 +++---- Modules/LockScreen/LockScreen.qml | 82 +++++++++---------- Modules/Notification/Notification.qml | 4 +- .../Notification/NotificationHistoryPanel.qml | 14 ++-- Modules/Settings/SettingsPanel.qml | 2 +- Modules/Settings/Tabs/AboutTab.qml | 18 ++-- Modules/Settings/Tabs/AudioTab.qml | 2 +- Modules/Settings/Tabs/ColorSchemeTab.qml | 19 ++--- Modules/Settings/Tabs/TimeWeatherTab.qml | 2 +- .../Settings/Tabs/WallpaperSelectorTab.qml | 7 +- Modules/Settings/Tabs/WallpaperTab.qml | 2 +- Modules/SidePanel/Cards/MediaCard.qml | 6 +- Modules/SidePanel/Cards/ProfileCard.qml | 2 +- Modules/SidePanel/Cards/SystemMonitorCard.qml | 2 +- Modules/SidePanel/PowerMenu.qml | 10 +-- Widgets/NCircleStat.qml | 2 +- Widgets/NComboBox.qml | 2 +- Widgets/NRadioButton.qml | 6 +- Widgets/NTextInput.qml | 2 +- Widgets/NToggle.qml | 2 +- Widgets/NTooltip.qml | 2 +- 27 files changed, 155 insertions(+), 161 deletions(-) diff --git a/Modules/AppLauncher/AppLauncher.qml b/Modules/AppLauncher/AppLauncher.qml index 3158ff0..1735540 100644 --- a/Modules/AppLauncher/AppLauncher.qml +++ b/Modules/AppLauncher/AppLauncher.qml @@ -253,7 +253,7 @@ NLoader { anchors.centerIn: parent width: Math.min(700 * scaling, parent.width * 0.75) height: Math.min(550 * scaling, parent.height * 0.8) - radius: 32 * scaling + radius: Style.radiusLarge * scaling color: Colors.mSurface border.color: Colors.mOutline border.width: Style.borderThin * scaling @@ -278,22 +278,22 @@ NLoader { // Search bar Rectangle { Layout.fillWidth: true - Layout.preferredHeight: 40 * scaling + Layout.preferredHeight: Style.barHeight * scaling Layout.bottomMargin: Style.marginMedium * scaling - radius: 20 * scaling + radius: Style.radiusMedium * scaling color: Colors.mSurface border.color: searchInput.activeFocus ? Colors.mPrimary : Colors.mOutline - border.width: searchInput.activeFocus ? 2 : 1 + border.width: Math.max(1, searchInput.activeFocus ? Style.borderMedium * scaling : Style.borderThin * scaling) Row { anchors.fill: parent - anchors.margins: 12 * scaling - spacing: 10 * scaling + anchors.margins: Style.marginMedium * scaling + spacing: Style.marginSmall * scaling Text { text: "search" font.family: "Material Symbols Outlined" - font.pointSize: 16 * scaling + font.pointSize: Style.fontSizeLarger * scaling color: searchInput.activeFocus ? Colors.mPrimary : Colors.mOnSurface } @@ -303,7 +303,7 @@ NLoader { color: Colors.mOnSurface placeholderTextColor: Colors.mOnSurface background: null - font.pointSize: 13 * scaling + font.pointSize: Style.fontSizeLarge * scaling Layout.fillWidth: true onTextChanged: { searchText = text @@ -338,13 +338,13 @@ NLoader { Behavior on border.color { ColorAnimation { - duration: 120 + duration: Style.animationFast } } Behavior on border.width { NumberAnimation { - duration: 120 + duration: Style.animationFast } } } @@ -360,35 +360,35 @@ NLoader { ListView { id: appsList anchors.fill: parent - spacing: 4 * scaling + spacing: Style.marginTiniest * scaling model: filteredEntries currentIndex: selectedIndex delegate: Rectangle { width: appsList.width - Style.marginSmall * scaling height: 65 * scaling - radius: 16 * scaling + radius: Style.radiusMedium * scaling property bool isSelected: index === selectedIndex color: (appCardArea.containsMouse || isSelected) ? Qt.darker(Colors.mPrimary, 1.1) : Colors.mSurface border.color: (appCardArea.containsMouse || isSelected) ? Colors.mPrimary : "transparent" - border.width: (appCardArea.containsMouse || isSelected) ? 2 : 0 + border.width: Math.max(1, (appCardArea.containsMouse || isSelected) ? Style.borderMedium * scaling : 0) Behavior on color { ColorAnimation { - duration: 150 + duration: Style.animationFast } } Behavior on border.color { ColorAnimation { - duration: 150 + duration: Style.animationFast } } Behavior on border.width { NumberAnimation { - duration: 150 + duration: Style.animationFast } } @@ -399,9 +399,9 @@ NLoader { // App icon with background Rectangle { - Layout.preferredWidth: 40 * scaling - Layout.preferredHeight: 40 * scaling - radius: 14 * scaling + Layout.preferredWidth: Style.baseWidgetSize * 1.25 * scaling + Layout.preferredHeight: Style.baseWidgetSize * 1.25 * scaling + radius: Style.radiusSmall * scaling color: appCardArea.containsMouse ? Qt.darker(Colors.mPrimary, 1.1) : Colors.mSurfaceVariant property bool iconLoaded: (modelData.isCalculator || modelData.isClipboard @@ -414,9 +414,9 @@ NLoader { // Clipboard image display Image { id: clipboardImage - anchors.fill: parent - anchors.margins: 6 * scaling - visible: modelData.type === 'image' + anchors.fill: parent + anchors.margins: Style.marginTiny * scaling + visible: modelData.type === 'image' source: modelData.data || "" fillMode: Image.PreserveAspectCrop asynchronous: true @@ -426,7 +426,7 @@ NLoader { IconImage { id: iconImg anchors.fill: parent - anchors.margins: 6 * scaling + anchors.margins: Style.marginTiny * scaling asynchronous: true source: modelData.isCalculator ? "calculate" : modelData.isClipboard ? (modelData.type === 'image' ? "" : "content_paste") : modelData.isCommand ? modelData.icon : (modelData.icon ? Quickshell.iconPath(modelData.icon, "application-x-executable") : "") visible: (modelData.isCalculator || modelData.isClipboard || modelData.isCommand @@ -436,10 +436,10 @@ NLoader { // Fallback icon container Rectangle { anchors.fill: parent - anchors.margins: 6 * scaling - radius: 10 * scaling + anchors.margins: Style.marginTiny * scaling + radius: Style.radiusTiny * scaling color: Colors.mPrimary - opacity: 0.3 + opacity: Style.opacityMedium visible: !parent.iconLoaded } @@ -448,14 +448,14 @@ NLoader { visible: !parent.iconLoaded && !(modelData.isCalculator || modelData.isClipboard || modelData.isCommand) text: modelData.name ? modelData.name.charAt(0).toUpperCase() : "?" - font.pointSize: 18 * scaling + font.pointSize: Style.fontSizeXL * scaling font.weight: Font.Bold color: Colors.mPrimary } Behavior on color { ColorAnimation { - duration: 150 + duration: Style.animationFast } } } @@ -463,11 +463,11 @@ NLoader { // App info ColumnLayout { Layout.fillWidth: true - spacing: 2 * scaling + spacing: Style.marginTiniest * scaling NText { text: modelData.name || "Unknown" - font.pointSize: 14 * scaling + font.pointSize: Style.fontSizeLarge * scaling font.weight: Font.Bold color: Colors.mOnSurface elide: Text.ElideRight @@ -476,7 +476,7 @@ NLoader { NText { text: modelData.isCalculator ? (modelData.expr + " = " + modelData.result) : modelData.isClipboard ? modelData.content : modelData.isCommand ? modelData.content : (modelData.genericName || modelData.comment || "") - font.pointSize: 11 * scaling + font.pointSize: Style.fontSizeMedium * scaling color: (appCardArea.containsMouse || isSelected) ? Colors.mOnSurface : Colors.mOnSurface elide: Text.ElideRight diff --git a/Modules/Audio/CircularSpectrum.qml b/Modules/Audio/CircularSpectrum.qml index a58079a..02371a8 100644 --- a/Modules/Audio/CircularSpectrum.qml +++ b/Modules/Audio/CircularSpectrum.qml @@ -45,7 +45,7 @@ Item { Behavior on height { SmoothedAnimation { - duration: 120 + duration: Style.animationFast } } } diff --git a/Modules/Bar/ActiveWindow.qml b/Modules/Bar/ActiveWindow.qml index 921f211..408ead5 100644 --- a/Modules/Bar/ActiveWindow.qml +++ b/Modules/Bar/ActiveWindow.qml @@ -16,7 +16,7 @@ Row { // Timer to hide full title after window switch Timer { id: fullTitleTimer - interval: 2000 // Show full title for 2 seconds + interval: Style.animationSlow * 4 // Show full title for 2 seconds repeat: false onTriggered: { showingFullTitle = false @@ -59,7 +59,7 @@ Row { Behavior on width { NumberAnimation { - duration: 300 + duration: Style.animationNormal easing.type: Easing.OutCubic } } diff --git a/Modules/Bar/TrayMenu.qml b/Modules/Bar/TrayMenu.qml index 5289a72..98f2dcb 100644 --- a/Modules/Bar/TrayMenu.qml +++ b/Modules/Bar/TrayMenu.qml @@ -13,7 +13,7 @@ PopupWindow { property real anchorX property real anchorY - implicitWidth: 180 * scaling + implicitWidth: Style.baseWidgetSize * 5.625 * scaling implicitHeight: Math.max(60 * scaling, listView.contentHeight + (Style.marginMedium * 2 * scaling)) visible: false color: "transparent" @@ -250,7 +250,7 @@ PopupWindow { PopupWindow { id: subMenu - implicitWidth: 180 * scaling + implicitWidth: Style.baseWidgetSize * 5.625 * scaling implicitHeight: Math.max(40, listView.contentHeight + 12) visible: false color: "transparent" diff --git a/Modules/Bar/WiFiMenu.qml b/Modules/Bar/WiFiMenu.qml index 9163c4e..3d9ac0f 100644 --- a/Modules/Bar/WiFiMenu.qml +++ b/Modules/Bar/WiFiMenu.qml @@ -86,9 +86,9 @@ NLoader { Rectangle { id: wifiMenuRect color: Colors.mSurface - radius: Style.radiusMedium * scaling - border.color: Colors.mOutline - border.width: Math.max(1, Style.borderMedium * scaling) + radius: Style.radiusLarge * scaling + border.color: Colors.mOutlineVariant + border.width: Math.max(1, Style.borderThin * scaling) width: 340 * scaling height: 320 * scaling anchors.top: parent.top @@ -306,8 +306,8 @@ NLoader { } Item { - Layout.preferredWidth: 22 - Layout.preferredHeight: 22 + Layout.preferredWidth: Style.baseWidgetSize * 0.7 * scaling + Layout.preferredHeight: Style.baseWidgetSize * 0.7 * scaling visible: network.connectStatusSsid === modelData.ssid && (network.connectStatus !== "" || network.connectingSsid === modelData.ssid) @@ -324,7 +324,7 @@ NLoader { visible: network.connectStatus === "success" && !network.connectingSsid text: "check_circle" font.family: "Material Symbols Outlined" - font.pointSize: 18 * scaling + font.pointSize: Style.fontSizeXL * scaling color: "#43a047" // TBC: No! anchors.centerIn: parent } @@ -374,7 +374,7 @@ NLoader { id: passwordPromptSection Layout.fillWidth: true Layout.preferredHeight: modelData.ssid === passwordPromptSsid && showPasswordPrompt ? 60 : 0 - Layout.margins: 8 + Layout.margins: Style.marginSmall * scaling visible: modelData.ssid === passwordPromptSsid && showPasswordPrompt color: Colors.mSurfaceVariant radius: Style.radiusSmall * scaling @@ -386,14 +386,14 @@ NLoader { Item { Layout.fillWidth: true - Layout.preferredHeight: 36 + Layout.preferredHeight: Style.barHeight * scaling Rectangle { anchors.fill: parent - radius: 8 + radius: Style.radiusTiny * scaling color: "transparent" border.color: passwordInputField.activeFocus ? Colors.mPrimary : Colors.mOutline - border.width: 1 + border.width: Math.max(1, Style.borderThin * scaling) TextInput { id: passwordInputField @@ -425,12 +425,10 @@ NLoader { } Rectangle { - Layout.preferredWidth: 80 - Layout.preferredHeight: 36 + Layout.preferredWidth: Style.baseWidgetSize * 2.5 * scaling + Layout.preferredHeight: Style.barHeight * scaling radius: Style.radiusMedium * scaling color: Colors.mPrimary - border.color: Colors.mPrimary - border.width: 0 Behavior on color { ColorAnimation { diff --git a/Modules/Calendar/Calendar.qml b/Modules/Calendar/Calendar.qml index a51b8d3..48846c9 100644 --- a/Modules/Calendar/Calendar.qml +++ b/Modules/Calendar/Calendar.qml @@ -221,7 +221,7 @@ NLoader { Behavior on color { ColorAnimation { - duration: 150 + duration: Style.animationFast } } } diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml index 4ae7b3b..9b4a18f 100644 --- a/Modules/Dock/Dock.qml +++ b/Modules/Dock/Dock.qml @@ -26,11 +26,11 @@ NLoader { property bool hidden: autoHide // Start hidden only if auto-hide is enabled property int hideDelay: 500 property int showDelay: 100 - property int hideAnimationDuration: 200 - property int showAnimationDuration: 150 + property int hideAnimationDuration: Style.animationFast + property int showAnimationDuration: Style.animationFast property int peekHeight: 2 property int fullHeight: dockContainer.height - property int iconSize: 48 + property int iconSize: 36 // Track hover state property bool dockHovered: false @@ -51,7 +51,7 @@ NLoader { anchors.right: true focusable: false color: "transparent" - implicitHeight: iconSize * 1.5 * scaling + implicitHeight: iconSize * 1.4 * scaling // Timer for auto-hide delay Timer { @@ -110,8 +110,8 @@ NLoader { Rectangle { id: dockContainer width: dock.width + 48 * scaling - height: iconSize * 1.5 * scaling - color: Colors.mSurfaceVariant + height: iconSize * 1.4 * scaling + color: Colors.mSurface anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom topLeftRadius: Style.radiusLarge * scaling @@ -258,7 +258,7 @@ NLoader { radius: Style.radiusTiny anchors.top: parent.bottom anchors.horizontalCenter: parent.horizontalCenter - anchors.topMargin: 2 * scaling + anchors.topMargin: Style.marginTiniest * scaling } } } @@ -290,12 +290,12 @@ NLoader { Rectangle { id: contextMenuContainer - width: 80 - height: 32 - radius: 8 + width: Style.baseWidgetSize * 2.5 * scaling + height: Style.baseWidgetSize * scaling + radius: Style.radiusTiny * scaling color: closeMouseArea.containsMouse ? Colors.mTertiary : Colors.mSurface border.color: Colors.mOutline - border.width: 1 + border.width: Math.max(1, Style.borderThin * scaling) x: { if (!contextMenuTarget) @@ -315,7 +315,7 @@ NLoader { Text { anchors.centerIn: parent text: "Close" - font.pixelSize: 14 + font.pointSize: Style.fontSizeMedium * scaling color: Colors.mOnSurface } @@ -340,14 +340,14 @@ NLoader { Behavior on scale { NumberAnimation { - duration: 150 + duration: Style.animationFast easing.type: Easing.OutBack } } Behavior on opacity { NumberAnimation { - duration: 100 + duration: Style.animationFast } } } diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index 9efce8e..e8e289c 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -174,7 +174,7 @@ WlSessionLock { // Time display - Large and prominent with pulse animation Column { - spacing: 8 * scaling + spacing: Style.marginSmall * scaling Layout.alignment: Qt.AlignHCenter Text { @@ -216,7 +216,7 @@ WlSessionLock { // User section with animated avatar Column { - spacing: 16 * scaling + spacing: Style.marginMedium * scaling Layout.alignment: Qt.AlignHCenter // Animated avatar with glow effect @@ -226,7 +226,7 @@ WlSessionLock { radius: width * 0.5 color: "transparent" border.color: Colors.mPrimary - border.width: 3 * scaling + border.width: Math.max(1, Style.borderThick * scaling) anchors.horizontalCenter: parent.horizontalCenter // Glow effect @@ -237,7 +237,7 @@ WlSessionLock { radius: width * 0.5 color: "transparent" border.color: Qt.rgba(Colors.mPrimary.r, Colors.mPrimary.g, Colors.mPrimary.b, 0.3) - border.width: 2 * scaling + border.width: Math.max(1, Style.borderMedium * scaling) z: -1 SequentialAnimation on scale { @@ -274,7 +274,7 @@ WlSessionLock { Behavior on scale { NumberAnimation { - duration: 200 + duration: Style.animationFast easing.type: Easing.OutBack } } @@ -303,10 +303,10 @@ WlSessionLock { Rectangle { id: terminalBackground anchors.fill: parent - radius: 16 + radius: Style.radiusMedium * scaling color: Colors.applyOpacity(Colors.mSurface, "E6") border.color: Colors.mPrimary - border.width: 2 * scaling + border.width: Math.max(1, Style.borderMedium * scaling) // Scanline effect Repeater { @@ -316,7 +316,7 @@ WlSessionLock { height: 1 color: Colors.applyOpacity(Colors.mPrimary, "1A") y: index * 10 - opacity: 0.3 + opacity: Style.opacityMedium SequentialAnimation on opacity { loops: Animation.Infinite @@ -337,13 +337,13 @@ WlSessionLock { width: parent.width height: 40 * scaling color: Colors.applyOpacity(Colors.mPrimary, "33") - topLeftRadius: 14 - topRightRadius: 14 + topLeftRadius: Style.radiusSmall * scaling + topRightRadius: Style.radiusSmall * scaling RowLayout { anchors.fill: parent - anchors.margins: 12 * scaling - spacing: 12 * scaling + anchors.margins: Style.marginMedium * scaling + spacing: Style.marginMedium * scaling Text { text: "SECURE TERMINAL" @@ -363,13 +363,13 @@ WlSessionLock { anchors.right: parent.right anchors.bottom: parent.bottom anchors.topMargin: 70 * scaling - anchors.margins: 12 * scaling - spacing: 12 * scaling + anchors.margins: Style.marginMedium * scaling + spacing: Style.marginMedium * scaling // Welcome back typing effect RowLayout { Layout.fillWidth: true - spacing: 12 * scaling + spacing: Style.marginMedium * scaling Text { text: "root@noctalia:~$" @@ -389,7 +389,7 @@ WlSessionLock { property string fullText: "Welcome back, " + Quickshell.env("USER") + "!" Timer { - interval: 100 + interval: Style.animationFast running: true repeat: true onTriggered: { @@ -407,7 +407,7 @@ WlSessionLock { // Command line with integrated password input RowLayout { Layout.fillWidth: true - spacing: 12 * scaling + spacing: Style.marginMedium * scaling Text { text: "root@noctalia:~$" @@ -489,7 +489,7 @@ WlSessionLock { color: Colors.mPrimary visible: passwordInput.activeFocus anchors.left: asterisksText.right - anchors.leftMargin: 2 * scaling + anchors.leftMargin: Style.marginTiniest * scaling anchors.verticalCenter: asterisksText.verticalCenter SequentialAnimation on opacity { @@ -532,10 +532,10 @@ WlSessionLock { Rectangle { width: 120 * scaling height: 40 * scaling - radius: 12 + radius: Style.radiusSmall * scaling color: executeButtonArea.containsMouse ? Colors.mPrimary : Colors.applyOpacity(Colors.mPrimary, "33") border.color: Colors.mPrimary - border.width: 1 + border.width: Math.max(1, Style.borderThin * scaling) enabled: !lock.authenticating Layout.alignment: Qt.AlignRight Layout.bottomMargin: -12 * scaling @@ -559,7 +559,7 @@ WlSessionLock { running: containsMouse NumberAnimation { to: 1.05 - duration: 150 + duration: Style.animationFast easing.type: Easing.OutCubic } } @@ -568,7 +568,7 @@ WlSessionLock { running: !containsMouse NumberAnimation { to: 1.0 - duration: 150 + duration: Style.animationFast easing.type: Easing.OutCubic } } @@ -598,7 +598,7 @@ WlSessionLock { radius: parent.radius color: "transparent" border.color: Colors.applyOpacity(Colors.mPrimary, "4D") - border.width: 1 + border.width: Math.max(1, Style.borderThin * scaling) z: -1 SequentialAnimation on opacity { @@ -632,10 +632,10 @@ WlSessionLock { Rectangle { width: 64 * scaling height: 64 * scaling - radius: 32 + radius: Style.radiusLarge * scaling color: Qt.rgba(Colors.mError.r, Colors.mError.g, Colors.mError.b, shutdownArea.containsMouse ? 0.9 : 0.2) border.color: Colors.mError - border.width: 2 * scaling + border.width: Math.max(1, Style.borderMedium * scaling) // Glow effect Rectangle { @@ -645,13 +645,13 @@ WlSessionLock { radius: width * 0.5 color: "transparent" border.color: Qt.rgba(Colors.mError.r, Colors.mError.g, Colors.mError.b, 0.3) - border.width: 2 * scaling + border.width: Math.max(1, Style.borderMedium * scaling) opacity: shutdownArea.containsMouse ? 1 : 0 z: -1 Behavior on opacity { NumberAnimation { - duration: 200 + duration: Style.animationFast easing.type: Easing.OutCubic } } @@ -672,13 +672,13 @@ WlSessionLock { anchors.centerIn: parent text: "power_settings_new" font.family: "Material Symbols Outlined" - font.pixelSize: 28 * scaling + font.pointSize: Style.fontSizeXXL * scaling color: shutdownArea.containsMouse ? Colors.onAccent : Colors.mError } Behavior on color { ColorAnimation { - duration: 200 + duration: Style.animationFast easing.type: Easing.OutCubic } } @@ -689,10 +689,10 @@ WlSessionLock { Rectangle { width: 64 * scaling height: 64 * scaling - radius: 32 + radius: Style.radiusLarge * scaling color: Qt.rgba(Colors.mPrimary.r, Colors.mPrimary.g, Colors.mPrimary.b, rebootArea.containsMouse ? 0.9 : 0.2) border.color: Colors.mPrimary - border.width: 2 * scaling + border.width: Math.max(1, Style.borderMedium * scaling) // Glow effect Rectangle { @@ -702,13 +702,13 @@ WlSessionLock { radius: width * 0.5 color: "transparent" border.color: Qt.rgba(Colors.mPrimary.r, Colors.mPrimary.g, Colors.mPrimary.b, 0.3) - border.width: 2 * scaling + border.width: Math.max(1, Style.borderMedium * scaling) opacity: rebootArea.containsMouse ? 1 : 0 z: -1 Behavior on opacity { NumberAnimation { - duration: 200 + duration: Style.animationFast easing.type: Easing.OutCubic } } @@ -728,13 +728,13 @@ WlSessionLock { anchors.centerIn: parent text: "refresh" font.family: "Material Symbols Outlined" - font.pixelSize: 28 * scaling + font.pointSize: Style.fontSizeXXL * scaling color: rebootArea.containsMouse ? Colors.onAccent : Colors.mPrimary } Behavior on color { ColorAnimation { - duration: 200 + duration: Style.animationFast easing.type: Easing.OutCubic } } @@ -745,11 +745,11 @@ WlSessionLock { Rectangle { width: 64 * scaling height: 64 * scaling - radius: 32 + radius: Style.radiusLarge * scaling color: Qt.rgba(Colors.mSecondary.r, Colors.mSecondary.g, Colors.mSecondary.b, logoutArea.containsMouse ? 0.9 : 0.2) border.color: Colors.mSecondary - border.width: 2 * scaling + border.width: Math.max(1, Style.borderMedium * scaling) // Glow effect Rectangle { @@ -759,13 +759,13 @@ WlSessionLock { radius: width * 0.5 color: "transparent" border.color: Qt.rgba(Colors.mSecondary.r, Colors.mSecondary.g, Colors.mSecondary.b, 0.3) - border.width: 2 * scaling + border.width: Math.max(1, Style.borderMedium * scaling) opacity: logoutArea.containsMouse ? 1 : 0 z: -1 Behavior on opacity { NumberAnimation { - duration: 200 + duration: Style.animationFast easing.type: Easing.OutCubic } } @@ -787,13 +787,13 @@ WlSessionLock { anchors.centerIn: parent text: "exit_to_app" font.family: "Material Symbols Outlined" - font.pixelSize: 28 * scaling + font.pointSize: Style.fontSizeXXL * scaling color: logoutArea.containsMouse ? Colors.onAccent : Colors.mSecondary } Behavior on color { ColorAnimation { - duration: 200 + duration: Style.animationFast easing.type: Easing.OutCubic } } diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index 9ce4234..294c93c 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -58,7 +58,7 @@ Variants { id: notificationStack anchors.top: parent.top anchors.right: parent.right - spacing: 8 * scaling + spacing: Style.marginSmall * scaling width: 360 * scaling visible: true @@ -146,7 +146,7 @@ Variants { Rectangle { width: 6 * scaling height: 6 * scaling - radius: 3 * scaling + radius: Style.radiusTiny * scaling color: (model.urgency === NotificationUrgency.Critical) ? Colors.mError : (model.urgency === NotificationUrgency.Low) ? Colors.mOnSurface : Colors.mPrimary Layout.alignment: Qt.AlignVCenter } diff --git a/Modules/Notification/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml index 146b1a7..4ee40f6 100644 --- a/Modules/Notification/NotificationHistoryPanel.qml +++ b/Modules/Notification/NotificationHistoryPanel.qml @@ -65,10 +65,10 @@ NLoader { Rectangle { id: notificationRect - color: Colors.mSurfaceVariant - radius: Style.radiusMedium * scaling + color: Colors.mSurface + radius: Style.radiusLarge * scaling border.color: Colors.mOutlineVariant - border.width: Math.max(1, Style.borderMedium * scaling) + border.width: Math.max(1, Style.borderThin * scaling) width: 400 * scaling height: 500 * scaling anchors.top: parent.top @@ -165,16 +165,16 @@ NLoader { RowLayout { anchors { fill: parent - margins: 15 + margins: Style.marginMedium * scaling } - spacing: 15 + spacing: Style.marginMedium * scaling // Notification content Column { id: notificationContent Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter - spacing: 5 + spacing: Style.marginTiniest * scaling NText { text: (summary || "No summary").substring(0, 100) @@ -220,7 +220,7 @@ NLoader { MouseArea { id: notificationMouseArea anchors.fill: parent - anchors.rightMargin: 50 * scaling + anchors.rightMargin: Style.marginLarge * 3 * scaling hoverEnabled: true // Remove the onClicked handler since we now have a dedicated delete button } diff --git a/Modules/Settings/SettingsPanel.qml b/Modules/Settings/SettingsPanel.qml index 68f5a36..3099a7f 100644 --- a/Modules/Settings/SettingsPanel.qml +++ b/Modules/Settings/SettingsPanel.qml @@ -234,7 +234,7 @@ NLoader { Rectangle { id: sidebar - Layout.preferredWidth: 260 * scaling + Layout.preferredWidth: Style.sliderWidth * 1.3 * scaling Layout.fillHeight: true color: Colors.mSurfaceVariant border.color: Colors.mOutlineVariant diff --git a/Modules/Settings/Tabs/AboutTab.qml b/Modules/Settings/Tabs/AboutTab.qml index 3153379..2fd7973 100644 --- a/Modules/Settings/Tabs/AboutTab.qml +++ b/Modules/Settings/Tabs/AboutTab.qml @@ -107,11 +107,11 @@ ColumnLayout { Layout.alignment: Qt.AlignCenter Layout.topMargin: Style.marginSmall * scaling Layout.preferredWidth: updateText.implicitWidth + 46 * scaling - Layout.preferredHeight: 32 * scaling + Layout.preferredHeight: Style.barHeight * scaling radius: Style.radiusLarge * scaling color: updateArea.containsMouse ? Colors.mPrimary : "transparent" border.color: Colors.mPrimary - border.width: 1 + border.width: Math.max(1, Style.borderThin * scaling) visible: { if (root.currentVersion === "Unknown" || root.latestVersion === "Unknown") return false @@ -132,19 +132,19 @@ ColumnLayout { RowLayout { anchors.centerIn: parent - spacing: 8 + spacing: Style.marginSmall * scaling NText { text: "system_update" font.family: "Material Symbols Outlined" - font.pointSize: 18 * scaling + font.pointSize: Style.fontSizeXL * scaling color: updateArea.containsMouse ? Colors.mSurface : Colors.mPrimary } NText { id: updateText text: "Download latest release" - font.pointSize: 14 * scaling + font.pointSize: Style.fontSizeLarge * scaling color: updateArea.containsMouse ? Colors.mSurface : Colors.mPrimary } } @@ -191,8 +191,8 @@ ColumnLayout { anchors.fill: parent width: 200 * 4 * scaling height: Math.ceil(root.contributors.length / 4) * 100 - cellWidth: 200 * scaling - cellHeight: 100 * scaling + cellWidth: Style.baseWidgetSize * 6.25 * scaling + cellHeight: Style.baseWidgetSize * 3.125 * scaling model: root.contributors delegate: Rectangle { @@ -208,8 +208,8 @@ ColumnLayout { Item { Layout.alignment: Qt.AlignVCenter - Layout.preferredWidth: 64 * scaling - Layout.preferredHeight: 64 * scaling + Layout.preferredWidth: Style.baseWidgetSize * 2 * scaling + Layout.preferredHeight: Style.baseWidgetSize * 2 * scaling NImageRounded { imagePath: modelData.avatar_url || "" diff --git a/Modules/Settings/Tabs/AudioTab.qml b/Modules/Settings/Tabs/AudioTab.qml index 3314c4e..36e3e2d 100644 --- a/Modules/Settings/Tabs/AudioTab.qml +++ b/Modules/Settings/Tabs/AudioTab.qml @@ -86,7 +86,7 @@ ColumnLayout { // Probably because they have some quick fades in and out to avoid clipping // We use a timer to space out the updates, to avoid lock up Timer { - interval: 100 + interval: Style.animationFast running: true repeat: true onTriggered: { diff --git a/Modules/Settings/Tabs/ColorSchemeTab.qml b/Modules/Settings/Tabs/ColorSchemeTab.qml index a8fe35c..c984552 100644 --- a/Modules/Settings/Tabs/ColorSchemeTab.qml +++ b/Modules/Settings/Tabs/ColorSchemeTab.qml @@ -30,6 +30,10 @@ ColumnLayout { // Array to hold FileView objects property var fileViews: [] + // Scale properties for card animations + property real cardScaleLow: 0.95 + property real cardScaleHigh: 1.0 + // Load color scheme data when schemes are available Connections { target: ColorSchemes @@ -100,8 +104,6 @@ ColumnLayout { } function schemeLoaded(schemeName, jsonData) { - console.log("Loading scheme colors for:", schemeName) - var colors = {} // Extract colors from JSON data @@ -130,8 +132,6 @@ ColumnLayout { var newCache = schemeColorsCache newCache[schemeName] = colors schemeColorsCache = newCache - - console.log("Cached colors for", schemeName, ":", JSON.stringify(colors)) } ScrollView { @@ -212,9 +212,6 @@ ColumnLayout { Repeater { model: ColorSchemes.schemes - property real cardScaleLow: 0.95 - property real cardScaleHigh: 1.00 - Rectangle { id: schemeCard Layout.fillWidth: true @@ -223,7 +220,7 @@ ColumnLayout { color: getSchemeColor(modelData, "mSurface") border.width: Math.max(1, Style.borderThick * scaling) border.color: Settings.data.colorSchemes.predefinedScheme === modelData ? Colors.mPrimary : Colors.mOutline - scale: cardScaleLow + scale: root.cardScaleLow property string schemePath: modelData @@ -240,11 +237,11 @@ ColumnLayout { cursorShape: Qt.PointingHandCursor onEntered: { - schemeCard.scale = cardScaleHight + schemeCard.scale = root.cardScaleHigh } onExited: { - schemeCard.scale = cardScaleLow + schemeCard.scale = root.cardScaleLow } } @@ -287,7 +284,7 @@ ColumnLayout { Rectangle { width: 28 * scaling height: 28 * scaling - radius: 14 * scaling + radius: Style.radiusSmall * scaling color: getSchemeColor(modelData, "mSecondary") } diff --git a/Modules/Settings/Tabs/TimeWeatherTab.qml b/Modules/Settings/Tabs/TimeWeatherTab.qml index 0847341..47cbf7c 100644 --- a/Modules/Settings/Tabs/TimeWeatherTab.qml +++ b/Modules/Settings/Tabs/TimeWeatherTab.qml @@ -74,7 +74,7 @@ ColumnLayout { font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold color: Colors.mOnSurface - Layout.bottomMargin: 8 + Layout.bottomMargin: Style.marginSmall * scaling } NToggle { diff --git a/Modules/Settings/Tabs/WallpaperSelectorTab.qml b/Modules/Settings/Tabs/WallpaperSelectorTab.qml index 5100b9a..0077ff8 100644 --- a/Modules/Settings/Tabs/WallpaperSelectorTab.qml +++ b/Modules/Settings/Tabs/WallpaperSelectorTab.qml @@ -149,8 +149,7 @@ Item { anchors.margins: Style.marginTiny * scaling imagePath: wallpaperPath fallbackIcon: "image" - borderColor: "transparent" - borderWidth: 0 + imageRadius: Style.radiusMedium * scaling } @@ -185,7 +184,7 @@ Item { Behavior on opacity { NumberAnimation { - duration: 150 + duration: Style.animationFast } } } @@ -235,7 +234,7 @@ Item { color: Colors.mOnSurface wrapMode: Text.WordWrap horizontalAlignment: Text.AlignHCenter - Layout.preferredWidth: 300 * scaling + Layout.preferredWidth: Style.sliderWidth * 1.5 * scaling } } } diff --git a/Modules/Settings/Tabs/WallpaperTab.qml b/Modules/Settings/Tabs/WallpaperTab.qml index 50e6377..9e75c84 100644 --- a/Modules/Settings/Tabs/WallpaperTab.qml +++ b/Modules/Settings/Tabs/WallpaperTab.qml @@ -149,7 +149,7 @@ ColumnLayout { font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold color: Colors.mOnSurface - Layout.bottomMargin: 8 + Layout.bottomMargin: Style.marginSmall * scaling } // Use SWWW diff --git a/Modules/SidePanel/Cards/MediaCard.qml b/Modules/SidePanel/Cards/MediaCard.qml index 43b1594..4a9a47f 100644 --- a/Modules/SidePanel/Cards/MediaCard.qml +++ b/Modules/SidePanel/Cards/MediaCard.qml @@ -65,7 +65,7 @@ NBox { ComboBox { id: playerSelector Layout.fillWidth: true - Layout.preferredHeight: 30 * scaling + Layout.preferredHeight: Style.barHeight * 0.83 * scaling visible: MediaPlayer.getAvailablePlayers().length > 1 model: MediaPlayer.getAvailablePlayers() textRole: "identity" @@ -252,7 +252,7 @@ NBox { Behavior on width { NumberAnimation { - duration: 200 + duration: Style.animationFast } } } @@ -273,7 +273,7 @@ NBox { Behavior on scale { NumberAnimation { - duration: 150 + duration: Style.animationFast } } } diff --git a/Modules/SidePanel/Cards/ProfileCard.qml b/Modules/SidePanel/Cards/ProfileCard.qml index d8921d3..8b17fb2 100644 --- a/Modules/SidePanel/Cards/ProfileCard.qml +++ b/Modules/SidePanel/Cards/ProfileCard.qml @@ -38,7 +38,7 @@ NBox { ColumnLayout { Layout.fillWidth: true - spacing: 2 * scaling + spacing: Style.marginTiniest * scaling NText { text: Quickshell.env("USER") || "user" font.weight: Style.fontWeightBold diff --git a/Modules/SidePanel/Cards/SystemMonitorCard.qml b/Modules/SidePanel/Cards/SystemMonitorCard.qml index a4633b1..f353896 100644 --- a/Modules/SidePanel/Cards/SystemMonitorCard.qml +++ b/Modules/SidePanel/Cards/SystemMonitorCard.qml @@ -7,7 +7,7 @@ import qs.Widgets NBox { id: root - Layout.preferredWidth: 84 * scaling + Layout.preferredWidth: Style.baseWidgetSize * 2.625 * scaling implicitHeight: content.implicitHeight + Style.marginTiny * 2 * scaling Column { diff --git a/Modules/SidePanel/PowerMenu.qml b/Modules/SidePanel/PowerMenu.qml index 7a434f5..28139f4 100644 --- a/Modules/SidePanel/PowerMenu.qml +++ b/Modules/SidePanel/PowerMenu.qml @@ -54,7 +54,7 @@ NPanel { // Lock Rectangle { Layout.fillWidth: true - Layout.preferredHeight: 36 * scaling + Layout.preferredHeight: Style.barHeight * scaling radius: Style.radiusSmall * scaling color: lockButtonArea.containsMouse ? Colors.mTertiary : "transparent" @@ -114,7 +114,7 @@ NPanel { // Suspend Rectangle { Layout.fillWidth: true - Layout.preferredHeight: 36 * scaling + Layout.preferredHeight: Style.barHeight * scaling radius: Style.radiusSmall * scaling color: suspendButtonArea.containsMouse ? Colors.mTertiary : "transparent" @@ -172,7 +172,7 @@ NPanel { // Reboot Rectangle { Layout.fillWidth: true - Layout.preferredHeight: 36 * scaling + Layout.preferredHeight: Style.barHeight * scaling radius: Style.radiusSmall * scaling color: rebootButtonArea.containsMouse ? Colors.mTertiary : "transparent" @@ -230,7 +230,7 @@ NPanel { // Logout Rectangle { Layout.fillWidth: true - Layout.preferredHeight: 36 * scaling + Layout.preferredHeight: Style.barHeight * scaling radius: Style.radiusSmall * scaling color: logoutButtonArea.containsMouse ? Colors.mTertiary : "transparent" @@ -288,7 +288,7 @@ NPanel { // Shutdown Rectangle { Layout.fillWidth: true - Layout.preferredHeight: 36 * scaling + Layout.preferredHeight: Style.barHeight * scaling radius: Style.radiusSmall * scaling color: shutdownButtonArea.containsMouse ? Colors.mTertiary : "transparent" diff --git a/Widgets/NCircleStat.qml b/Widgets/NCircleStat.qml index c5c0672..d41806c 100644 --- a/Widgets/NCircleStat.qml +++ b/Widgets/NCircleStat.qml @@ -94,7 +94,7 @@ Rectangle { anchors.right: parent.right anchors.top: parent.top anchors.rightMargin: -6 * scaling * contentScale - anchors.topMargin: 4 * scaling * contentScale + anchors.topMargin: Style.marginTiniest * scaling * contentScale Text { anchors.centerIn: parent diff --git a/Widgets/NComboBox.qml b/Widgets/NComboBox.qml index a89d6bc..4504d49 100644 --- a/Widgets/NComboBox.qml +++ b/Widgets/NComboBox.qml @@ -58,7 +58,7 @@ ColumnLayout { } background: Rectangle { - implicitWidth: 120 * scaling + implicitWidth: Style.baseWidgetSize * 3.75 * scaling implicitHeight: preferredHeight color: Colors.mSurface border.color: combo.activeFocus ? Colors.mTertiary : Colors.mOutline diff --git a/Widgets/NRadioButton.qml b/Widgets/NRadioButton.qml index ce94040..d9b232f 100644 --- a/Widgets/NRadioButton.qml +++ b/Widgets/NRadioButton.qml @@ -9,12 +9,12 @@ RadioButton { indicator: Rectangle { id: outerCircle - implicitWidth: 20 * scaling - implicitHeight: 20 * scaling + implicitWidth: Style.baseWidgetSize * 0.625 * scaling + implicitHeight: Style.baseWidgetSize * 0.625 * scaling radius: width * 0.5 color: "transparent" border.color: root.checked ? Colors.mPrimary : Colors.mOnSurface - border.width: 2 + border.width: Math.max(1, Style.borderMedium * scaling) anchors.verticalCenter: parent.verticalCenter Rectangle { diff --git a/Widgets/NTextInput.qml b/Widgets/NTextInput.qml index db61e2d..4c30955 100644 --- a/Widgets/NTextInput.qml +++ b/Widgets/NTextInput.qml @@ -17,7 +17,7 @@ Item { signal editingFinished // Sizing - implicitWidth: 320 * scaling + implicitWidth: Style.sliderWidth * 1.6 * scaling implicitHeight: Style.baseWidgetSize * 2.75 * scaling ColumnLayout { diff --git a/Widgets/NToggle.qml b/Widgets/NToggle.qml index fd42e39..bc068f6 100644 --- a/Widgets/NToggle.qml +++ b/Widgets/NToggle.qml @@ -60,7 +60,7 @@ RowLayout { Behavior on x { NumberAnimation { - duration: 200 + duration: Style.animationFast easing.type: Easing.OutCubic } } diff --git a/Widgets/NTooltip.qml b/Widgets/NTooltip.qml index 28bfd55..3af7657 100644 --- a/Widgets/NTooltip.qml +++ b/Widgets/NTooltip.qml @@ -121,7 +121,7 @@ Window { // Timer to trigger show animation Timer { id: showTimer - interval: 10 // Very short delay to ensure component is visible + interval: Style.animationFast / 15 // Very short delay to ensure component is visible repeat: false onTriggered: { // Animate to final values From 9990a88e902ddc6de1b7b15840fee029c2221fbc Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 21:05:47 -0400 Subject: [PATCH 337/394] Tooltips on all NIconButtons --- Modules/AppLauncher/AppLauncher.qml | 13 ++++++++----- Modules/Audio/CircularSpectrum.qml | 2 +- Modules/Bar/ActiveWindow.qml | 4 ++-- Modules/Bar/Bar.qml | 4 ++-- Modules/Bar/Brightness.qml | 2 +- Modules/Bar/TrayMenu.qml | 2 +- Modules/Bar/Volume.qml | 12 +++++++++++- Modules/Bar/WiFiMenu.qml | 2 ++ Modules/Calendar/Calendar.qml | 2 ++ Modules/Demo/DemoPanel.qml | 4 ++++ Modules/Notification/Notification.qml | 6 ++++-- Modules/Notification/NotificationHistoryPanel.qml | 6 ++++-- Modules/SidePanel/Cards/MediaCard.qml | 9 ++++++++- Modules/SidePanel/Cards/PowerProfilesCard.qml | 3 +++ Modules/SidePanel/Cards/ProfileCard.qml | 6 ++++-- Modules/SidePanel/Cards/UtilitiesCard.qml | 2 ++ Widgets/NCircleStat.qml | 2 +- Widgets/NToggle.qml | 2 +- Widgets/NTooltip.qml | 4 ++-- 19 files changed, 63 insertions(+), 24 deletions(-) diff --git a/Modules/AppLauncher/AppLauncher.qml b/Modules/AppLauncher/AppLauncher.qml index 1735540..e3aa4c5 100644 --- a/Modules/AppLauncher/AppLauncher.qml +++ b/Modules/AppLauncher/AppLauncher.qml @@ -283,7 +283,9 @@ NLoader { radius: Style.radiusMedium * scaling color: Colors.mSurface border.color: searchInput.activeFocus ? Colors.mPrimary : Colors.mOutline - border.width: Math.max(1, searchInput.activeFocus ? Style.borderMedium * scaling : Style.borderThin * scaling) + border.width: Math.max( + 1, + searchInput.activeFocus ? Style.borderMedium * scaling : Style.borderThin * scaling) Row { anchors.fill: parent @@ -372,7 +374,8 @@ NLoader { color: (appCardArea.containsMouse || isSelected) ? Qt.darker(Colors.mPrimary, 1.1) : Colors.mSurface border.color: (appCardArea.containsMouse || isSelected) ? Colors.mPrimary : "transparent" - border.width: Math.max(1, (appCardArea.containsMouse || isSelected) ? Style.borderMedium * scaling : 0) + border.width: Math.max(1, (appCardArea.containsMouse + || isSelected) ? Style.borderMedium * scaling : 0) Behavior on color { ColorAnimation { @@ -414,9 +417,9 @@ NLoader { // Clipboard image display Image { id: clipboardImage - anchors.fill: parent - anchors.margins: Style.marginTiny * scaling - visible: modelData.type === 'image' + anchors.fill: parent + anchors.margins: Style.marginTiny * scaling + visible: modelData.type === 'image' source: modelData.data || "" fillMode: Image.PreserveAspectCrop asynchronous: true diff --git a/Modules/Audio/CircularSpectrum.qml b/Modules/Audio/CircularSpectrum.qml index 02371a8..467d855 100644 --- a/Modules/Audio/CircularSpectrum.qml +++ b/Modules/Audio/CircularSpectrum.qml @@ -45,7 +45,7 @@ Item { Behavior on height { SmoothedAnimation { - duration: Style.animationFast + duration: Style.animationFast } } } diff --git a/Modules/Bar/ActiveWindow.qml b/Modules/Bar/ActiveWindow.qml index 408ead5..822824b 100644 --- a/Modules/Bar/ActiveWindow.qml +++ b/Modules/Bar/ActiveWindow.qml @@ -16,7 +16,7 @@ Row { // Timer to hide full title after window switch Timer { id: fullTitleTimer - interval: Style.animationSlow * 4 // Show full title for 2 seconds + interval: Style.animationSlow * 4 // Show full title for 2 seconds repeat: false onTriggered: { showingFullTitle = false @@ -59,7 +59,7 @@ Row { Behavior on width { NumberAnimation { - duration: Style.animationNormal + duration: Style.animationNormal easing.type: Easing.OutCubic } } diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 14907dd..3f34a27 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -130,7 +130,7 @@ Variants { NIconButton { id: demoPanelToggle icon: "experiment" - tooltipText: "Open demo panel" + tooltipText: "Open Demo Panel" sizeMultiplier: 0.8 showBorder: false anchors.verticalCenter: parent.verticalCenter @@ -142,7 +142,7 @@ Variants { NIconButton { id: sidePanelToggle icon: "widgets" - tooltipText: "Open side panel" + tooltipText: "Open Side Panel" sizeMultiplier: 0.8 showBorder: false anchors.verticalCenter: parent.verticalCenter diff --git a/Modules/Bar/Brightness.qml b/Modules/Bar/Brightness.qml index 486611e..adc68cd 100644 --- a/Modules/Bar/Brightness.qml +++ b/Modules/Bar/Brightness.qml @@ -49,7 +49,7 @@ Item { icon: getIcon() iconCircleColor: Colors.mPrimary collapsedIconColor: Colors.mOnSurface - autoHide: true + autoHide: false // Important to be false so we can hover as long as we want text: Math.round(BrightnessService.brightness) + "%" tooltipText: "Brightness: " + Math.round( BrightnessService.brightness) + "%\nMethod: " + BrightnessService.currentMethod diff --git a/Modules/Bar/TrayMenu.qml b/Modules/Bar/TrayMenu.qml index 98f2dcb..be43d09 100644 --- a/Modules/Bar/TrayMenu.qml +++ b/Modules/Bar/TrayMenu.qml @@ -13,7 +13,7 @@ PopupWindow { property real anchorX property real anchorY - implicitWidth: Style.baseWidgetSize * 5.625 * scaling + implicitWidth: Style.baseWidgetSize * 5.625 * scaling implicitHeight: Math.max(60 * scaling, listView.contentHeight + (Style.marginMedium * 2 * scaling)) visible: false color: "transparent" diff --git a/Modules/Bar/Volume.qml b/Modules/Bar/Volume.qml index 65a3f33..f34726e 100644 --- a/Modules/Bar/Volume.qml +++ b/Modules/Bar/Volume.qml @@ -31,16 +31,26 @@ Item { firstVolumeReceived = true } else { pill.show() + externalHideTimer.restart() } } } + Timer { + id: externalHideTimer + running: false + interval: 1500 + onTriggered: { + pill.hide() + } + } + NPill { id: pill icon: getIcon() iconCircleColor: Colors.mPrimary collapsedIconColor: Colors.mOnSurface - autoHide: true + autoHide: false // Important to be false so we can hover as long as we want text: Math.floor(Audio.volume * 100) + "%" tooltipText: "Volume: " + Math.round( Audio.volume * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume." diff --git a/Modules/Bar/WiFiMenu.qml b/Modules/Bar/WiFiMenu.qml index 3d9ac0f..ff4ea54 100644 --- a/Modules/Bar/WiFiMenu.qml +++ b/Modules/Bar/WiFiMenu.qml @@ -164,6 +164,7 @@ NLoader { NIconButton { icon: "refresh" + tooltipText: "Refresh Networks" sizeMultiplier: 0.8 enabled: Settings.data.network.wifiEnabled && !network.isLoading onClicked: { @@ -173,6 +174,7 @@ NLoader { NIconButton { icon: "close" + tooltipText: "Close" sizeMultiplier: 0.8 onClicked: { wifiPanel.hide() diff --git a/Modules/Calendar/Calendar.qml b/Modules/Calendar/Calendar.qml index 48846c9..3596b2b 100644 --- a/Modules/Calendar/Calendar.qml +++ b/Modules/Calendar/Calendar.qml @@ -120,6 +120,7 @@ NLoader { NIconButton { icon: "chevron_left" + tooltipText: "Previous Month" onClicked: { let newDate = new Date(grid.year, grid.month - 1, 1) grid.year = newDate.getFullYear() @@ -138,6 +139,7 @@ NLoader { NIconButton { icon: "chevron_right" + tooltipText: "Next Month" onClicked: { let newDate = new Date(grid.year, grid.month + 1, 1) grid.year = newDate.getFullYear() diff --git a/Modules/Demo/DemoPanel.qml b/Modules/Demo/DemoPanel.qml index cd02fd7..6546ed1 100644 --- a/Modules/Demo/DemoPanel.qml +++ b/Modules/Demo/DemoPanel.qml @@ -154,6 +154,7 @@ NLoader { } NIconButton { icon: "refresh" + tooltipText: "Reset Scaling" fontPointSize: Style.fontSizeLarge * scaling onClicked: { Scaling.overrideEnabled = false @@ -178,6 +179,7 @@ NLoader { NIconButton { id: myIconButton icon: "celebration" + tooltipText: "A nice tooltip" fontPointSize: Style.fontSizeLarge * scaling } @@ -318,6 +320,7 @@ NLoader { spacing: Style.marginSmall * scaling NIconButton { icon: "brightness_low" + tooltipText: "Decrease Brightness" fontPointSize: Style.fontSizeLarge * scaling onClicked: { BrightnessService.decreaseBrightness() @@ -335,6 +338,7 @@ NLoader { } NIconButton { icon: "brightness_high" + tooltipText: "Increase Brightness" fontPointSize: Style.fontSizeLarge * scaling onClicked: { BrightnessService.increaseBrightness() diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index 294c93c..5b7e115 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -58,7 +58,7 @@ Variants { id: notificationStack anchors.top: parent.top anchors.right: parent.right - spacing: Style.marginSmall * scaling + spacing: Style.marginSmall * scaling width: 360 * scaling visible: true @@ -183,12 +183,14 @@ Variants { } NIconButton { + icon: "close" + tooltipText: "Close" sizeMultiplier: 0.8 showBorder: false anchors.top: parent.top anchors.right: parent.right anchors.margins: Style.marginSmall * scaling - icon: "close" + onClicked: { animateOut() } diff --git a/Modules/Notification/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml index 9b4dff2..829df38 100644 --- a/Modules/Notification/NotificationHistoryPanel.qml +++ b/Modules/Notification/NotificationHistoryPanel.qml @@ -131,13 +131,14 @@ NLoader { NIconButton { icon: "delete" + tooltipText: "Clear History" sizeMultiplier: 0.8 - tooltipText: "Clear history" onClicked: NotificationService.clearHistory() } NIconButton { icon: "close" + tooltipText: "Close" sizeMultiplier: 0.8 onClicked: { notificationPanel.hide() @@ -207,8 +208,9 @@ NLoader { // Trash icon button NIconButton { icon: "delete" + tooltipText: "Delete Notification" sizeMultiplier: 0.7 - tooltipText: "Delete notification" + onClicked: { console.log("[NotificationHistory] Removing notification:", summary) NotificationService.historyModel.remove(index) diff --git a/Modules/SidePanel/Cards/MediaCard.qml b/Modules/SidePanel/Cards/MediaCard.qml index 4a9a47f..5414789 100644 --- a/Modules/SidePanel/Cards/MediaCard.qml +++ b/Modules/SidePanel/Cards/MediaCard.qml @@ -40,7 +40,7 @@ NBox { text: "album" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXXL * 2.5 * scaling - color: Colors.mOnSurfaceVariant + color: Colors.mPrimary Layout.alignment: Qt.AlignHCenter } NText { @@ -182,6 +182,7 @@ NBox { NText { anchors.centerIn: parent text: "album" + color: Colors.mPrimary font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeLarge * 12 * scaling visible: !trackArt.visible @@ -310,18 +311,24 @@ NBox { // Previous button NIconButton { icon: "skip_previous" + tooltipText: "Previous Media" + visible: MediaPlayer.canGoPrevious onClicked: MediaPlayer.canGoPrevious ? MediaPlayer.previous() : {} } // Play/Pause button NIconButton { icon: MediaPlayer.isPlaying ? "pause" : "play_arrow" + tooltipText: MediaPlayer.isPlaying ? "Pause" : "Play" + visible: (MediaPlayer.canPlay || MediaPlayer.canPause) onClicked: (MediaPlayer.canPlay || MediaPlayer.canPause) ? MediaPlayer.playPause() : {} } // Next button NIconButton { icon: "skip_next" + tooltipText: "Next Media" + visible: MediaPlayer.canGoNext onClicked: MediaPlayer.canGoNext ? MediaPlayer.next() : {} } } diff --git a/Modules/SidePanel/Cards/PowerProfilesCard.qml b/Modules/SidePanel/Cards/PowerProfilesCard.qml index 8562967..8d6b49d 100644 --- a/Modules/SidePanel/Cards/PowerProfilesCard.qml +++ b/Modules/SidePanel/Cards/PowerProfilesCard.qml @@ -27,6 +27,7 @@ NBox { // Performance NIconButton { icon: "speed" + tooltipText: "Set Performance Power Profile" enabled: hasPP opacity: enabled ? Style.opacityFull : Style.opacityMedium showFilled: enabled && powerProfiles.profile === PowerProfile.Performance @@ -40,6 +41,7 @@ NBox { // Balanced NIconButton { icon: "balance" + tooltipText: "Set Balanced Power Profile" enabled: hasPP opacity: enabled ? Style.opacityFull : Style.opacityMedium showFilled: enabled && powerProfiles.profile === PowerProfile.Balanced @@ -53,6 +55,7 @@ NBox { // Eco NIconButton { icon: "eco" + tooltipText: "Set Eco Power Profile" enabled: hasPP opacity: enabled ? Style.opacityFull : Style.opacityMedium showFilled: enabled && powerProfiles.profile === PowerProfile.PowerSaver diff --git a/Modules/SidePanel/Cards/ProfileCard.qml b/Modules/SidePanel/Cards/ProfileCard.qml index 8b17fb2..99bef0c 100644 --- a/Modules/SidePanel/Cards/ProfileCard.qml +++ b/Modules/SidePanel/Cards/ProfileCard.qml @@ -38,7 +38,7 @@ NBox { ColumnLayout { Layout.fillWidth: true - spacing: Style.marginTiniest * scaling + spacing: Style.marginTiniest * scaling NText { text: Quickshell.env("USER") || "user" font.weight: Style.fontWeightBold @@ -57,15 +57,17 @@ NBox { } NIconButton { icon: "settings" - tooltipText: "Open settings" + tooltipText: "Open Settings" onClicked: { settingsPanel.requestedTab = SettingsPanel.Tab.General settingsPanel.isLoaded = !settingsPanel.isLoaded } } + NIconButton { id: powerButton icon: "power_settings_new" + tooltipText: "Power Menu" onClicked: { powerMenu.show() } diff --git a/Modules/SidePanel/Cards/UtilitiesCard.qml b/Modules/SidePanel/Cards/UtilitiesCard.qml index 9704d26..81e21f7 100644 --- a/Modules/SidePanel/Cards/UtilitiesCard.qml +++ b/Modules/SidePanel/Cards/UtilitiesCard.qml @@ -22,6 +22,7 @@ NBox { // Screen Recorder NIconButton { icon: "videocam" + tooltipText: ScreenRecorder.isRecording ? "Stop Screen Recording" : "Start Screen Recording" showFilled: ScreenRecorder.isRecording onClicked: { ScreenRecorder.toggleRecording() @@ -31,6 +32,7 @@ NBox { // Wallpaper NIconButton { icon: "image" + tooltipText: "Open Wallpaper Selector" onClicked: { settingsPanel.requestedTab = SettingsPanel.Tab.WallpaperSelector settingsPanel.isLoaded = true diff --git a/Widgets/NCircleStat.qml b/Widgets/NCircleStat.qml index d41806c..d1aeb6e 100644 --- a/Widgets/NCircleStat.qml +++ b/Widgets/NCircleStat.qml @@ -94,7 +94,7 @@ Rectangle { anchors.right: parent.right anchors.top: parent.top anchors.rightMargin: -6 * scaling * contentScale - anchors.topMargin: Style.marginTiniest * scaling * contentScale + anchors.topMargin: Style.marginTiniest * scaling * contentScale Text { anchors.centerIn: parent diff --git a/Widgets/NToggle.qml b/Widgets/NToggle.qml index bc068f6..735a7ae 100644 --- a/Widgets/NToggle.qml +++ b/Widgets/NToggle.qml @@ -60,7 +60,7 @@ RowLayout { Behavior on x { NumberAnimation { - duration: Style.animationFast + duration: Style.animationFast easing.type: Easing.OutCubic } } diff --git a/Widgets/NTooltip.qml b/Widgets/NTooltip.qml index 3af7657..516a5af 100644 --- a/Widgets/NTooltip.qml +++ b/Widgets/NTooltip.qml @@ -37,7 +37,7 @@ Window { function _showNow() { // Compute new size everytime we show the tooltip width = Math.max(50 * scaling, tooltipText.implicitWidth + Style.marginLarge * 2 * scaling) - height = Math.max(50 * scaling, tooltipText.implicitHeight + Style.marginMedium * 2 * scaling) + height = Math.max(40 * scaling, tooltipText.implicitHeight + Style.marginMedium * 2 * scaling) if (!target) { return @@ -121,7 +121,7 @@ Window { // Timer to trigger show animation Timer { id: showTimer - interval: Style.animationFast / 15 // Very short delay to ensure component is visible + interval: Style.animationFast / 15 // Very short delay to ensure component is visible repeat: false onTriggered: { // Animate to final values From 7e334ae7683ca5d55a904bed97225dc9df97de33 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 21:17:52 -0400 Subject: [PATCH 338/394] Bar: more unified look on the left section --- Modules/Bar/ActiveWindow.qml | 2 +- Modules/Bar/MediaMini.qml | 35 ++++++++++++++++++++++++++++------- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/Modules/Bar/ActiveWindow.qml b/Modules/Bar/ActiveWindow.qml index 822824b..d970734 100644 --- a/Modules/Bar/ActiveWindow.qml +++ b/Modules/Bar/ActiveWindow.qml @@ -69,9 +69,9 @@ Row { text: getDisplayText() font.pointSize: Style.fontSizeSmall * scaling font.weight: Style.fontWeightBold + elide: Text.ElideRight anchors.verticalCenter: parent.verticalCenter verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight } // Mouse area for hover detection diff --git a/Modules/Bar/MediaMini.qml b/Modules/Bar/MediaMini.qml index 2a54498..4a06421 100644 --- a/Modules/Bar/MediaMini.qml +++ b/Modules/Bar/MediaMini.qml @@ -15,20 +15,41 @@ Item { height: parent.height spacing: Style.spacingTiniest * scaling - NIconButton { - icon: MediaPlayer.isPlaying ? "pause" : "play_arrow" - tooltipText: "Play/pause media" - sizeMultiplier: 0.8 - showBorder: false - onClicked: MediaPlayer.playPause() + // NIconButton { + // icon: MediaPlayer.isPlaying ? "pause" : "play_arrow" + // tooltipText: "Play/pause media" + // sizeMultiplier: 0.8 + // showBorder: false + // onClicked: MediaPlayer.playPause() + // } + NText { + text: MediaPlayer.isPlaying ? "pause" : "play_arrow" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeLarge * scaling + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + color: Colors.mPrimary + + MouseArea { + id: titleContainerMouseArea + anchors.fill: parent + + onClicked: { + onClicked: MediaPlayer.playPause() + } + } } // Track info NText { - text: MediaPlayer.trackTitle + " - " + MediaPlayer.trackArtist + text: MediaPlayer.trackTitle + (MediaPlayer.trackArtist !== "" ? ` - {MediaPlayer.trackArtist}` : "") color: Colors.mOnSurface font.pointSize: Style.fontSizeSmall * scaling + font.weight: Style.fontWeightBold elide: Text.ElideRight + + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter Layout.maximumWidth: 200 * scaling Layout.alignment: Qt.AlignVCenter } From 83ff5f5589e9c98cb5c32d737bac845933c3cf4b Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 21:45:58 -0400 Subject: [PATCH 339/394] Renamed all services to xxxService. Moved a couple things in Commons --- {Services => Commons}/Colors.qml | 1 + {Services => Commons}/IPCManager.qml | 3 - {Services => Commons}/Settings.qml | 9 +-- {Services => Commons}/Style.qml | 0 {Services => Commons}/Time.qml | 1 + Modules/AppLauncher/AppLauncher.qml | 11 ++-- Modules/Audio/CircularSpectrum.qml | 1 + Modules/Audio/LinearSpectrum.qml | 1 + Modules/Background/Background.qml | 5 +- Modules/Background/Overview.qml | 9 +-- Modules/Background/ScreenCorners.qml | 3 +- Modules/Background/WallpaperPicker.qml | 1 + Modules/Bar/ActiveWindow.qml | 9 +-- Modules/Bar/Bar.qml | 9 +-- Modules/Bar/Battery.qml | 1 + Modules/Bar/Brightness.qml | 1 + Modules/Bar/Clock.qml | 1 + Modules/Bar/MediaMini.qml | 15 +++-- Modules/Bar/NotificationHistory.qml | 1 + Modules/Bar/SystemMonitor.qml | 7 ++- Modules/Bar/Tray.qml | 1 + Modules/Bar/TrayMenu.qml | 1 + Modules/Bar/Volume.qml | 17 ++--- Modules/Bar/WiFi.qml | 15 ++--- Modules/Bar/WiFiMenu.qml | 43 ++++++------- Modules/Bar/Workspace.qml | 13 ++-- Modules/Calendar/Calendar.qml | 1 + Modules/Demo/DemoPanel.qml | 13 ++-- Modules/Dock/Dock.qml | 3 +- Modules/LockScreen/LockScreen.qml | 5 +- Modules/Notification/Notification.qml | 3 +- .../Notification/NotificationHistoryPanel.qml | 1 + Modules/Settings/SettingsPanel.qml | 7 ++- Modules/Settings/Tabs/AboutTab.qml | 5 +- Modules/Settings/Tabs/AudioTab.qml | 45 ++++++------- Modules/Settings/Tabs/BarTab.qml | 1 + Modules/Settings/Tabs/ColorSchemeTab.qml | 12 ++-- Modules/Settings/Tabs/DisplayTab.qml | 1 + Modules/Settings/Tabs/GeneralTab.qml | 1 + Modules/Settings/Tabs/NetworkTab.qml | 1 + Modules/Settings/Tabs/ScreenRecorderTab.qml | 15 ++--- Modules/Settings/Tabs/TimeWeatherTab.qml | 1 + .../Settings/Tabs/WallpaperSelectorTab.qml | 15 ++--- Modules/Settings/Tabs/WallpaperTab.qml | 1 + Modules/SidePanel/Cards/MediaCard.qml | 63 ++++++++++--------- Modules/SidePanel/Cards/PowerProfilesCard.qml | 1 + Modules/SidePanel/Cards/ProfileCard.qml | 1 + Modules/SidePanel/Cards/SystemMonitorCard.qml | 9 +-- Modules/SidePanel/Cards/UtilitiesCard.qml | 7 ++- Modules/SidePanel/Cards/WeatherCard.qml | 27 ++++---- Modules/SidePanel/PowerMenu.qml | 5 +- Modules/SidePanel/SidePanel.qml | 1 + Services/{Audio.qml => AudioService.qml} | 8 +-- Services/{Cava.qml => CavaService.qml} | 2 +- .../{Clipboard.qml => ClipboardService.qml} | 1 + ...lorSchemes.qml => ColorSchemesService.qml} | 2 +- Services/{GitHub.qml => GitHubService.qml} | 3 +- .../{Location.qml => LocationService.qml} | 3 +- .../{MediaPlayer.qml => MediaService.qml} | 1 + Services/{Network.qml => NetworkService.qml} | 2 +- Services/{Niri.qml => NiriService.qml} | 0 Services/NotificationService.qml | 1 + .../{PanelManager.qml => PanelService.qml} | 0 Services/{Scaling.qml => ScalingService.qml} | 0 ...Recorder.qml => ScreenRecorderService.qml} | 1 + ...SystemStats.qml => SystemStatsService.qml} | 0 .../{Wallpapers.qml => WallpapersService.qml} | 2 +- .../{Workspaces.qml => WorkspacesService.qml} | 5 +- Widgets/NBox.qml | 2 + Widgets/NBusyIndicator.qml | 1 + Widgets/NCard.qml | 1 + Widgets/NCircleStat.qml | 1 + Widgets/NClock.qml | 1 + Widgets/NComboBox.qml | 1 + Widgets/NDivider.qml | 1 + Widgets/NIconButton.qml | 1 + Widgets/NImageRounded.qml | 1 + Widgets/NPanel.qml | 11 ++-- Widgets/NPill.qml | 1 + Widgets/NRadioButton.qml | 1 + Widgets/NSlider.qml | 1 + Widgets/NText.qml | 1 + Widgets/NTextInput.qml | 1 + Widgets/NToggle.qml | 1 + Widgets/NTooltip.qml | 1 + shell.qml | 3 +- 86 files changed, 275 insertions(+), 211 deletions(-) rename {Services => Commons}/Colors.qml (99%) rename {Services => Commons}/IPCManager.qml (92%) rename {Services => Commons}/Settings.qml (94%) rename {Services => Commons}/Style.qml (100%) rename {Services => Commons}/Time.qml (99%) rename Services/{Audio.qml => AudioService.qml} (91%) rename Services/{Cava.qml => CavaService.qml} (98%) rename Services/{Clipboard.qml => ClipboardService.qml} (99%) rename Services/{ColorSchemes.qml => ColorSchemesService.qml} (93%) rename Services/{GitHub.qml => GitHubService.qml} (98%) rename Services/{Location.qml => LocationService.qml} (98%) rename Services/{MediaPlayer.qml => MediaService.qml} (99%) rename Services/{Network.qml => NetworkService.qml} (99%) rename Services/{Niri.qml => NiriService.qml} (100%) rename Services/{PanelManager.qml => PanelService.qml} (100%) rename Services/{Scaling.qml => ScalingService.qml} (100%) rename Services/{ScreenRecorder.qml => ScreenRecorderService.qml} (99%) rename Services/{SystemStats.qml => SystemStatsService.qml} (100%) rename Services/{Wallpapers.qml => WallpapersService.qml} (99%) rename Services/{Workspaces.qml => WorkspacesService.qml} (97%) diff --git a/Services/Colors.qml b/Commons/Colors.qml similarity index 99% rename from Services/Colors.qml rename to Commons/Colors.qml index 2a20655..b6d09e2 100644 --- a/Services/Colors.qml +++ b/Commons/Colors.qml @@ -3,6 +3,7 @@ pragma Singleton import QtQuick import Quickshell import Quickshell.Io +import qs.Commons import qs.Services // -------------------------------- diff --git a/Services/IPCManager.qml b/Commons/IPCManager.qml similarity index 92% rename from Services/IPCManager.qml rename to Commons/IPCManager.qml index 745c529..638fc52 100644 --- a/Services/IPCManager.qml +++ b/Commons/IPCManager.qml @@ -4,9 +4,6 @@ import Quickshell.Io Item { id: root - // Reference to the lockscreen component - property var lockscreen: null - IpcHandler { target: "settings" diff --git a/Services/Settings.qml b/Commons/Settings.qml similarity index 94% rename from Services/Settings.qml rename to Commons/Settings.qml index d982699..f8cc593 100644 --- a/Services/Settings.qml +++ b/Commons/Settings.qml @@ -1,6 +1,7 @@ import QtQuick import Quickshell import Quickshell.Io +import qs.Commons import qs.Services pragma Singleton @@ -53,7 +54,7 @@ Singleton { // Only set wallpaper on initial load, not on reloads if (isInitialLoad && adapter.wallpaper.current !== "") { console.log("[Settings] Set current wallpaper", adapter.wallpaper.current) - Wallpapers.setCurrentWallpaper(adapter.wallpaper.current, true) + WallpapersService.setCurrentWallpaper(adapter.wallpaper.current, true) } isInitialLoad = false }) @@ -121,9 +122,9 @@ Singleton { property int randomInterval: 300 property JsonObject swww - onDirectoryChanged: Wallpapers.loadWallpapers() - onIsRandomChanged: Wallpapers.toggleRandomWallpaper() - onRandomIntervalChanged: Wallpapers.restartRandomWallpaperTimer() + onDirectoryChanged: WallpapersService.loadWallpapers() + onIsRandomChanged: WallpapersService.toggleRandomWallpaper() + onRandomIntervalChanged: WallpapersService.restartRandomWallpaperTimer() swww: JsonObject { property bool enabled: false diff --git a/Services/Style.qml b/Commons/Style.qml similarity index 100% rename from Services/Style.qml rename to Commons/Style.qml diff --git a/Services/Time.qml b/Commons/Time.qml similarity index 99% rename from Services/Time.qml rename to Commons/Time.qml index 4748192..8f8d3bf 100644 --- a/Services/Time.qml +++ b/Commons/Time.qml @@ -2,6 +2,7 @@ pragma Singleton import Quickshell import QtQuick +import qs.Commons import qs.Services Singleton { diff --git a/Modules/AppLauncher/AppLauncher.qml b/Modules/AppLauncher/AppLauncher.qml index e3aa4c5..e0a4f7c 100644 --- a/Modules/AppLauncher/AppLauncher.qml +++ b/Modules/AppLauncher/AppLauncher.qml @@ -6,6 +6,7 @@ import Quickshell import Quickshell.Io import Quickshell.Wayland import Quickshell.Widgets +import qs.Commons import qs.Services import qs.Widgets @@ -15,7 +16,7 @@ import "../../Helpers/MathHelper.js" as MathHelper NLoader { id: appLauncher isLoaded: false - // Clipboard state is persisted in Services/Clipboard.qml + // Clipboard state is persisted in Services/ClipboardService.qml content: Component { NPanel { id: appLauncherPanel @@ -36,7 +37,7 @@ NLoader { } function updateClipboardHistory() { - Clipboard.refresh() + ClipboardService.refresh() } function selectNext() { @@ -119,12 +120,12 @@ NLoader { // Handle clipboard history if (query.startsWith(">clip")) { - if (!Clipboard.initialized) { - Clipboard.refresh() + if (!ClipboardService.initialized) { + ClipboardService.refresh() } const searchTerm = query.slice(5).trim() - Clipboard.history.forEach(function (clip, index) { + ClipboardService.history.forEach(function (clip, index) { let searchContent = clip.type === 'image' ? clip.mimeType : clip.content || clip if (!searchTerm || searchContent.toLowerCase().includes(searchTerm)) { diff --git a/Modules/Audio/CircularSpectrum.qml b/Modules/Audio/CircularSpectrum.qml index 467d855..efb6f7a 100644 --- a/Modules/Audio/CircularSpectrum.qml +++ b/Modules/Audio/CircularSpectrum.qml @@ -1,4 +1,5 @@ import QtQuick +import qs.Commons import qs.Services // Not used ATM and need rework diff --git a/Modules/Audio/LinearSpectrum.qml b/Modules/Audio/LinearSpectrum.qml index 93f8e41..32768b3 100644 --- a/Modules/Audio/LinearSpectrum.qml +++ b/Modules/Audio/LinearSpectrum.qml @@ -1,4 +1,5 @@ import QtQuick +import qs.Commons import qs.Services Item { diff --git a/Modules/Background/Background.qml b/Modules/Background/Background.qml index e2cda36..437cd3e 100644 --- a/Modules/Background/Background.qml +++ b/Modules/Background/Background.qml @@ -1,6 +1,7 @@ import QtQuick import Quickshell import Quickshell.Wayland +import qs.Commons import qs.Services Variants { @@ -8,8 +9,8 @@ Variants { delegate: PanelWindow { required property ShellScreen modelData - property string wallpaperSource: Wallpapers.currentWallpaper !== "" - && !Settings.data.wallpaper.swww.enabled ? Wallpapers.currentWallpaper : "" + property string wallpaperSource: WallpapersService.currentWallpaper !== "" + && !Settings.data.wallpaper.swww.enabled ? WallpapersService.currentWallpaper : "" visible: wallpaperSource !== "" && !Settings.data.wallpaper.swww.enabled diff --git a/Modules/Background/Overview.qml b/Modules/Background/Overview.qml index 22765e7..3c3d680 100644 --- a/Modules/Background/Overview.qml +++ b/Modules/Background/Overview.qml @@ -2,14 +2,15 @@ import QtQuick import QtQuick.Effects import Quickshell import Quickshell.Wayland +import qs.Commons import qs.Services import qs.Widgets NLoader { - active: Workspaces.isNiri + active: WorkspacesService.isNiri Component.onCompleted: { - if (Workspaces.isNiri) { + if (WorkspacesService.isNiri) { console.log("[Overview] Loading Overview component (Niri detected)") } else { console.log("[Overview] Skipping Overview component (Niri not detected)") @@ -21,8 +22,8 @@ NLoader { delegate: PanelWindow { required property ShellScreen modelData - property string wallpaperSource: Wallpapers.currentWallpaper !== "" - && !Settings.data.wallpaper.swww.enabled ? Wallpapers.currentWallpaper : "" + property string wallpaperSource: WallpapersService.currentWallpaper !== "" + && !Settings.data.wallpaper.swww.enabled ? WallpapersService.currentWallpaper : "" visible: wallpaperSource !== "" && !Settings.data.wallpaper.swww.enabled color: "transparent" diff --git a/Modules/Background/ScreenCorners.qml b/Modules/Background/ScreenCorners.qml index 8dbb353..0ddf297 100644 --- a/Modules/Background/ScreenCorners.qml +++ b/Modules/Background/ScreenCorners.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Effects import Quickshell import Quickshell.Wayland +import qs.Commons import qs.Services import qs.Widgets @@ -15,7 +16,7 @@ NLoader { id: root required property ShellScreen modelData - readonly property real scaling: Scaling.scale(screen) + readonly property real scaling: ScalingService.scale(screen) screen: modelData // Visible ring color diff --git a/Modules/Background/WallpaperPicker.qml b/Modules/Background/WallpaperPicker.qml index 0b1a417..2387e9e 100644 --- a/Modules/Background/WallpaperPicker.qml +++ b/Modules/Background/WallpaperPicker.qml @@ -2,6 +2,7 @@ pragma Singleton import QtQuick import Quickshell +import qs.Commons import qs.Services Item { diff --git a/Modules/Bar/ActiveWindow.qml b/Modules/Bar/ActiveWindow.qml index d970734..366e1d3 100644 --- a/Modules/Bar/ActiveWindow.qml +++ b/Modules/Bar/ActiveWindow.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Controls import Quickshell +import qs.Commons import qs.Services import qs.Widgets @@ -29,8 +30,8 @@ Row { target: typeof Niri !== "undefined" ? Niri : null function onFocusedWindowIndexChanged() { // Check if window actually changed - if (Niri.focusedWindowIndex !== lastWindowIndex) { - lastWindowIndex = Niri.focusedWindowIndex + if (NiriService.focusedWindowIndex !== lastWindowIndex) { + lastWindowIndex = NiriService.focusedWindowIndex showingFullTitle = true fullTitleTimer.restart() } @@ -96,8 +97,8 @@ Row { } // Get the focused window data - const focusedWindow = Niri.focusedWindowIndex >= 0 - && Niri.focusedWindowIndex < Niri.windows.length ? Niri.windows[Niri.focusedWindowIndex] : null + const focusedWindow = NiriService.focusedWindowIndex >= 0 + && NiriService.focusedWindowIndex < NiriService.windows.length ? NiriService.windows[NiriService.focusedWindowIndex] : null if (!focusedWindow) { return "" diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 3f34a27..741c842 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell +import qs.Commons import qs.Services import qs.Widgets import qs.Modules.Notification @@ -13,7 +14,7 @@ Variants { id: root required property ShellScreen modelData - readonly property real scaling: Scaling.scale(screen) + readonly property real scaling: ScalingService.scale(screen) screen: modelData implicitHeight: Style.barHeight * scaling @@ -88,11 +89,11 @@ Variants { tooltipText: "Screen Recording Active" sizeMultiplier: 0.8 showBorder: false - showFilled: ScreenRecorder.isRecording - visible: ScreenRecorder.isRecording + showFilled: ScreenRecorderService.isRecording + visible: ScreenRecorderService.isRecording anchors.verticalCenter: parent.verticalCenter onClicked: { - ScreenRecorder.toggleRecording() + ScreenRecorderService.toggleRecording() } } diff --git a/Modules/Bar/Battery.qml b/Modules/Bar/Battery.qml index ce4acdb..59d8ed3 100644 --- a/Modules/Bar/Battery.qml +++ b/Modules/Bar/Battery.qml @@ -2,6 +2,7 @@ import QtQuick import Quickshell import Quickshell.Services.UPower import QtQuick.Layouts +import qs.Commons import qs.Services import qs.Widgets diff --git a/Modules/Bar/Brightness.qml b/Modules/Bar/Brightness.qml index adc68cd..8b12050 100644 --- a/Modules/Bar/Brightness.qml +++ b/Modules/Bar/Brightness.qml @@ -1,6 +1,7 @@ import QtQuick import Quickshell import qs.Modules.Settings +import qs.Commons import qs.Services import qs.Widgets diff --git a/Modules/Bar/Clock.qml b/Modules/Bar/Clock.qml index 916687f..a2af29f 100644 --- a/Modules/Bar/Clock.qml +++ b/Modules/Bar/Clock.qml @@ -1,4 +1,5 @@ import QtQuick +import qs.Commons import qs.Services import qs.Widgets diff --git a/Modules/Bar/MediaMini.qml b/Modules/Bar/MediaMini.qml index 4a06421..bc7eae4 100644 --- a/Modules/Bar/MediaMini.qml +++ b/Modules/Bar/MediaMini.qml @@ -1,5 +1,6 @@ import QtQuick import QtQuick.Layouts +import qs.Commons import qs.Services import qs.Widgets @@ -8,7 +9,7 @@ Item { width: visible ? mediaRow.width : 0 height: Style.barHeight * scaling - visible: Settings.data.bar.showMedia && (MediaPlayer.canPlay || MediaPlayer.canPause) + visible: Settings.data.bar.showMedia && (MediaService.canPlay || MediaService.canPause) RowLayout { id: mediaRow @@ -16,18 +17,17 @@ Item { spacing: Style.spacingTiniest * scaling // NIconButton { - // icon: MediaPlayer.isPlaying ? "pause" : "play_arrow" + // icon: MediaService.isPlaying ? "pause" : "play_arrow" // tooltipText: "Play/pause media" // sizeMultiplier: 0.8 // showBorder: false - // onClicked: MediaPlayer.playPause() + // onClicked: MediaService.playPause() // } NText { - text: MediaPlayer.isPlaying ? "pause" : "play_arrow" + text: MediaService.isPlaying ? "pause" : "play_arrow" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeLarge * scaling verticalAlignment: Text.AlignVCenter - anchors.verticalCenter: parent.verticalCenter color: Colors.mPrimary MouseArea { @@ -35,21 +35,20 @@ Item { anchors.fill: parent onClicked: { - onClicked: MediaPlayer.playPause() + onClicked: MediaService.playPause() } } } // Track info NText { - text: MediaPlayer.trackTitle + (MediaPlayer.trackArtist !== "" ? ` - {MediaPlayer.trackArtist}` : "") + text: MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - {MediaService.trackArtist}` : "") color: Colors.mOnSurface font.pointSize: Style.fontSizeSmall * scaling font.weight: Style.fontWeightBold elide: Text.ElideRight verticalAlignment: Text.AlignVCenter - anchors.verticalCenter: parent.verticalCenter Layout.maximumWidth: 200 * scaling Layout.alignment: Qt.AlignVCenter } diff --git a/Modules/Bar/NotificationHistory.qml b/Modules/Bar/NotificationHistory.qml index f6ae033..d9fb9cc 100644 --- a/Modules/Bar/NotificationHistory.qml +++ b/Modules/Bar/NotificationHistory.qml @@ -3,6 +3,7 @@ import QtQuick.Layouts import QtQuick.Controls import Quickshell import Quickshell.Wayland +import qs.Commons import qs.Services import qs.Widgets diff --git a/Modules/Bar/SystemMonitor.qml b/Modules/Bar/SystemMonitor.qml index 8a8a84d..13d5a26 100644 --- a/Modules/Bar/SystemMonitor.qml +++ b/Modules/Bar/SystemMonitor.qml @@ -1,5 +1,6 @@ import QtQuick import Quickshell +import qs.Commons import qs.Services import qs.Widgets @@ -28,7 +29,7 @@ Row { NText { id: cpuUsageText - text: `${SystemStats.cpuUsage}%` + text: `${SystemStatsService.cpuUsage}%` font.pointSize: Style.fontSizeSmall * scaling font.weight: Style.fontWeightBold anchors.verticalCenter: parent.verticalCenter @@ -51,7 +52,7 @@ Row { } NText { - text: `${SystemStats.cpuTemp}°C` + text: `${SystemStatsService.cpuTemp}°C` font.pointSize: Style.fontSizeSmall * scaling font.weight: Style.fontWeightBold anchors.verticalCenter: parent.verticalCenter @@ -74,7 +75,7 @@ Row { } NText { - text: `${SystemStats.memoryUsageGb}G` + text: `${SystemStatsService.memoryUsageGb}G` font.pointSize: Style.fontSizeSmall * scaling font.weight: Style.fontWeightBold anchors.verticalCenter: parent.verticalCenter diff --git a/Modules/Bar/Tray.qml b/Modules/Bar/Tray.qml index 615749f..af612e9 100644 --- a/Modules/Bar/Tray.qml +++ b/Modules/Bar/Tray.qml @@ -5,6 +5,7 @@ import QtQuick.Controls import Quickshell import Quickshell.Services.SystemTray import Quickshell.Widgets +import qs.Commons import qs.Services import qs.Widgets diff --git a/Modules/Bar/TrayMenu.qml b/Modules/Bar/TrayMenu.qml index be43d09..d9741e3 100644 --- a/Modules/Bar/TrayMenu.qml +++ b/Modules/Bar/TrayMenu.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell +import qs.Commons import qs.Services import qs.Widgets diff --git a/Modules/Bar/Volume.qml b/Modules/Bar/Volume.qml index f34726e..03a3889 100644 --- a/Modules/Bar/Volume.qml +++ b/Modules/Bar/Volume.qml @@ -2,6 +2,7 @@ import QtQuick import Quickshell import Quickshell.Services.Pipewire import qs.Modules.Settings +import qs.Commons import qs.Services import qs.Widgets @@ -15,15 +16,15 @@ Item { property bool firstVolumeReceived: false function getIcon() { - if (Audio.muted) { + if (AudioService.muted) { return "volume_off" } - return Audio.volume <= Number.EPSILON ? "volume_off" : (Audio.volume < 0.33 ? "volume_down" : "volume_up") + return AudioService.volume <= Number.EPSILON ? "volume_off" : (AudioService.volume < 0.33 ? "volume_down" : "volume_up") } // Connection used to open the pill when volume changes Connections { - target: Audio.sink?.audio ? Audio.sink?.audio : null + target: AudioService.sink?.audio ? AudioService.sink?.audio : null function onVolumeChanged() { // console.log("[Bar:Volume] onVolumeChanged") if (!firstVolumeReceived) { @@ -51,19 +52,19 @@ Item { iconCircleColor: Colors.mPrimary collapsedIconColor: Colors.mOnSurface autoHide: false // Important to be false so we can hover as long as we want - text: Math.floor(Audio.volume * 100) + "%" + text: Math.floor(AudioService.volume * 100) + "%" tooltipText: "Volume: " + Math.round( - Audio.volume * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume." + AudioService.volume * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume." onWheel: function (angle) { if (angle > 0) { - Audio.increaseVolume() + AudioService.increaseVolume() } else if (angle < 0) { - Audio.decreaseVolume() + AudioService.decreaseVolume() } } onClicked: { - settingsPanel.requestedTab = SettingsPanel.Tab.Audio + settingsPanel.requestedTab = SettingsPanel.Tab.AudioService settingsPanel.isLoaded = true } } diff --git a/Modules/Bar/WiFi.qml b/Modules/Bar/WiFi.qml index ef28f39..ee4bb9d 100644 --- a/Modules/Bar/WiFi.qml +++ b/Modules/Bar/WiFi.qml @@ -3,6 +3,7 @@ import QtQuick.Layouts import QtQuick.Controls import Quickshell import Quickshell.Wayland +import qs.Commons import qs.Services import qs.Widgets @@ -15,14 +16,14 @@ NIconButton { icon: { let connected = false let signalStrength = 0 - for (const net in network.networks) { - if (network.networks[net].connected) { + for (const net in NetworkService.networks) { + if (NetworkService.networks[net].connected) { connected = true signalStrength = network.networks[net].signal break } } - return connected ? network.signalIcon(signalStrength) : "wifi_off" + return connected ? NetworkService.signalIcon(signalStrength) : "wifi_off" } tooltipText: "WiFi Networks" onClicked: { @@ -36,20 +37,16 @@ NIconButton { wifiMenuLoader.item.hide() } else { wifiMenuLoader.item.visible = false - network.onMenuClosed() + NetworkService.onMenuClosed() } } else { // Panel is hidden, show it wifiMenuLoader.item.visible = true - network.onMenuOpened() + NetworkService.onMenuOpened() } } } - Network { - id: network - } - WiFiMenu { id: wifiMenuLoader } diff --git a/Modules/Bar/WiFiMenu.qml b/Modules/Bar/WiFiMenu.qml index ff4ea54..44ad9bc 100644 --- a/Modules/Bar/WiFiMenu.qml +++ b/Modules/Bar/WiFiMenu.qml @@ -3,6 +3,7 @@ import QtQuick.Layouts import QtQuick.Controls import Quickshell import Quickshell.Wayland +import qs.Commons import qs.Services import qs.Widgets @@ -42,7 +43,7 @@ NLoader { // Also handle visibility changes from external sources onVisibleChanged: { if (visible && Settings.data.network.wifiEnabled) { - network.refreshNetworks() + NetworkService.refreshNetworks() } else if (wifiMenuRect.opacityValue > 0) { // Start hide animation wifiMenuRect.scaleValue = 0.8 @@ -61,16 +62,12 @@ NLoader { onTriggered: { wifiPanel.visible = false wifiPanel.dismissed() - network.onMenuClosed() + NetworkService.onMenuClosed() } } WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand - Network { - id: network - } - // Timer to refresh networks when WiFi is enabled while menu is open Timer { id: wifiEnableRefreshTimer @@ -78,7 +75,7 @@ NLoader { repeat: false onTriggered: { if (Settings.data.network.wifiEnabled && wifiPanel.visible) { - network.refreshNetworks() + NetworkService.refreshNetworks() } } } @@ -153,7 +150,7 @@ NLoader { value: Settings.data.network.wifiEnabled onToggled: function (value) { Settings.data.network.wifiEnabled = value - network.setWifiEnabled(value) + NetworkService.setWifiEnabled(value) // If enabling WiFi while menu is open, refresh after a delay if (value) { @@ -168,7 +165,7 @@ NLoader { sizeMultiplier: 0.8 enabled: Settings.data.network.wifiEnabled && !network.isLoading onClicked: { - network.refreshNetworks() + NetworkService.refreshNetworks() } } @@ -268,7 +265,7 @@ NLoader { spacing: Style.marginSmall * scaling NText { - text: network.signalIcon(modelData.signal) + text: NetworkService.signalIcon(modelData.signal) font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXL * scaling color: modelData.connected ? Colors.mSurface : (networkMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurface) @@ -297,9 +294,9 @@ NLoader { } NText { - visible: network.connectStatusSsid === modelData.ssid && network.connectStatus === "error" + visible: NetworkService.connectStatusSsid === modelData.ssid && NetworkService.connectStatus === "error" && network.connectError.length > 0 - text: network.connectError + text: NetworkService.connectError color: Colors.mError font.pointSize: Style.fontSizeSmall * scaling elide: Text.ElideRight @@ -310,12 +307,12 @@ NLoader { Item { Layout.preferredWidth: Style.baseWidgetSize * 0.7 * scaling Layout.preferredHeight: Style.baseWidgetSize * 0.7 * scaling - visible: network.connectStatusSsid === modelData.ssid - && (network.connectStatus !== "" || network.connectingSsid === modelData.ssid) + visible: NetworkService.connectStatusSsid === modelData.ssid + && (network.connectStatus !== "" || NetworkService.connectingSsid === modelData.ssid) NBusyIndicator { - visible: network.connectingSsid === modelData.ssid - running: network.connectingSsid === modelData.ssid + visible: NetworkService.connectingSsid === modelData.ssid + running: NetworkService.connectingSsid === modelData.ssid color: Colors.mPrimary anchors.centerIn: parent size: Style.baseWidgetSize * 0.7 * scaling @@ -323,7 +320,7 @@ NLoader { // TBC: Does nothing on my setup NText { - visible: network.connectStatus === "success" && !network.connectingSsid + visible: NetworkService.connectStatus === "success" && !NetworkService.connectingSsid text: "check_circle" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXL * scaling @@ -333,7 +330,7 @@ NLoader { // TBC: Does nothing on my setup NText { - visible: network.connectStatus === "error" && !network.connectingSsid + visible: NetworkService.connectStatus === "error" && !NetworkService.connectingSsid text: "error" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeSmall * scaling @@ -356,8 +353,8 @@ NLoader { hoverEnabled: true onClicked: { if (modelData.connected) { - network.disconnectNetwork(modelData.ssid) - } else if (network.isSecured(modelData.security) && !modelData.existing) { + NetworkService.disconnectNetwork(modelData.ssid) + } else if (NetworkService.isSecured(modelData.security) && !modelData.existing) { passwordPromptSsid = modelData.ssid showPasswordPrompt = true passwordInput = "" // Clear previous input @@ -365,7 +362,7 @@ NLoader { passwordInputField.forceActiveFocus() }) } else { - network.connectNetwork(modelData.ssid, modelData.security) + NetworkService.connectNetwork(modelData.ssid, modelData.security) } } } @@ -413,7 +410,7 @@ NLoader { echoMode: TextInput.Password onTextChanged: passwordInput = text onAccepted: { - network.submitPassword(passwordPromptSsid, passwordInput) + NetworkService.submitPassword(passwordPromptSsid, passwordInput) showPasswordPrompt = false } @@ -448,7 +445,7 @@ NLoader { MouseArea { anchors.fill: parent onClicked: { - network.submitPassword(passwordPromptSsid, passwordInput) + NetworkService.submitPassword(passwordPromptSsid, passwordInput) showPasswordPrompt = false } cursorShape: Qt.PointingHandCursor diff --git a/Modules/Bar/Workspace.qml b/Modules/Bar/Workspace.qml index 669192b..54a5858 100644 --- a/Modules/Bar/Workspace.qml +++ b/Modules/Bar/Workspace.qml @@ -5,6 +5,7 @@ import QtQuick.Window import QtQuick.Effects import Quickshell import Quickshell.Io +import qs.Commons import qs.Services Item { @@ -42,8 +43,8 @@ Item { Component.onCompleted: { localWorkspaces.clear() - for (var i = 0; i < Workspaces.workspaces.count; i++) { - const ws = Workspaces.workspaces.get(i) + for (var i = 0; i < WorkspacesService.workspaces.count; i++) { + const ws = WorkspacesService.workspaces.get(i) if (ws.output.toLowerCase() === screen.name.toLowerCase()) { localWorkspaces.append(ws) } @@ -53,11 +54,11 @@ Item { } Connections { - target: Workspaces + target: WorkspacesService function onWorkspacesChanged() { localWorkspaces.clear() - for (var i = 0; i < Workspaces.workspaces.count; i++) { - const ws = Workspaces.workspaces.get(i) + for (var i = 0; i < WorkspacesService.workspaces.count; i++) { + const ws = WorkspacesService.workspaces.get(i) if (ws.output.toLowerCase() === screen.name.toLowerCase()) { localWorkspaces.append(ws) } @@ -181,7 +182,7 @@ Item { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { - Workspaces.switchToWorkspace(model.idx) + WorkspacesService.switchToWorkspace(model.idx) } hoverEnabled: true } diff --git a/Modules/Calendar/Calendar.qml b/Modules/Calendar/Calendar.qml index 3596b2b..ac7071c 100644 --- a/Modules/Calendar/Calendar.qml +++ b/Modules/Calendar/Calendar.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import Quickshell import Quickshell.Wayland +import qs.Commons import qs.Services import qs.Widgets diff --git a/Modules/Demo/DemoPanel.qml b/Modules/Demo/DemoPanel.qml index 6546ed1..4dc220e 100644 --- a/Modules/Demo/DemoPanel.qml +++ b/Modules/Demo/DemoPanel.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import Quickshell import Quickshell.Wayland +import qs.Commons import qs.Services import qs.Widgets @@ -132,7 +133,7 @@ NLoader { font.weight: Style.fontWeightBold } NText { - text: `${Math.round(Scaling.overrideScale * 100)}%` + text: `${Math.round(ScalingService.overrideScale * 100)}%` Layout.alignment: Qt.AlignVCenter } RowLayout { @@ -142,14 +143,14 @@ NLoader { from: 0.6 to: 1.8 stepSize: 0.01 - value: Scaling.overrideScale + value: ScalingService.overrideScale implicitWidth: bgRect.width * 0.75 onMoved: { } onPressedChanged: { - Scaling.overrideScale = value - Scaling.overrideEnabled = true + ScalingService.overrideScale = value + ScalingService.overrideEnabled = true } } NIconButton { @@ -157,8 +158,8 @@ NLoader { tooltipText: "Reset Scaling" fontPointSize: Style.fontSizeLarge * scaling onClicked: { - Scaling.overrideEnabled = false - Scaling.overrideScale = 1.0 + ScalingService.overrideEnabled = false + ScalingService.overrideScale = 1.0 } } } diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml index 9b4a18f..a4e1826 100644 --- a/Modules/Dock/Dock.qml +++ b/Modules/Dock/Dock.qml @@ -5,6 +5,7 @@ import QtQuick.Effects import Quickshell import Quickshell.Wayland import Quickshell.Widgets +import qs.Commons import qs.Services import qs.Widgets @@ -18,7 +19,7 @@ NLoader { id: dockWindow required property ShellScreen modelData - readonly property real scaling: Scaling.scale(screen) + readonly property real scaling: ScalingService.scale(screen) screen: modelData // Auto-hide properties diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index e8e289c..bdfd843 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -7,6 +7,7 @@ import Quickshell.Wayland import Quickshell.Services.Pam import Quickshell.Io import Quickshell.Widgets +import qs.Commons import qs.Services import qs.Widgets @@ -14,7 +15,7 @@ WlSessionLock { id: lock // Lockscreen is a different beast, needs a capital 'S' in 'Screen' to get the current screen - readonly property real scaling: Scaling.scale(Screen) + readonly property real scaling: ScalingService.scale(Screen) property string errorMessage: "" property bool authenticating: false @@ -95,7 +96,7 @@ WlSessionLock { id: lockBgImage anchors.fill: parent fillMode: Image.PreserveAspectCrop - source: Wallpapers.currentWallpaper !== "" ? Wallpapers.currentWallpaper : "" + source: WallpapersService.currentWallpaper !== "" ? WallpapersService.currentWallpaper : "" cache: true smooth: true mipmap: false diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index 5b7e115..0420eeb 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -4,6 +4,7 @@ import Quickshell import Quickshell.Widgets import Quickshell.Wayland import Quickshell.Services.Notifications +import qs.Commons import qs.Services import qs.Widgets @@ -15,7 +16,7 @@ Variants { id: root required property ShellScreen modelData - readonly property real scaling: Scaling.scale(screen) + readonly property real scaling: ScalingService.scale(screen) screen: modelData // Access the notification model from the service diff --git a/Modules/Notification/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml index 829df38..f508b82 100644 --- a/Modules/Notification/NotificationHistoryPanel.qml +++ b/Modules/Notification/NotificationHistoryPanel.qml @@ -4,6 +4,7 @@ import QtQuick.Controls import Quickshell import Quickshell.Wayland import Quickshell.Services.Notifications +import qs.Commons import qs.Services import qs.Widgets diff --git a/Modules/Settings/SettingsPanel.qml b/Modules/Settings/SettingsPanel.qml index 3099a7f..d423b3f 100644 --- a/Modules/Settings/SettingsPanel.qml +++ b/Modules/Settings/SettingsPanel.qml @@ -4,6 +4,7 @@ import QtQuick.Layouts import Quickshell import Quickshell.Wayland import qs.Modules.Settings.Tabs as Tabs +import qs.Commons import qs.Services import qs.Widgets @@ -13,7 +14,7 @@ NLoader { // Tabs enumeration, order is NOT relevant enum Tab { About, - Audio, + AudioService, Bar, ColorScheme, Display, @@ -119,8 +120,8 @@ NLoader { "icon": "web_asset", "source": barTab }, { - "id": SettingsPanel.Tab.Audio, - "label": "Audio", + "id": SettingsPanel.Tab.AudioService, + "label": "AudioService", "icon": "volume_up", "source": audioTab }, { diff --git a/Modules/Settings/Tabs/AboutTab.qml b/Modules/Settings/Tabs/AboutTab.qml index 2fd7973..b9e9cbb 100644 --- a/Modules/Settings/Tabs/AboutTab.qml +++ b/Modules/Settings/Tabs/AboutTab.qml @@ -4,15 +4,16 @@ import QtQuick.Effects import QtQuick.Layouts import Quickshell import Quickshell.Io +import qs.Commons import qs.Services import qs.Widgets ColumnLayout { id: root - property string latestVersion: GitHub.latestVersion + property string latestVersion: GitHubService.latestVersion property string currentVersion: "Unknown" // Fallback version - property var contributors: GitHub.contributors + property var contributors: GitHubService.contributors spacing: 0 Layout.fillWidth: true diff --git a/Modules/Settings/Tabs/AudioTab.qml b/Modules/Settings/Tabs/AudioTab.qml index 36e3e2d..8944043 100644 --- a/Modules/Settings/Tabs/AudioTab.qml +++ b/Modules/Settings/Tabs/AudioTab.qml @@ -5,18 +5,19 @@ import Quickshell.Services.Pipewire import qs.Modules.Settings import qs.Widgets +import qs.Commons import qs.Services ColumnLayout { id: root - property real localVolume: Audio.volume + property real localVolume: AudioService.volume // Connection used to open the pill when volume changes Connections { - target: Audio.sink?.audio ? Audio.sink?.audio : null + target: AudioService.sink?.audio ? AudioService.sink?.audio : null function onVolumeChanged() { - localVolume = Audio.volume + localVolume = AudioService.volume } } @@ -46,7 +47,7 @@ ColumnLayout { Layout.fillWidth: true NText { - text: "Audio" + text: "AudioService" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold color: Colors.mOnSurface @@ -90,8 +91,8 @@ ColumnLayout { running: true repeat: true onTriggered: { - if (Math.abs(localVolume - Audio.volume) >= 0.01) { - Audio.setVolume(localVolume) + if (Math.abs(localVolume - AudioService.volume) >= 0.01) { + AudioService.setVolume(localVolume) } } } @@ -108,7 +109,7 @@ ColumnLayout { } NText { - text: Math.floor(Audio.volume * 100) + "%" + text: Math.floor(AudioService.volume * 100) + "%" Layout.alignment: Qt.AlignVCenter color: Colors.mOnSurface } @@ -122,12 +123,12 @@ ColumnLayout { Layout.topMargin: Style.marginMedium * scaling NToggle { - label: "Mute Audio" + label: "Mute AudioService" description: "Mute or unmute the default audio output" - value: Audio.muted + value: AudioService.muted onToggled: function (newValue) { - if (Audio.sink && Audio.sink.audio) { - Audio.sink.audio.muted = newValue + if (AudioService.sink && AudioService.sink.audio) { + AudioService.sink.audio.muted = newValue } } } @@ -140,13 +141,13 @@ ColumnLayout { Layout.bottomMargin: Style.marginLarge * scaling } - // Audio Devices + // AudioService Devices ColumnLayout { spacing: Style.marginLarge * scaling Layout.fillWidth: true NText { - text: "Audio Devices" + text: "AudioService Devices" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold color: Colors.mOnSurface @@ -180,12 +181,12 @@ ColumnLayout { } Repeater { - model: Audio.sinks + model: AudioService.sinks NRadioButton { required property PwNode modelData ButtonGroup.group: sinks - checked: Audio.sink?.id === modelData.id - onClicked: Audio.setAudioSink(modelData) + checked: AudioService.sink?.id === modelData.id + onClicked: AudioService.setAudioSink(modelData) text: modelData.description } } @@ -219,12 +220,12 @@ ColumnLayout { } Repeater { - model: Audio.sources + model: AudioService.sources NRadioButton { required property PwNode modelData ButtonGroup.group: sources - checked: Audio.source?.id === modelData.id - onClicked: Audio.setAudioSource(modelData) + checked: AudioService.source?.id === modelData.id + onClicked: AudioService.setAudioSource(modelData) text: modelData.description } } @@ -238,20 +239,20 @@ ColumnLayout { Layout.bottomMargin: Style.marginMedium * scaling } - // Audio Visualizer Category + // AudioService Visualizer Category ColumnLayout { spacing: Style.marginSmall * scaling Layout.fillWidth: true NText { - text: "Audio Visualizer" + text: "AudioService Visualizer" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold color: Colors.mOnSurface Layout.bottomMargin: Style.marginSmall * scaling } - // Audio Visualizer section + // AudioService Visualizer section NComboBox { id: audioVisualizerCombo label: "Visualization Type" diff --git a/Modules/Settings/Tabs/BarTab.qml b/Modules/Settings/Tabs/BarTab.qml index 56b8e02..892fc96 100644 --- a/Modules/Settings/Tabs/BarTab.qml +++ b/Modules/Settings/Tabs/BarTab.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import qs.Commons import qs.Services import qs.Widgets diff --git a/Modules/Settings/Tabs/ColorSchemeTab.qml b/Modules/Settings/Tabs/ColorSchemeTab.qml index 1b62b8f..390cb6c 100644 --- a/Modules/Settings/Tabs/ColorSchemeTab.qml +++ b/Modules/Settings/Tabs/ColorSchemeTab.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import qs.Commons import qs.Services import qs.Widgets import Quickshell.Io @@ -66,7 +67,7 @@ ColumnLayout { // When the list of available schemes changes, clear the cache. // The Repeater below will automatically re-create the FileViews. Connections { - target: ColorSchemes + target: ColorSchemesService function onSchemesChanged() { schemeColorsCache = {} } @@ -78,7 +79,7 @@ ColumnLayout { id: fileLoaders Repeater { - model: ColorSchemes.schemes + model: ColorSchemesService.schemes // The delegate is a Component, which correctly wraps the non-visual FileView delegate: Item { @@ -132,7 +133,7 @@ ColumnLayout { onToggled: function (newValue) { Settings.data.colorSchemes.useWallpaperColors = newValue if (Settings.data.colorSchemes.useWallpaperColors) { - ColorSchemes.changedWallpaper() + ColorSchemesService.changedWallpaper() } } } @@ -177,7 +178,7 @@ ColumnLayout { Layout.fillWidth: true Repeater { - model: ColorSchemes.schemes + model: ColorSchemesService.schemes Rectangle { id: schemeCard @@ -197,9 +198,10 @@ ColumnLayout { anchors.fill: parent onClicked: { // Disable useWallpaperColors when picking a predefined color scheme + // TBC: broken uncheck useWallpaperColors Settings.data.colorSchemes.useWallpaperColors = false Settings.data.colorSchemes.predefinedScheme = schemePath - ColorSchemes.applyScheme(schemePath) + ColorSchemesService.applyScheme(schemePath) } hoverEnabled: true cursorShape: Qt.PointingHandCursor diff --git a/Modules/Settings/Tabs/DisplayTab.qml b/Modules/Settings/Tabs/DisplayTab.qml index b9b39d0..3b46880 100644 --- a/Modules/Settings/Tabs/DisplayTab.qml +++ b/Modules/Settings/Tabs/DisplayTab.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Layouts import QtQuick.Controls import Quickshell +import qs.Commons import qs.Services import qs.Widgets diff --git a/Modules/Settings/Tabs/GeneralTab.qml b/Modules/Settings/Tabs/GeneralTab.qml index b3bf2a9..aae1f08 100644 --- a/Modules/Settings/Tabs/GeneralTab.qml +++ b/Modules/Settings/Tabs/GeneralTab.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import qs.Commons import qs.Services import qs.Widgets diff --git a/Modules/Settings/Tabs/NetworkTab.qml b/Modules/Settings/Tabs/NetworkTab.qml index 6ff7b82..e90fccf 100644 --- a/Modules/Settings/Tabs/NetworkTab.qml +++ b/Modules/Settings/Tabs/NetworkTab.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import Quickshell import Quickshell.Bluetooth +import qs.Commons import qs.Services import qs.Widgets diff --git a/Modules/Settings/Tabs/ScreenRecorderTab.qml b/Modules/Settings/Tabs/ScreenRecorderTab.qml index eb14a4c..ebc9e26 100644 --- a/Modules/Settings/Tabs/ScreenRecorderTab.qml +++ b/Modules/Settings/Tabs/ScreenRecorderTab.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import qs.Commons import qs.Services import qs.Widgets @@ -206,23 +207,23 @@ ColumnLayout { Layout.bottomMargin: Style.marginLarge * scaling } - // Audio Settings + // AudioService Settings ColumnLayout { spacing: Style.marginLarge * scaling Layout.fillWidth: true NText { - text: "Audio Settings" + text: "AudioService Settings" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold color: Colors.mOnSurface Layout.bottomMargin: Style.marginSmall * scaling } - // Audio Source + // AudioService Source NComboBox { - label: "Audio Source" - description: "Audio source to capture during recording" + label: "AudioService Source" + description: "AudioService source to capture during recording" model: ListModel { ListElement { key: "default_output" @@ -243,9 +244,9 @@ ColumnLayout { } } - // Audio Codec + // AudioService Codec NComboBox { - label: "Audio Codec" + label: "AudioService Codec" description: "Opus is recommended for best performance and smallest audio size" model: ListModel { ListElement { diff --git a/Modules/Settings/Tabs/TimeWeatherTab.qml b/Modules/Settings/Tabs/TimeWeatherTab.qml index 47cbf7c..8cd1dec 100644 --- a/Modules/Settings/Tabs/TimeWeatherTab.qml +++ b/Modules/Settings/Tabs/TimeWeatherTab.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import qs.Commons import qs.Services import qs.Widgets diff --git a/Modules/Settings/Tabs/WallpaperSelectorTab.qml b/Modules/Settings/Tabs/WallpaperSelectorTab.qml index 0077ff8..20faf93 100644 --- a/Modules/Settings/Tabs/WallpaperSelectorTab.qml +++ b/Modules/Settings/Tabs/WallpaperSelectorTab.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Layouts import QtQuick.Controls import Qt.labs.folderlistmodel +import qs.Commons import qs.Services import qs.Widgets @@ -48,7 +49,7 @@ Item { id: currentWallpaperImage anchors.fill: parent anchors.margins: Style.marginSmall * scaling - imagePath: Wallpapers.currentWallpaper + imagePath: WallpapersService.currentWallpaper fallbackIcon: "image" borderColor: Colors.mOutline borderWidth: Math.max(1, Style.borderThin * scaling) @@ -96,7 +97,7 @@ Item { icon: "refresh" tooltipText: "Refresh wallpaper list" onClicked: { - Wallpapers.loadWallpapers() + WallpapersService.loadWallpapers() } Layout.alignment: Qt.AlignTop | Qt.AlignRight } @@ -106,14 +107,14 @@ Item { Item { Layout.fillWidth: true Layout.preferredHeight: { - return Math.ceil(Wallpapers.wallpaperList.length / wallpaperGridView.columns) * wallpaperGridView.cellHeight + return Math.ceil(WallpapersService.wallpaperList.length / wallpaperGridView.columns) * wallpaperGridView.cellHeight } GridView { id: wallpaperGridView anchors.fill: parent clip: true - model: Wallpapers.wallpaperList + model: WallpapersService.wallpaperList boundsBehavior: Flickable.StopAtBounds flickableDirection: Flickable.AutoFlickDirection @@ -134,7 +135,7 @@ Item { delegate: Rectangle { id: wallpaperItem property string wallpaperPath: modelData - property bool isSelected: wallpaperPath === Wallpapers.currentWallpaper + property bool isSelected: wallpaperPath === WallpapersService.currentWallpaper width: wallpaperGridView.itemSize height: Math.floor(wallpaperGridView.itemSize * 0.67) @@ -195,7 +196,7 @@ Item { acceptedButtons: Qt.LeftButton hoverEnabled: true onClicked: { - Wallpapers.changeWallpaper(wallpaperPath) + WallpapersService.changeWallpaper(wallpaperPath) } } } @@ -208,7 +209,7 @@ Item { radius: Style.radiusMedium * scaling border.color: Colors.mOutline border.width: Math.max(1, Style.borderThin * scaling) - visible: Wallpapers.wallpaperList.length === 0 && !Wallpapers.scanning + visible: WallpapersService.wallpaperList.length === 0 && !WallpapersService.scanning ColumnLayout { anchors.centerIn: parent diff --git a/Modules/Settings/Tabs/WallpaperTab.qml b/Modules/Settings/Tabs/WallpaperTab.qml index 9e75c84..102689a 100644 --- a/Modules/Settings/Tabs/WallpaperTab.qml +++ b/Modules/Settings/Tabs/WallpaperTab.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import qs.Commons import qs.Services import qs.Widgets diff --git a/Modules/SidePanel/Cards/MediaCard.qml b/Modules/SidePanel/Cards/MediaCard.qml index 5414789..76d6f72 100644 --- a/Modules/SidePanel/Cards/MediaCard.qml +++ b/Modules/SidePanel/Cards/MediaCard.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import Quickshell import qs.Modules.Audio +import qs.Commons import qs.Services import qs.Widgets @@ -17,7 +18,7 @@ NBox { // Height can be overridden by parent layout (SidePanel binds it to stats card) //implicitHeight: content.implicitHeight + Style.marginLarge * 2 * scaling // Component.onCompleted: { - // console.log(MediaPlayer.trackArtUrl) + // console.log(MediaService.trackArtUrl) // } ColumnLayout { anchors.fill: parent @@ -58,7 +59,7 @@ NBox { ColumnLayout { id: main - visible: MediaPlayer.currentPlayer && MediaPlayer.canPlay + visible: MediaService.currentPlayer && MediaService.canPlay spacing: Style.marginMedium * scaling // Player selector @@ -66,10 +67,10 @@ NBox { id: playerSelector Layout.fillWidth: true Layout.preferredHeight: Style.barHeight * 0.83 * scaling - visible: MediaPlayer.getAvailablePlayers().length > 1 - model: MediaPlayer.getAvailablePlayers() + visible: MediaService.getAvailablePlayers().length > 1 + model: MediaService.getAvailablePlayers() textRole: "identity" - currentIndex: MediaPlayer.selectedPlayerIndex + currentIndex: MediaService.selectedPlayerIndex background: Rectangle { visible: false @@ -145,8 +146,8 @@ NBox { } onActivated: { - MediaPlayer.selectedPlayerIndex = currentIndex - MediaPlayer.updateCurrentPlayer() + MediaService.selectedPlayerIndex = currentIndex + MediaService.updateCurrentPlayer() } } @@ -167,11 +168,11 @@ NBox { NImageRounded { id: trackArt - visible: MediaPlayer.trackArtUrl.toString() !== "" + visible: MediaService.trackArtUrl.toString() !== "" anchors.fill: parent anchors.margins: Style.marginTiny * scaling - imagePath: MediaPlayer.trackArtUrl + imagePath: MediaService.trackArtUrl fallbackIcon: "music_note" borderColor: Colors.mOutline borderWidth: Math.max(1, Style.borderThin * scaling) @@ -196,8 +197,8 @@ NBox { spacing: Style.marginTiny * scaling NText { - visible: MediaPlayer.trackTitle !== "" - text: MediaPlayer.trackTitle + visible: MediaService.trackTitle !== "" + text: MediaService.trackTitle font.pointSize: Style.fontSizeMedium * scaling font.weight: Style.fontWeightBold elide: Text.ElideRight @@ -207,8 +208,8 @@ NBox { } NText { - visible: MediaPlayer.trackArtist !== "" - text: MediaPlayer.trackArtist + visible: MediaService.trackArtist !== "" + text: MediaService.trackArtist color: Colors.mOnSurface font.pointSize: Style.fontSizeSmall * scaling elide: Text.ElideRight @@ -216,8 +217,8 @@ NBox { } NText { - visible: MediaPlayer.trackAlbum !== "" - text: MediaPlayer.trackAlbum + visible: MediaService.trackAlbum !== "" + text: MediaService.trackAlbum color: Colors.mOnSurface font.pointSize: Style.fontSizeSmall * scaling elide: Text.ElideRight @@ -230,7 +231,7 @@ NBox { // Progress bar Rectangle { id: progressBarBackground - visible: (MediaPlayer.currentPlayer && MediaPlayer.trackLength > 0) + visible: (MediaService.currentPlayer && MediaService.trackLength > 0) width: parent.width height: 4 * scaling radius: Style.radiusSmall * scaling @@ -238,10 +239,10 @@ NBox { Layout.fillWidth: true property real progressRatio: { - if (!MediaPlayer.currentPlayer || !MediaPlayer.isPlaying || MediaPlayer.trackLength <= 0) { + if (!MediaService.currentPlayer || !MediaService.isPlaying || MediaService.trackLength <= 0) { return 0 } - return Math.min(1, MediaPlayer.currentPosition / MediaPlayer.trackLength) + return Math.min(1, MediaService.currentPosition / MediaService.trackLength) } Rectangle { @@ -261,7 +262,7 @@ NBox { // Interactive progress handle Rectangle { id: progressHandle - visible: (MediaPlayer.currentPlayer && MediaPlayer.trackLength > 0) + visible: (MediaService.currentPlayer && MediaService.trackLength > 0) width: 16 * scaling height: 16 * scaling radius: width * 0.5 @@ -285,17 +286,17 @@ NBox { anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor - enabled: MediaPlayer.trackLength > 0 && MediaPlayer.canSeek + enabled: MediaService.trackLength > 0 && MediaService.canSeek onClicked: function (mouse) { let ratio = mouse.x / width - MediaPlayer.seekByRatio(ratio) + MediaService.seekByRatio(ratio) } onPositionChanged: function (mouse) { if (pressed) { let ratio = Math.max(0, Math.min(1, mouse.x / width)) - MediaPlayer.seekByRatio(ratio) + MediaService.seekByRatio(ratio) } } } @@ -312,24 +313,24 @@ NBox { NIconButton { icon: "skip_previous" tooltipText: "Previous Media" - visible: MediaPlayer.canGoPrevious - onClicked: MediaPlayer.canGoPrevious ? MediaPlayer.previous() : {} + visible: MediaService.canGoPrevious + onClicked: MediaService.canGoPrevious ? MediaService.previous() : {} } // Play/Pause button NIconButton { - icon: MediaPlayer.isPlaying ? "pause" : "play_arrow" - tooltipText: MediaPlayer.isPlaying ? "Pause" : "Play" - visible: (MediaPlayer.canPlay || MediaPlayer.canPause) - onClicked: (MediaPlayer.canPlay || MediaPlayer.canPause) ? MediaPlayer.playPause() : {} + icon: MediaService.isPlaying ? "pause" : "play_arrow" + tooltipText: MediaService.isPlaying ? "Pause" : "Play" + visible: (MediaService.canPlay || MediaService.canPause) + onClicked: (MediaService.canPlay || MediaService.canPause) ? MediaService.playPause() : {} } // Next button NIconButton { icon: "skip_next" tooltipText: "Next Media" - visible: MediaPlayer.canGoNext - onClicked: MediaPlayer.canGoNext ? MediaPlayer.next() : {} + visible: MediaService.canGoNext + onClicked: MediaService.canGoNext ? MediaService.next() : {} } } } @@ -341,7 +342,7 @@ NBox { sourceComponent: LinearSpectrum { width: 300 * scaling height: 80 * scaling - values: Cava.values + values: CavaService.values fillColor: Colors.mOnSurface Layout.alignment: Qt.AlignHCenter } diff --git a/Modules/SidePanel/Cards/PowerProfilesCard.qml b/Modules/SidePanel/Cards/PowerProfilesCard.qml index 8d6b49d..e77081c 100644 --- a/Modules/SidePanel/Cards/PowerProfilesCard.qml +++ b/Modules/SidePanel/Cards/PowerProfilesCard.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import Quickshell import Quickshell.Services.UPower +import qs.Commons import qs.Services import qs.Widgets diff --git a/Modules/SidePanel/Cards/ProfileCard.qml b/Modules/SidePanel/Cards/ProfileCard.qml index 99bef0c..c5e8c5a 100644 --- a/Modules/SidePanel/Cards/ProfileCard.qml +++ b/Modules/SidePanel/Cards/ProfileCard.qml @@ -6,6 +6,7 @@ import Quickshell.Io import Quickshell.Widgets import qs.Modules.Settings import qs.Modules.SidePanel +import qs.Commons import qs.Services import qs.Widgets diff --git a/Modules/SidePanel/Cards/SystemMonitorCard.qml b/Modules/SidePanel/Cards/SystemMonitorCard.qml index f353896..c3fb93e 100644 --- a/Modules/SidePanel/Cards/SystemMonitorCard.qml +++ b/Modules/SidePanel/Cards/SystemMonitorCard.qml @@ -1,5 +1,6 @@ import QtQuick import QtQuick.Layouts +import qs.Commons import qs.Services import qs.Widgets @@ -27,7 +28,7 @@ NBox { } NCircleStat { - value: SystemStats.cpuUsage + value: SystemStatsService.cpuUsage icon: "speed" flat: true contentScale: 0.8 @@ -35,7 +36,7 @@ NBox { height: 68 * scaling } NCircleStat { - value: SystemStats.cpuTemp + value: SystemStatsService.cpuTemp suffix: "°C" icon: "device_thermostat" flat: true @@ -44,7 +45,7 @@ NBox { height: 68 * scaling } NCircleStat { - value: SystemStats.memoryUsagePer + value: SystemStatsService.memoryUsagePer icon: "memory" flat: true contentScale: 0.8 @@ -52,7 +53,7 @@ NBox { height: 68 * scaling } NCircleStat { - value: SystemStats.diskUsage + value: SystemStatsService.diskUsage icon: "hard_drive" flat: true contentScale: 0.8 diff --git a/Modules/SidePanel/Cards/UtilitiesCard.qml b/Modules/SidePanel/Cards/UtilitiesCard.qml index 81e21f7..11c8a96 100644 --- a/Modules/SidePanel/Cards/UtilitiesCard.qml +++ b/Modules/SidePanel/Cards/UtilitiesCard.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import Quickshell import qs.Modules.Settings +import qs.Commons import qs.Services import qs.Widgets @@ -22,10 +23,10 @@ NBox { // Screen Recorder NIconButton { icon: "videocam" - tooltipText: ScreenRecorder.isRecording ? "Stop Screen Recording" : "Start Screen Recording" - showFilled: ScreenRecorder.isRecording + tooltipText: ScreenRecorderService.isRecording ? "Stop Screen Recording" : "Start Screen Recording" + showFilled: ScreenRecorderService.isRecording onClicked: { - ScreenRecorder.toggleRecording() + ScreenRecorderService.toggleRecording() } } diff --git a/Modules/SidePanel/Cards/WeatherCard.qml b/Modules/SidePanel/Cards/WeatherCard.qml index 5063108..58bf982 100644 --- a/Modules/SidePanel/Cards/WeatherCard.qml +++ b/Modules/SidePanel/Cards/WeatherCard.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Layouts import Quickshell +import qs.Commons import qs.Services import qs.Widgets @@ -8,7 +9,7 @@ import qs.Widgets NBox { id: root - readonly property bool weatherReady: (Location.data.weather !== null) + readonly property bool weatherReady: (LocationService.data.weather !== null) // TBC weatherReady is not turning to false when we reset weather... Layout.fillWidth: true @@ -26,7 +27,7 @@ NBox { RowLayout { spacing: Style.marginSmall * scaling NText { - text: weatherReady ? Location.weatherSymbolFromCode(Location.data.weather.current_weather.weathercode) : "" + text: weatherReady ? LocationService.weatherSymbolFromCode(LocationService.data.weather.current_weather.weathercode) : "" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXXL * 1.5 * scaling color: Colors.mPrimary @@ -51,10 +52,10 @@ NBox { if (!weatherReady) { return "" } - var temp = Location.data.weather.current_weather.temperature + var temp = LocationService.data.weather.current_weather.temperature var suffix = "C" if (Settings.data.location.useFahrenheit) { - temp = Location.celsiusToFahrenheit(temp) + temp = LocationService.celsiusToFahrenheit(temp) var suffix = "F" } temp = Math.round(temp) @@ -65,9 +66,9 @@ NBox { } NText { - text: weatherReady ? `(${Location.data.weather.timezone_abbreviation})` : "" + text: weatherReady ? `(${LocationService.data.weather.timezone_abbreviation})` : "" font.pointSize: Style.fontSizeSmall * scaling - visible: Location.data.weather + visible: LocationService.data.weather } } } @@ -84,27 +85,27 @@ NBox { Layout.alignment: Qt.AlignHCenter spacing: Style.marginLarge * scaling Repeater { - model: weatherReady ? Location.data.weather.daily.time : [] + model: weatherReady ? LocationService.data.weather.daily.time : [] delegate: ColumnLayout { Layout.alignment: Qt.AlignHCenter spacing: Style.marginSmall * scaling NText { - text: Qt.formatDateTime(new Date(Location.data.weather.daily.time[index]), "ddd") + text: Qt.formatDateTime(new Date(LocationService.data.weather.daily.time[index]), "ddd") color: Colors.mOnSurface } NText { - text: Location.weatherSymbolFromCode(Location.data.weather.daily.weathercode[index]) + text: LocationService.weatherSymbolFromCode(LocationService.data.weather.daily.weathercode[index]) font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXL * scaling color: Colors.mPrimary } NText { text: { - var max = Location.data.weather.daily.temperature_2m_max[index] - var min = Location.data.weather.daily.temperature_2m_min[index] + var max = LocationService.data.weather.daily.temperature_2m_max[index] + var min = LocationService.data.weather.daily.temperature_2m_min[index] if (Settings.data.location.useFahrenheit) { - max = Location.celsiusToFahrenheit(max) - min = Location.celsiusToFahrenheit(min) + max = LocationService.celsiusToFahrenheit(max) + min = LocationService.celsiusToFahrenheit(min) } max = Math.round(max) min = Math.round(min) diff --git a/Modules/SidePanel/PowerMenu.qml b/Modules/SidePanel/PowerMenu.qml index 28139f4..fbbd3e5 100644 --- a/Modules/SidePanel/PowerMenu.qml +++ b/Modules/SidePanel/PowerMenu.qml @@ -4,6 +4,7 @@ import QtQuick.Layouts import Quickshell import Quickshell.Io import Quickshell.Widgets +import qs.Commons import qs.Services import qs.Widgets import qs.Modules.LockScreen @@ -347,9 +348,9 @@ NPanel { // ---------------------------------- // System functions function logout() { - if (Workspaces.isNiri) { + if (WorkspacesService.isNiri) { logoutProcessNiri.running = true - } else if (Workspaces.isHyprland) { + } else if (WorkspacesService.isHyprland) { logoutProcessHyprland.running = true } else { console.warn("No supported compositor detected for logout") diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml index 8f187ac..76f03e0 100644 --- a/Modules/SidePanel/SidePanel.qml +++ b/Modules/SidePanel/SidePanel.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import Quickshell import qs.Modules.SidePanel.Cards +import qs.Commons import qs.Services import qs.Widgets diff --git a/Services/Audio.qml b/Services/AudioService.qml similarity index 91% rename from Services/Audio.qml rename to Services/AudioService.qml index 137dff6..e31f17e 100644 --- a/Services/Audio.qml +++ b/Services/AudioService.qml @@ -53,7 +53,7 @@ Singleton { function onMutedChanged() { root._muted = (sink?.audio.muted ?? true) - console.log("[Audio] onMuteChanged:", root._muted) + console.log("[AudioService] onMuteChanged:", root._muted) } } @@ -70,9 +70,9 @@ Singleton { // Clamp it accordingly sink.audio.muted = false sink.audio.volume = Math.max(0, Math.min(1, newVolume)) - //console.log("[Audio] setVolume", sink.audio.volume); + //console.log("[AudioService] setVolume", sink.audio.volume); } else { - console.warn("[Audio] No sink available") + console.warn("[AudioService] No sink available") } } @@ -80,7 +80,7 @@ Singleton { if (sink?.ready && sink?.audio) { sink.audio.muted = muted } else { - console.warn("[Audio] No sink available") + console.warn("[AudioService] No sink available") } } diff --git a/Services/Cava.qml b/Services/CavaService.qml similarity index 98% rename from Services/Cava.qml rename to Services/CavaService.qml index 3a5626f..89e4248 100644 --- a/Services/Cava.qml +++ b/Services/CavaService.qml @@ -37,7 +37,7 @@ Singleton { Process { id: process stdinEnabled: true - running: MediaPlayer.isPlaying + running: MediaService.isPlaying command: ["cava", "-p", "/dev/stdin"] onExited: { stdinEnabled = true diff --git a/Services/Clipboard.qml b/Services/ClipboardService.qml similarity index 99% rename from Services/Clipboard.qml rename to Services/ClipboardService.qml index c66b3ce..008e83c 100644 --- a/Services/Clipboard.qml +++ b/Services/ClipboardService.qml @@ -3,6 +3,7 @@ pragma Singleton import QtQuick import Quickshell import Quickshell.Io +import qs.Commons import qs.Services Singleton { diff --git a/Services/ColorSchemes.qml b/Services/ColorSchemesService.qml similarity index 93% rename from Services/ColorSchemes.qml rename to Services/ColorSchemesService.qml index 4ef659e..2123a99 100644 --- a/Services/ColorSchemes.qml +++ b/Services/ColorSchemesService.qml @@ -61,7 +61,7 @@ Singleton { Process { id: generateColorsProcess - command: ["matugen", "image", Wallpapers.currentWallpaper, "--config", Quickshell.shellDir + "/Assets/Matugen/matugen.toml"] + command: ["matugen", "image", WallpapersService.currentWallpaper, "--config", Quickshell.shellDir + "/Assets/Matugen/matugen.toml"] workingDirectory: Quickshell.shellDir running: false stdout: StdioCollector { diff --git a/Services/GitHub.qml b/Services/GitHubService.qml similarity index 98% rename from Services/GitHub.qml rename to Services/GitHubService.qml index 6404578..2e329e5 100644 --- a/Services/GitHub.qml +++ b/Services/GitHubService.qml @@ -1,6 +1,7 @@ import QtQuick import Quickshell import Quickshell.Io +import qs.Commons import qs.Services pragma Singleton @@ -10,7 +11,7 @@ Singleton { property string githubDataFile: Quickshell.env("NOCTALIA_GITHUB_FILE") || (Settings.cacheDir + "github.json") property int githubUpdateFrequency: 60 * 60 // 1 hour expressed in seconds - property var data: adapter // Used to access via GitHub.data.xxx.yyy + property var data: adapter // Used to access via GitHubService.data.xxx.yyy property bool isFetchingData: false // Public properties for easy access diff --git a/Services/Location.qml b/Services/LocationService.qml similarity index 98% rename from Services/Location.qml rename to Services/LocationService.qml index 267f6ee..b70e372 100644 --- a/Services/Location.qml +++ b/Services/LocationService.qml @@ -1,6 +1,7 @@ import QtQuick import Quickshell import Quickshell.Io +import qs.Commons import qs.Services pragma Singleton @@ -10,7 +11,7 @@ Singleton { property string locationFile: Quickshell.env("NOCTALIA_WEATHER_FILE") || (Settings.cacheDir + "location.json") property int weatherUpdateFrequency: 30 * 60 // 30 minutes expressed in seconds - property var data: adapter // Used to access via Location.data.xxx + property var data: adapter // Used to access via LocationService.data.xxx property bool isFetchingWeather: false FileView { diff --git a/Services/MediaPlayer.qml b/Services/MediaService.qml similarity index 99% rename from Services/MediaPlayer.qml rename to Services/MediaService.qml index 4617145..2c5bd1b 100644 --- a/Services/MediaPlayer.qml +++ b/Services/MediaService.qml @@ -3,6 +3,7 @@ pragma Singleton import QtQuick import Quickshell import Quickshell.Services.Mpris +import qs.Commons import qs.Services Singleton { diff --git a/Services/Network.qml b/Services/NetworkService.qml similarity index 99% rename from Services/Network.qml rename to Services/NetworkService.qml index c25c646..0ffa766 100644 --- a/Services/Network.qml +++ b/Services/NetworkService.qml @@ -2,7 +2,7 @@ import QtQuick import Quickshell import Quickshell.Io -QtObject { +Singleton { id: root property var networks: ({}) diff --git a/Services/Niri.qml b/Services/NiriService.qml similarity index 100% rename from Services/Niri.qml rename to Services/NiriService.qml diff --git a/Services/NotificationService.qml b/Services/NotificationService.qml index f771ef3..65ac02e 100644 --- a/Services/NotificationService.qml +++ b/Services/NotificationService.qml @@ -1,6 +1,7 @@ import QtQuick import Quickshell import Quickshell.Io +import qs.Commons import qs.Services import Quickshell.Services.Notifications pragma Singleton diff --git a/Services/PanelManager.qml b/Services/PanelService.qml similarity index 100% rename from Services/PanelManager.qml rename to Services/PanelService.qml diff --git a/Services/Scaling.qml b/Services/ScalingService.qml similarity index 100% rename from Services/Scaling.qml rename to Services/ScalingService.qml diff --git a/Services/ScreenRecorder.qml b/Services/ScreenRecorderService.qml similarity index 99% rename from Services/ScreenRecorder.qml rename to Services/ScreenRecorderService.qml index 4f48d15..ac9d9fa 100644 --- a/Services/ScreenRecorder.qml +++ b/Services/ScreenRecorderService.qml @@ -2,6 +2,7 @@ pragma Singleton import QtQuick import Quickshell +import qs.Commons import qs.Services Singleton { diff --git a/Services/SystemStats.qml b/Services/SystemStatsService.qml similarity index 100% rename from Services/SystemStats.qml rename to Services/SystemStatsService.qml diff --git a/Services/Wallpapers.qml b/Services/WallpapersService.qml similarity index 99% rename from Services/Wallpapers.qml rename to Services/WallpapersService.qml index 448df02..4ca8761 100644 --- a/Services/Wallpapers.qml +++ b/Services/WallpapersService.qml @@ -66,7 +66,7 @@ Singleton { // Only notify ColorSchemes service if the wallpaper actually changed if (wallpaperChanged) { - ColorSchemes.changedWallpaper() + ColorSchemesService.changedWallpaper() } } diff --git a/Services/Workspaces.qml b/Services/WorkspacesService.qml similarity index 97% rename from Services/Workspaces.qml rename to Services/WorkspacesService.qml index 12acdba..1d540a6 100644 --- a/Services/Workspaces.qml +++ b/Services/WorkspacesService.qml @@ -6,6 +6,7 @@ import QtQuick import Quickshell import Quickshell.Io import Quickshell.Hyprland +import qs.Commons import qs.Services Singleton { @@ -106,14 +107,14 @@ Singleton { } Connections { - target: Niri + target: NiriService function onWorkspacesChanged() { updateNiriWorkspaces() } } function updateNiriWorkspaces() { - const niriWorkspaces = Niri.workspaces || [] + const niriWorkspaces = NiriService.workspaces || [] workspaces.clear() for (var i = 0; i < niriWorkspaces.length; i++) { const ws = niriWorkspaces[i] diff --git a/Widgets/NBox.qml b/Widgets/NBox.qml index a29d07d..ffa69f4 100644 --- a/Widgets/NBox.qml +++ b/Widgets/NBox.qml @@ -1,4 +1,6 @@ import QtQuick +import qs.Commons +import qs.Commons import qs.Services // Rounded group container using the variant surface color. diff --git a/Widgets/NBusyIndicator.qml b/Widgets/NBusyIndicator.qml index 8098fde..bef3a28 100644 --- a/Widgets/NBusyIndicator.qml +++ b/Widgets/NBusyIndicator.qml @@ -1,4 +1,5 @@ import QtQuick +import qs.Commons import qs.Services Item { diff --git a/Widgets/NCard.qml b/Widgets/NCard.qml index 0fcfd1e..db01764 100644 --- a/Widgets/NCard.qml +++ b/Widgets/NCard.qml @@ -1,4 +1,5 @@ import QtQuick +import qs.Commons import qs.Services // Generic card container diff --git a/Widgets/NCircleStat.qml b/Widgets/NCircleStat.qml index d1aeb6e..81d66bf 100644 --- a/Widgets/NCircleStat.qml +++ b/Widgets/NCircleStat.qml @@ -1,4 +1,5 @@ import QtQuick +import qs.Commons import qs.Services // Compact circular statistic display used in the SidePanel diff --git a/Widgets/NClock.qml b/Widgets/NClock.qml index 25ab5c4..b46a0c5 100644 --- a/Widgets/NClock.qml +++ b/Widgets/NClock.qml @@ -1,4 +1,5 @@ import QtQuick +import qs.Commons import qs.Services import qs.Widgets diff --git a/Widgets/NComboBox.qml b/Widgets/NComboBox.qml index 4504d49..fa4ffa1 100644 --- a/Widgets/NComboBox.qml +++ b/Widgets/NComboBox.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import qs.Commons import qs.Services import qs.Widgets diff --git a/Widgets/NDivider.qml b/Widgets/NDivider.qml index 7b82fe2..ca7cbcb 100644 --- a/Widgets/NDivider.qml +++ b/Widgets/NDivider.qml @@ -1,6 +1,7 @@ import QtQuick import Quickshell import Quickshell.Widgets +import qs.Commons import qs.Services Rectangle { diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml index 933411d..6b74153 100644 --- a/Widgets/NIconButton.qml +++ b/Widgets/NIconButton.qml @@ -1,6 +1,7 @@ import QtQuick import Quickshell import Quickshell.Widgets +import qs.Commons import qs.Services Rectangle { diff --git a/Widgets/NImageRounded.qml b/Widgets/NImageRounded.qml index 6fb2ba8..edf3b2b 100644 --- a/Widgets/NImageRounded.qml +++ b/Widgets/NImageRounded.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Effects import Quickshell import Quickshell.Widgets +import qs.Commons import qs.Services Rectangle { diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 0c6878b..3407a70 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -1,12 +1,13 @@ import QtQuick import Quickshell import Quickshell.Wayland +import qs.Commons import qs.Services PanelWindow { id: root - readonly property real scaling: Scaling.scale(screen) + readonly property real scaling: ScalingService.scale(screen) property bool showOverlay: Settings.data.general.dimDesktop property int topMargin: Style.barHeight * scaling @@ -19,12 +20,12 @@ PanelWindow { } function show() { - // Ensure only one panel is visible at a time using PanelManager as ephemeral storage + // Ensure only one panel is visible at a time using PanelService as ephemeral storage try { - if (PanelManager.openedPanel && PanelManager.openedPanel !== root && PanelManager.openedPanel.hide) { - PanelManager.openedPanel.hide() + if (PanelService.openedPanel && PanelService.openedPanel !== root && PanelService.openedPanel.hide) { + PanelService.openedPanel.hide() } - PanelManager.openedPanel = root + PanelService.openedPanel = root } catch (e) { // ignore diff --git a/Widgets/NPill.qml b/Widgets/NPill.qml index 07b9c70..4ed4f46 100644 --- a/Widgets/NPill.qml +++ b/Widgets/NPill.qml @@ -1,5 +1,6 @@ import QtQuick import QtQuick.Controls +import qs.Commons import qs.Services Item { diff --git a/Widgets/NRadioButton.qml b/Widgets/NRadioButton.qml index d9b232f..34b7f20 100644 --- a/Widgets/NRadioButton.qml +++ b/Widgets/NRadioButton.qml @@ -1,5 +1,6 @@ import QtQuick import QtQuick.Controls +import qs.Commons import qs.Services import qs.Widgets diff --git a/Widgets/NSlider.qml b/Widgets/NSlider.qml index 875f988..642dbd7 100644 --- a/Widgets/NSlider.qml +++ b/Widgets/NSlider.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Effects +import qs.Commons import qs.Services Slider { diff --git a/Widgets/NText.qml b/Widgets/NText.qml index ca0f1fb..656f632 100644 --- a/Widgets/NText.qml +++ b/Widgets/NText.qml @@ -1,4 +1,5 @@ import QtQuick +import qs.Commons import qs.Services import qs.Widgets diff --git a/Widgets/NTextInput.qml b/Widgets/NTextInput.qml index 4c30955..0ced0f0 100644 --- a/Widgets/NTextInput.qml +++ b/Widgets/NTextInput.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import qs.Commons import qs.Services Item { diff --git a/Widgets/NToggle.qml b/Widgets/NToggle.qml index 735a7ae..4dea6fd 100644 --- a/Widgets/NToggle.qml +++ b/Widgets/NToggle.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import qs.Commons import qs.Services RowLayout { diff --git a/Widgets/NTooltip.qml b/Widgets/NTooltip.qml index 516a5af..aba0660 100644 --- a/Widgets/NTooltip.qml +++ b/Widgets/NTooltip.qml @@ -1,4 +1,5 @@ import QtQuick +import qs.Commons import qs.Services Window { diff --git a/shell.qml b/shell.qml index 5b52447..262772f 100644 --- a/shell.qml +++ b/shell.qml @@ -4,6 +4,7 @@ import Quickshell import Quickshell.Io import Quickshell.Services.Pipewire import Quickshell.Widgets +import qs.Commons import qs.Modules.AppLauncher import qs.Modules.Background import qs.Modules.Bar @@ -62,6 +63,6 @@ ShellRoot { Component.onCompleted: { // Ensure our singleton is created as soon as possible so we start fetching weather asap - Location.init() + LocationService.init() } } From 605e3610d20b1a71410075a42d7b00c66f073099 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 21:46:42 -0400 Subject: [PATCH 340/394] Missing Commons --- Services/BrightnessService.qml | 1 + Services/WallpapersService.qml | 1 + 2 files changed, 2 insertions(+) diff --git a/Services/BrightnessService.qml b/Services/BrightnessService.qml index 9dd9e15..85eb9fd 100644 --- a/Services/BrightnessService.qml +++ b/Services/BrightnessService.qml @@ -5,6 +5,7 @@ pragma ComponentBehavior import QtQuick import Quickshell import Quickshell.Io +import qs.Commons Singleton { id: root diff --git a/Services/WallpapersService.qml b/Services/WallpapersService.qml index 4ca8761..13a8f11 100644 --- a/Services/WallpapersService.qml +++ b/Services/WallpapersService.qml @@ -4,6 +4,7 @@ import QtQuick import Qt.labs.folderlistmodel import Quickshell import Quickshell.Io +import qs.Commons Singleton { id: root From 4631239b9294ef15c7dbf8c5d4626f6e4ce8ad47 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 21:55:32 -0400 Subject: [PATCH 341/394] Post refactoring fixes 1/?? --- Modules/Bar/Brightness.qml | 2 +- Modules/Bar/Volume.qml | 2 +- Modules/Bar/WiFiMenu.qml | 4 ++-- Modules/{Settings => SettingsPanel}/SettingsPanel.qml | 2 +- Modules/{Settings => SettingsPanel}/Tabs/AboutTab.qml | 0 Modules/{Settings => SettingsPanel}/Tabs/AudioTab.qml | 2 -- Modules/{Settings => SettingsPanel}/Tabs/BarTab.qml | 0 Modules/{Settings => SettingsPanel}/Tabs/ColorSchemeTab.qml | 0 Modules/{Settings => SettingsPanel}/Tabs/DisplayTab.qml | 0 Modules/{Settings => SettingsPanel}/Tabs/GeneralTab.qml | 0 Modules/{Settings => SettingsPanel}/Tabs/NetworkTab.qml | 0 .../{Settings => SettingsPanel}/Tabs/ScreenRecorderTab.qml | 0 Modules/{Settings => SettingsPanel}/Tabs/TimeWeatherTab.qml | 0 .../{Settings => SettingsPanel}/Tabs/WallpaperSelectorTab.qml | 0 Modules/{Settings => SettingsPanel}/Tabs/WallpaperTab.qml | 0 Modules/SidePanel/Cards/ProfileCard.qml | 2 +- Modules/SidePanel/Cards/UtilitiesCard.qml | 2 +- Services/PanelService.qml | 1 - shell.qml | 2 +- 19 files changed, 8 insertions(+), 11 deletions(-) rename Modules/{Settings => SettingsPanel}/SettingsPanel.qml (99%) rename Modules/{Settings => SettingsPanel}/Tabs/AboutTab.qml (100%) rename Modules/{Settings => SettingsPanel}/Tabs/AudioTab.qml (99%) rename Modules/{Settings => SettingsPanel}/Tabs/BarTab.qml (100%) rename Modules/{Settings => SettingsPanel}/Tabs/ColorSchemeTab.qml (100%) rename Modules/{Settings => SettingsPanel}/Tabs/DisplayTab.qml (100%) rename Modules/{Settings => SettingsPanel}/Tabs/GeneralTab.qml (100%) rename Modules/{Settings => SettingsPanel}/Tabs/NetworkTab.qml (100%) rename Modules/{Settings => SettingsPanel}/Tabs/ScreenRecorderTab.qml (100%) rename Modules/{Settings => SettingsPanel}/Tabs/TimeWeatherTab.qml (100%) rename Modules/{Settings => SettingsPanel}/Tabs/WallpaperSelectorTab.qml (100%) rename Modules/{Settings => SettingsPanel}/Tabs/WallpaperTab.qml (100%) diff --git a/Modules/Bar/Brightness.qml b/Modules/Bar/Brightness.qml index 8b12050..d336c14 100644 --- a/Modules/Bar/Brightness.qml +++ b/Modules/Bar/Brightness.qml @@ -1,7 +1,7 @@ import QtQuick import Quickshell -import qs.Modules.Settings import qs.Commons +import qs.Modules.SettingsPanel import qs.Services import qs.Widgets diff --git a/Modules/Bar/Volume.qml b/Modules/Bar/Volume.qml index 03a3889..4293e0c 100644 --- a/Modules/Bar/Volume.qml +++ b/Modules/Bar/Volume.qml @@ -1,8 +1,8 @@ import QtQuick import Quickshell import Quickshell.Services.Pipewire -import qs.Modules.Settings import qs.Commons +import qs.Modules.SettingsPanel import qs.Services import qs.Widgets diff --git a/Modules/Bar/WiFiMenu.qml b/Modules/Bar/WiFiMenu.qml index 44ad9bc..e052a11 100644 --- a/Modules/Bar/WiFiMenu.qml +++ b/Modules/Bar/WiFiMenu.qml @@ -188,11 +188,11 @@ NLoader { // Loading indicator ColumnLayout { anchors.centerIn: parent - visible: Settings.data.network.wifiEnabled && network.isLoading + visible: Settings.data.network.wifiEnabled && NetworkService.isLoading spacing: Style.marginMedium * scaling NBusyIndicator { - running: network.isLoading + running: NetworkService.isLoading color: Colors.mPrimary size: Style.baseWidgetSize * scaling Layout.alignment: Qt.AlignHCenter diff --git a/Modules/Settings/SettingsPanel.qml b/Modules/SettingsPanel/SettingsPanel.qml similarity index 99% rename from Modules/Settings/SettingsPanel.qml rename to Modules/SettingsPanel/SettingsPanel.qml index d423b3f..aa600cb 100644 --- a/Modules/Settings/SettingsPanel.qml +++ b/Modules/SettingsPanel/SettingsPanel.qml @@ -3,7 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import Quickshell import Quickshell.Wayland -import qs.Modules.Settings.Tabs as Tabs +import qs.Modules.SettingsPanel.Tabs as Tabs import qs.Commons import qs.Services import qs.Widgets diff --git a/Modules/Settings/Tabs/AboutTab.qml b/Modules/SettingsPanel/Tabs/AboutTab.qml similarity index 100% rename from Modules/Settings/Tabs/AboutTab.qml rename to Modules/SettingsPanel/Tabs/AboutTab.qml diff --git a/Modules/Settings/Tabs/AudioTab.qml b/Modules/SettingsPanel/Tabs/AudioTab.qml similarity index 99% rename from Modules/Settings/Tabs/AudioTab.qml rename to Modules/SettingsPanel/Tabs/AudioTab.qml index 8944043..3ac41ea 100644 --- a/Modules/Settings/Tabs/AudioTab.qml +++ b/Modules/SettingsPanel/Tabs/AudioTab.qml @@ -2,8 +2,6 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell.Services.Pipewire - -import qs.Modules.Settings import qs.Widgets import qs.Commons import qs.Services diff --git a/Modules/Settings/Tabs/BarTab.qml b/Modules/SettingsPanel/Tabs/BarTab.qml similarity index 100% rename from Modules/Settings/Tabs/BarTab.qml rename to Modules/SettingsPanel/Tabs/BarTab.qml diff --git a/Modules/Settings/Tabs/ColorSchemeTab.qml b/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml similarity index 100% rename from Modules/Settings/Tabs/ColorSchemeTab.qml rename to Modules/SettingsPanel/Tabs/ColorSchemeTab.qml diff --git a/Modules/Settings/Tabs/DisplayTab.qml b/Modules/SettingsPanel/Tabs/DisplayTab.qml similarity index 100% rename from Modules/Settings/Tabs/DisplayTab.qml rename to Modules/SettingsPanel/Tabs/DisplayTab.qml diff --git a/Modules/Settings/Tabs/GeneralTab.qml b/Modules/SettingsPanel/Tabs/GeneralTab.qml similarity index 100% rename from Modules/Settings/Tabs/GeneralTab.qml rename to Modules/SettingsPanel/Tabs/GeneralTab.qml diff --git a/Modules/Settings/Tabs/NetworkTab.qml b/Modules/SettingsPanel/Tabs/NetworkTab.qml similarity index 100% rename from Modules/Settings/Tabs/NetworkTab.qml rename to Modules/SettingsPanel/Tabs/NetworkTab.qml diff --git a/Modules/Settings/Tabs/ScreenRecorderTab.qml b/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml similarity index 100% rename from Modules/Settings/Tabs/ScreenRecorderTab.qml rename to Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml diff --git a/Modules/Settings/Tabs/TimeWeatherTab.qml b/Modules/SettingsPanel/Tabs/TimeWeatherTab.qml similarity index 100% rename from Modules/Settings/Tabs/TimeWeatherTab.qml rename to Modules/SettingsPanel/Tabs/TimeWeatherTab.qml diff --git a/Modules/Settings/Tabs/WallpaperSelectorTab.qml b/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml similarity index 100% rename from Modules/Settings/Tabs/WallpaperSelectorTab.qml rename to Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml diff --git a/Modules/Settings/Tabs/WallpaperTab.qml b/Modules/SettingsPanel/Tabs/WallpaperTab.qml similarity index 100% rename from Modules/Settings/Tabs/WallpaperTab.qml rename to Modules/SettingsPanel/Tabs/WallpaperTab.qml diff --git a/Modules/SidePanel/Cards/ProfileCard.qml b/Modules/SidePanel/Cards/ProfileCard.qml index c5e8c5a..544f129 100644 --- a/Modules/SidePanel/Cards/ProfileCard.qml +++ b/Modules/SidePanel/Cards/ProfileCard.qml @@ -4,7 +4,7 @@ import QtQuick.Layouts import Quickshell import Quickshell.Io import Quickshell.Widgets -import qs.Modules.Settings +import qs.Modules.SettingsPanel import qs.Modules.SidePanel import qs.Commons import qs.Services diff --git a/Modules/SidePanel/Cards/UtilitiesCard.qml b/Modules/SidePanel/Cards/UtilitiesCard.qml index 11c8a96..f5fc449 100644 --- a/Modules/SidePanel/Cards/UtilitiesCard.qml +++ b/Modules/SidePanel/Cards/UtilitiesCard.qml @@ -2,8 +2,8 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell -import qs.Modules.Settings import qs.Commons +import qs.Modules.SettingsPanel import qs.Services import qs.Widgets diff --git a/Services/PanelService.qml b/Services/PanelService.qml index 1c50d60..2e56987 100644 --- a/Services/PanelService.qml +++ b/Services/PanelService.qml @@ -1,7 +1,6 @@ pragma Singleton import Quickshell -import qs.Modules.Settings Singleton { id: root diff --git a/shell.qml b/shell.qml index 262772f..9ab1894 100644 --- a/shell.qml +++ b/shell.qml @@ -13,7 +13,7 @@ import qs.Modules.Demo import qs.Modules.Dock import qs.Modules.LockScreen import qs.Modules.Notification -import qs.Modules.Settings +import qs.Modules.SettingsPanel import qs.Modules.SidePanel import qs.Services import qs.Widgets From 258bb37533f6260745e5d860f6401db1c0b09fc9 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 22:06:39 -0400 Subject: [PATCH 342/394] Post refactoring fixes 2/? --- Commons/Settings.qml | 1 - Modules/Bar/ActiveWindow.qml | 9 +++++---- Modules/Bar/WiFiMenu.qml | 4 ++-- {Commons => Modules/IPC}/IPCManager.qml | 0 Modules/SettingsPanel/Tabs/BarTab.qml | 9 --------- Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml | 3 ++- Modules/SidePanel/Cards/WeatherCard.qml | 3 ++- Services/ColorSchemesService.qml | 1 + Services/WorkspacesService.qml | 2 +- shell.qml | 1 + 10 files changed, 14 insertions(+), 19 deletions(-) rename {Commons => Modules/IPC}/IPCManager.qml (100%) diff --git a/Commons/Settings.qml b/Commons/Settings.qml index f8cc593..32430e4 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -75,7 +75,6 @@ Singleton { property bool showActiveWindow: true property bool showSystemInfo: false property bool showMedia: false - property bool showTaskbar: false property list monitors: [] } diff --git a/Modules/Bar/ActiveWindow.qml b/Modules/Bar/ActiveWindow.qml index 366e1d3..4e5aafd 100644 --- a/Modules/Bar/ActiveWindow.qml +++ b/Modules/Bar/ActiveWindow.qml @@ -5,6 +5,7 @@ import qs.Commons import qs.Services import qs.Widgets + Row { id: layout anchors.verticalCenter: parent.verticalCenter @@ -27,7 +28,7 @@ Row { // Update text when window changes Connections { - target: typeof Niri !== "undefined" ? Niri : null + target: typeof NiriService !== "undefined" ? NiriService : null function onFocusedWindowIndexChanged() { // Check if window actually changed if (NiriService.focusedWindowIndex !== lastWindowIndex) { @@ -92,13 +93,13 @@ Row { function getDisplayText() { // Check if Niri service is available - if (typeof Niri === "undefined") { + if (typeof NiriService === "undefined") { return "" } // Get the focused window data - const focusedWindow = NiriService.focusedWindowIndex >= 0 - && NiriService.focusedWindowIndex < NiriService.windows.length ? NiriService.windows[NiriService.focusedWindowIndex] : null + const focusedWindow = NiriService.focusedWindowIndex >= 0 && NiriService.focusedWindowIndex + < NiriService.windows.length ? NiriService.windows[NiriService.focusedWindowIndex] : null if (!focusedWindow) { return "" diff --git a/Modules/Bar/WiFiMenu.qml b/Modules/Bar/WiFiMenu.qml index e052a11..e0c37df 100644 --- a/Modules/Bar/WiFiMenu.qml +++ b/Modules/Bar/WiFiMenu.qml @@ -294,8 +294,8 @@ NLoader { } NText { - visible: NetworkService.connectStatusSsid === modelData.ssid && NetworkService.connectStatus === "error" - && network.connectError.length > 0 + visible: NetworkService.connectStatusSsid === modelData.ssid + && NetworkService.connectStatus === "error" && network.connectError.length > 0 text: NetworkService.connectError color: Colors.mError font.pointSize: Style.fontSizeSmall * scaling diff --git a/Commons/IPCManager.qml b/Modules/IPC/IPCManager.qml similarity index 100% rename from Commons/IPCManager.qml rename to Modules/IPC/IPCManager.qml diff --git a/Modules/SettingsPanel/Tabs/BarTab.qml b/Modules/SettingsPanel/Tabs/BarTab.qml index 892fc96..4698432 100644 --- a/Modules/SettingsPanel/Tabs/BarTab.qml +++ b/Modules/SettingsPanel/Tabs/BarTab.qml @@ -58,15 +58,6 @@ ColumnLayout { } } - NToggle { - label: "Show Taskbar" - description: "Display a taskbar showing currently open windows" - value: Settings.data.bar.showTaskbar - onToggled: function (newValue) { - Settings.data.bar.showTaskbar = newValue - } - } - NToggle { label: "Show Media" description: "Display media controls and information" diff --git a/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml b/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml index 20faf93..7481da1 100644 --- a/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml +++ b/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml @@ -107,7 +107,8 @@ Item { Item { Layout.fillWidth: true Layout.preferredHeight: { - return Math.ceil(WallpapersService.wallpaperList.length / wallpaperGridView.columns) * wallpaperGridView.cellHeight + return Math.ceil( + WallpapersService.wallpaperList.length / wallpaperGridView.columns) * wallpaperGridView.cellHeight } GridView { diff --git a/Modules/SidePanel/Cards/WeatherCard.qml b/Modules/SidePanel/Cards/WeatherCard.qml index 58bf982..274689f 100644 --- a/Modules/SidePanel/Cards/WeatherCard.qml +++ b/Modules/SidePanel/Cards/WeatherCard.qml @@ -27,7 +27,8 @@ NBox { RowLayout { spacing: Style.marginSmall * scaling NText { - text: weatherReady ? LocationService.weatherSymbolFromCode(LocationService.data.weather.current_weather.weathercode) : "" + text: weatherReady ? LocationService.weatherSymbolFromCode( + LocationService.data.weather.current_weather.weathercode) : "" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXXL * 1.5 * scaling color: Colors.mPrimary diff --git a/Services/ColorSchemesService.qml b/Services/ColorSchemesService.qml index 2123a99..85d9c2c 100644 --- a/Services/ColorSchemesService.qml +++ b/Services/ColorSchemesService.qml @@ -4,6 +4,7 @@ import QtQuick import Qt.labs.folderlistmodel import Quickshell import Quickshell.Io +import qs.Commons Singleton { id: root diff --git a/Services/WorkspacesService.qml b/Services/WorkspacesService.qml index 1d540a6..ffa50eb 100644 --- a/Services/WorkspacesService.qml +++ b/Services/WorkspacesService.qml @@ -34,7 +34,7 @@ Singleton { } - if (typeof Niri !== "undefined") { + if (typeof NiriService !== "undefined") { isHyprland = false isNiri = true initNiri() diff --git a/shell.qml b/shell.qml index 9ab1894..5134397 100644 --- a/shell.qml +++ b/shell.qml @@ -11,6 +11,7 @@ import qs.Modules.Bar import qs.Modules.Calendar import qs.Modules.Demo import qs.Modules.Dock +import qs.Modules.IPC import qs.Modules.LockScreen import qs.Modules.Notification import qs.Modules.SettingsPanel From c371ea68a3f660c1002bce3f5cf041bb1901d755 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 22:19:18 -0400 Subject: [PATCH 343/394] Post refactoring fixes 3/? --- Modules/Bar/ActiveWindow.qml | 1 - Modules/Bar/WiFi.qml | 2 +- Modules/Bar/WiFiMenu.qml | 13 +++++++------ Services/NetworkService.qml | 22 +++++++++++++--------- 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/Modules/Bar/ActiveWindow.qml b/Modules/Bar/ActiveWindow.qml index 4e5aafd..7fd9cfb 100644 --- a/Modules/Bar/ActiveWindow.qml +++ b/Modules/Bar/ActiveWindow.qml @@ -5,7 +5,6 @@ import qs.Commons import qs.Services import qs.Widgets - Row { id: layout anchors.verticalCenter: parent.verticalCenter diff --git a/Modules/Bar/WiFi.qml b/Modules/Bar/WiFi.qml index ee4bb9d..cd8a620 100644 --- a/Modules/Bar/WiFi.qml +++ b/Modules/Bar/WiFi.qml @@ -19,7 +19,7 @@ NIconButton { for (const net in NetworkService.networks) { if (NetworkService.networks[net].connected) { connected = true - signalStrength = network.networks[net].signal + signalStrength = NetworkService.networks[net].signal break } } diff --git a/Modules/Bar/WiFiMenu.qml b/Modules/Bar/WiFiMenu.qml index e0c37df..c292c55 100644 --- a/Modules/Bar/WiFiMenu.qml +++ b/Modules/Bar/WiFiMenu.qml @@ -62,7 +62,7 @@ NLoader { onTriggered: { wifiPanel.visible = false wifiPanel.dismissed() - NetworkService.onMenuClosed() + // NetworkService.onMenuClosed() } } @@ -163,7 +163,7 @@ NLoader { icon: "refresh" tooltipText: "Refresh Networks" sizeMultiplier: 0.8 - enabled: Settings.data.network.wifiEnabled && !network.isLoading + enabled: Settings.data.network.wifiEnabled && !NetworkService.isLoading onClicked: { NetworkService.refreshNetworks() } @@ -239,8 +239,8 @@ NLoader { ListView { id: networkList anchors.fill: parent - visible: Settings.data.network.wifiEnabled && !network.isLoading - model: Object.values(network.networks) + visible: Settings.data.network.wifiEnabled && !NetworkService.isLoading + model: Object.values(NetworkService.networks) spacing: Style.marginMedium * scaling clip: true @@ -295,7 +295,7 @@ NLoader { NText { visible: NetworkService.connectStatusSsid === modelData.ssid - && NetworkService.connectStatus === "error" && network.connectError.length > 0 + && NetworkService.connectStatus === "error" && NetworkService.connectError.length > 0 text: NetworkService.connectError color: Colors.mError font.pointSize: Style.fontSizeSmall * scaling @@ -308,7 +308,8 @@ NLoader { Layout.preferredWidth: Style.baseWidgetSize * 0.7 * scaling Layout.preferredHeight: Style.baseWidgetSize * 0.7 * scaling visible: NetworkService.connectStatusSsid === modelData.ssid - && (network.connectStatus !== "" || NetworkService.connectingSsid === modelData.ssid) + && (NetworkService.connectStatus !== "" + || NetworkService.connectingSsid === modelData.ssid) NBusyIndicator { visible: NetworkService.connectingSsid === modelData.ssid diff --git a/Services/NetworkService.qml b/Services/NetworkService.qml index 0ffa766..07e5847 100644 --- a/Services/NetworkService.qml +++ b/Services/NetworkService.qml @@ -1,6 +1,9 @@ +pragma Singleton + import QtQuick import Quickshell import Quickshell.Io +import qs.Commons Singleton { id: root @@ -14,6 +17,14 @@ Singleton { property string lastConnectedNetwork: "" property bool isLoading: false + Component.onCompleted: { + console.log("[Network] Service started") + // Only refresh networks if WiFi is enabled + if (Settings.data.network.wifiEnabled) { + refreshNetworks() + } + } + function signalIcon(signal) { if (signal >= 80) return "network_wifi" @@ -252,7 +263,7 @@ Singleton { for (var i = 0; i < lines.length; ++i) { const line = lines[i].trim() if (!line) - continue + continue const parts = line.split(":") if (parts.length < 2) { @@ -291,7 +302,7 @@ Singleton { for (var i = 0; i < lines.length; ++i) { const line = lines[i].trim() if (!line) - continue + continue const parts = line.split(":") if (parts.length < 4) { @@ -456,11 +467,4 @@ Singleton { } } } - - Component.onCompleted: { - // Only refresh networks if WiFi is enabled - if (Settings.data.network.wifiEnabled) { - refreshNetworks() - } - } } From fc4bd796f12bcb6298301652e07228eb28d7f42c Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 23:15:16 -0400 Subject: [PATCH 344/394] Trying to fix a weird binding bug with NToggle --- Modules/Bar/WiFiMenu.qml | 10 +++++----- Modules/Demo/DemoPanel.qml | 4 ++-- Modules/SettingsPanel/Tabs/AudioTab.qml | 6 +++--- Modules/SettingsPanel/Tabs/BarTab.qml | 18 +++++++++--------- Modules/SettingsPanel/Tabs/ColorSchemeTab.qml | 6 +++--- Modules/SettingsPanel/Tabs/DisplayTab.qml | 18 +++++++++--------- Modules/SettingsPanel/Tabs/GeneralTab.qml | 18 +++++++++--------- Modules/SettingsPanel/Tabs/NetworkTab.qml | 12 ++++++------ .../SettingsPanel/Tabs/ScreenRecorderTab.qml | 6 +++--- Modules/SettingsPanel/Tabs/TimeWeatherTab.qml | 18 +++++++++--------- Modules/SettingsPanel/Tabs/WallpaperTab.qml | 12 ++++++------ Widgets/NToggle.qml | 18 +++++++++--------- 12 files changed, 73 insertions(+), 73 deletions(-) diff --git a/Modules/Bar/WiFiMenu.qml b/Modules/Bar/WiFiMenu.qml index c292c55..446fe12 100644 --- a/Modules/Bar/WiFiMenu.qml +++ b/Modules/Bar/WiFiMenu.qml @@ -147,13 +147,13 @@ NLoader { NToggle { baseSize: Style.baseWidgetSize * 0.75 - value: Settings.data.network.wifiEnabled - onToggled: function (value) { - Settings.data.network.wifiEnabled = value - NetworkService.setWifiEnabled(value) + checked: Settings.data.network.wifiEnabled + onToggled: checked => { + Settings.data.network.wifiEnabled = checked + NetworkService.setWifiEnabled(checked) // If enabling WiFi while menu is open, refresh after a delay - if (value) { + if (checked) { wifiEnableRefreshTimer.start() } } diff --git a/Modules/Demo/DemoPanel.qml b/Modules/Demo/DemoPanel.qml index 4dc220e..9ef9da3 100644 --- a/Modules/Demo/DemoPanel.qml +++ b/Modules/Demo/DemoPanel.qml @@ -201,8 +201,8 @@ NLoader { NToggle { label: "Label" description: "Description" - onToggled: function (value) { - console.log("[DemoPanel] NToggle:", value) + onToggled: checked => { + console.log("[DemoPanel] NToggle:", checked) } } diff --git a/Modules/SettingsPanel/Tabs/AudioTab.qml b/Modules/SettingsPanel/Tabs/AudioTab.qml index 3ac41ea..d8118de 100644 --- a/Modules/SettingsPanel/Tabs/AudioTab.qml +++ b/Modules/SettingsPanel/Tabs/AudioTab.qml @@ -123,10 +123,10 @@ ColumnLayout { NToggle { label: "Mute AudioService" description: "Mute or unmute the default audio output" - value: AudioService.muted - onToggled: function (newValue) { + checked: AudioService.muted + onToggled: checked => { if (AudioService.sink && AudioService.sink.audio) { - AudioService.sink.audio.muted = newValue + AudioService.sink.audio.muted = checked } } } diff --git a/Modules/SettingsPanel/Tabs/BarTab.qml b/Modules/SettingsPanel/Tabs/BarTab.qml index 4698432..f26a103 100644 --- a/Modules/SettingsPanel/Tabs/BarTab.qml +++ b/Modules/SettingsPanel/Tabs/BarTab.qml @@ -43,27 +43,27 @@ ColumnLayout { NToggle { label: "Show Active Window" description: "Display the title of the currently focused window below the bar" - value: Settings.data.bar.showActiveWindow - onToggled: function (newValue) { - Settings.data.bar.showActiveWindow = newValue + checked: Settings.data.bar.showActiveWindow + onToggled: checked => { + Settings.data.bar.showActiveWindow = checked } } NToggle { label: "Show System Info" description: "Display system information (CPU, RAM, Temperature)" - value: Settings.data.bar.showSystemInfo - onToggled: function (newValue) { - Settings.data.bar.showSystemInfo = newValue + checked: Settings.data.bar.showSystemInfo + onToggled: checked => { + Settings.data.bar.showSystemInfo = checked } } NToggle { label: "Show Media" description: "Display media controls and information" - value: Settings.data.bar.showMedia - onToggled: function (newValue) { - Settings.data.bar.showMedia = newValue + checked: Settings.data.bar.showMedia + onToggled: checked => { + Settings.data.bar.showMedia = checked } } } diff --git a/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml b/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml index 390cb6c..73a97eb 100644 --- a/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml +++ b/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml @@ -129,9 +129,9 @@ ColumnLayout { NToggle { label: "Use Matugen" description: "Automatically generate colors based on your active wallpaper using Matugen" - value: Settings.data.colorSchemes.useWallpaperColors - onToggled: function (newValue) { - Settings.data.colorSchemes.useWallpaperColors = newValue + checked: Settings.data.colorSchemes.useWallpaperColors + onToggled: checked => { + Settings.data.colorSchemes.useWallpaperColors = checked if (Settings.data.colorSchemes.useWallpaperColors) { ColorSchemesService.changedWallpaper() } diff --git a/Modules/SettingsPanel/Tabs/DisplayTab.qml b/Modules/SettingsPanel/Tabs/DisplayTab.qml index 3b46880..cf53a74 100644 --- a/Modules/SettingsPanel/Tabs/DisplayTab.qml +++ b/Modules/SettingsPanel/Tabs/DisplayTab.qml @@ -89,9 +89,9 @@ Item { NToggle { label: "Bar" description: "Enable the top bar on this monitor" - value: (Settings.data.bar.monitors || []).indexOf(modelData.name) !== -1 - onToggled: function (newValue) { - if (newValue) { + checked: (Settings.data.bar.monitors || []).indexOf(modelData.name) !== -1 + onToggled: checked => { + if (checked) { Settings.data.bar.monitors = addMonitor(Settings.data.bar.monitors, modelData.name) } else { Settings.data.bar.monitors = removeMonitor(Settings.data.bar.monitors, modelData.name) @@ -102,9 +102,9 @@ Item { NToggle { label: "Notifications" description: "Enable notifications on this monitor" - value: (Settings.data.notifications.monitors || []).indexOf(modelData.name) !== -1 - onToggled: function (newValue) { - if (newValue) { + checked: (Settings.data.notifications.monitors || []).indexOf(modelData.name) !== -1 + onToggled: checked => { + if (checked) { Settings.data.notifications.monitors = addMonitor(Settings.data.notifications.monitors, modelData.name) } else { @@ -117,9 +117,9 @@ Item { NToggle { label: "Dock" description: "Enable the dock on this monitor" - value: (Settings.data.dock.monitors || []).indexOf(modelData.name) !== -1 - onToggled: function (newValue) { - if (newValue) { + checked: (Settings.data.dock.monitors || []).indexOf(modelData.name) !== -1 + onToggled: checked => { + if (checked) { Settings.data.dock.monitors = addMonitor(Settings.data.dock.monitors, modelData.name) } else { Settings.data.dock.monitors = removeMonitor(Settings.data.dock.monitors, modelData.name) diff --git a/Modules/SettingsPanel/Tabs/GeneralTab.qml b/Modules/SettingsPanel/Tabs/GeneralTab.qml index aae1f08..f42766c 100644 --- a/Modules/SettingsPanel/Tabs/GeneralTab.qml +++ b/Modules/SettingsPanel/Tabs/GeneralTab.qml @@ -95,27 +95,27 @@ ColumnLayout { NToggle { label: "Show Corners" description: "Display rounded corners on the edge of the screen" - value: Settings.data.general.showScreenCorners - onToggled: function (v) { - Settings.data.general.showScreenCorners = v + checked: Settings.data.general.showScreenCorners + onToggled: checked => { + Settings.data.general.showScreenCorners = checked } } NToggle { label: "Dim Desktop" description: "Dim the desktop when panels or menus are open" - value: Settings.data.general.dimDesktop - onToggled: function (v) { - Settings.data.general.dimDesktop = v + checked: Settings.data.general.dimDesktop + onToggled: checked => { + Settings.data.general.dimDesktop = checked } } NToggle { label: "Auto-hide Dock" description: "Automatically hide the dock when not in use" - value: Settings.data.dock.autoHide - onToggled: function (v) { - Settings.data.dock.autoHide = v + checked: Settings.data.dock.autoHide + onToggled: checked => { + Settings.data.dock.autoHide = checked } } } diff --git a/Modules/SettingsPanel/Tabs/NetworkTab.qml b/Modules/SettingsPanel/Tabs/NetworkTab.qml index e90fccf..ab1e0b9 100644 --- a/Modules/SettingsPanel/Tabs/NetworkTab.qml +++ b/Modules/SettingsPanel/Tabs/NetworkTab.qml @@ -45,18 +45,18 @@ ColumnLayout { NToggle { label: "WiFi Enabled" description: "Enable WiFi connectivity" - value: Settings.data.network.wifiEnabled - onToggled: function (newValue) { - Settings.data.network.wifiEnabled = newValue + checked: Settings.data.network.wifiEnabled + onToggled: checked => { + Settings.data.network.wifiEnabled = checked } } NToggle { label: "Bluetooth Enabled" description: "Enable Bluetooth connectivity" - value: Settings.data.network.bluetoothEnabled - onToggled: function (newValue) { - Settings.data.network.bluetoothEnabled = newValue + checked: Settings.data.network.bluetoothEnabled + onToggled: checked => { + Settings.data.network.bluetoothEnabled = checked } } } diff --git a/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml b/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml index ebc9e26..46afb28 100644 --- a/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml +++ b/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml @@ -65,9 +65,9 @@ ColumnLayout { NToggle { label: "Show Cursor" description: "Record mouse cursor in the video" - value: Settings.data.screenRecorder.showCursor - onToggled: function (newValue) { - Settings.data.screenRecorder.showCursor = newValue + checked: Settings.data.screenRecorder.showCursor + onToggled: checked => { + Settings.data.screenRecorder.showCursor = checked } } } diff --git a/Modules/SettingsPanel/Tabs/TimeWeatherTab.qml b/Modules/SettingsPanel/Tabs/TimeWeatherTab.qml index 8cd1dec..dbafbf4 100644 --- a/Modules/SettingsPanel/Tabs/TimeWeatherTab.qml +++ b/Modules/SettingsPanel/Tabs/TimeWeatherTab.qml @@ -81,18 +81,18 @@ ColumnLayout { NToggle { label: "Use 12-Hour Clock" description: "Display time in 12-hour format (AM/PM) instead of 24-hour" - value: Settings.data.location.use12HourClock - onToggled: function (newValue) { - Settings.data.location.use12HourClock = newValue + checked: Settings.data.location.use12HourClock + onToggled: checked => { + Settings.data.location.use12HourClock = checked } } NToggle { label: "Reverse Day/Month" description: "Display date as DD/MM instead of MM/DD" - value: Settings.data.location.reverseDayMonth - onToggled: function (newValue) { - Settings.data.location.reverseDayMonth = newValue + checked: Settings.data.location.reverseDayMonth + onToggled: checked => { + Settings.data.location.reverseDayMonth = checked } } } @@ -119,9 +119,9 @@ ColumnLayout { NToggle { label: "Use Fahrenheit" description: "Display temperature in Fahrenheit instead of Celsius" - value: Settings.data.location.useFahrenheit - onToggled: function (newValue) { - Settings.data.location.useFahrenheit = newValue + checked: Settings.data.location.useFahrenheit + onToggled: checked => { + Settings.data.location.useFahrenheit = checked } } } diff --git a/Modules/SettingsPanel/Tabs/WallpaperTab.qml b/Modules/SettingsPanel/Tabs/WallpaperTab.qml index 102689a..777e6d0 100644 --- a/Modules/SettingsPanel/Tabs/WallpaperTab.qml +++ b/Modules/SettingsPanel/Tabs/WallpaperTab.qml @@ -87,9 +87,9 @@ ColumnLayout { NToggle { label: "Random Wallpaper" description: "Automatically select random wallpapers from the folder" - value: Settings.data.wallpaper.isRandom - onToggled: function (newValue) { - Settings.data.wallpaper.isRandom = newValue + checked: Settings.data.wallpaper.isRandom + onToggled: checked => { + Settings.data.wallpaper.isRandom = checked } } @@ -157,9 +157,9 @@ ColumnLayout { NToggle { label: "Use SWWW" description: "Use SWWW daemon for advanced wallpaper management" - value: Settings.data.wallpaper.swww.enabled - onToggled: function (newValue) { - Settings.data.wallpaper.swww.enabled = newValue + checked: Settings.data.wallpaper.swww.enabled + onToggled: checked => { + Settings.data.wallpaper.swww.enabled = checked } } diff --git a/Widgets/NToggle.qml b/Widgets/NToggle.qml index 4dea6fd..4ec9b50 100644 --- a/Widgets/NToggle.qml +++ b/Widgets/NToggle.qml @@ -9,11 +9,11 @@ RowLayout { property string label: "" property string description: "" - property bool value: false + property bool checked: false property bool hovering: false property int baseSize: Style.baseWidgetSize - signal toggled(bool balue) + signal toggled(bool checked) signal entered signal exited @@ -45,19 +45,19 @@ RowLayout { implicitWidth: root.baseSize * 1.625 * scaling implicitHeight: root.baseSize * scaling radius: height * 0.5 - color: value ? Colors.mPrimary : Colors.mSurface - border.color: value ? Colors.mPrimary : Colors.mOutline + color: root.checked ? Colors.mPrimary : Colors.mSurface + border.color: root.checked ? Colors.mPrimary : Colors.mOutline border.width: Math.max(1, Style.borderMedium * scaling) Rectangle { implicitWidth: (root.baseSize - 5) * scaling implicitHeight: (root.baseSize - 5) * scaling radius: height * 0.5 - color: value ? Colors.mOnPrimary : Colors.mPrimary - border.color: value ? Colors.mSurface : Colors.mSurface + color: root.checked ? Colors.mOnPrimary : Colors.mPrimary + border.color: root.checked ? Colors.mSurface : Colors.mSurface border.width: Math.max(1, Style.borderMedium * scaling) y: 2 * scaling - x: value ? switcher.width - width - 2 * scaling : 2 * scaling + x: root.checked ? switcher.width - width - 2 * scaling : 2 * scaling Behavior on x { NumberAnimation { @@ -80,8 +80,8 @@ RowLayout { root.exited() } onClicked: { - value = !value - root.toggled(value) + root.checked = !root.checked + root.toggled(root.checked) } } } From 34f6affe4fe169c1a4d1afcda23edd24dc92968f Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 23:26:41 -0400 Subject: [PATCH 345/394] NToggle should not manage its own state. Let the bindings do the job --- Widgets/NToggle.qml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Widgets/NToggle.qml b/Widgets/NToggle.qml index 4ec9b50..eddb228 100644 --- a/Widgets/NToggle.qml +++ b/Widgets/NToggle.qml @@ -80,8 +80,7 @@ RowLayout { root.exited() } onClicked: { - root.checked = !root.checked - root.toggled(root.checked) + root.toggled(!root.checked) } } } From 22f7dab34d6dc0b8d85f58e8185167c51aa1379e Mon Sep 17 00:00:00 2001 From: quadbyte Date: Fri, 15 Aug 2025 23:40:35 -0400 Subject: [PATCH 346/394] formatting --- Modules/Bar/WiFiMenu.qml | 14 +++---- Modules/Demo/DemoPanel.qml | 4 +- Modules/SettingsPanel/Tabs/AudioTab.qml | 8 ++-- Modules/SettingsPanel/Tabs/BarTab.qml | 12 +++--- Modules/SettingsPanel/Tabs/ColorSchemeTab.qml | 10 ++--- Modules/SettingsPanel/Tabs/DisplayTab.qml | 41 ++++++++++--------- Modules/SettingsPanel/Tabs/GeneralTab.qml | 12 +++--- Modules/SettingsPanel/Tabs/NetworkTab.qml | 8 ++-- .../SettingsPanel/Tabs/ScreenRecorderTab.qml | 4 +- Modules/SettingsPanel/Tabs/TimeWeatherTab.qml | 12 +++--- Modules/SettingsPanel/Tabs/WallpaperTab.qml | 8 ++-- 11 files changed, 67 insertions(+), 66 deletions(-) diff --git a/Modules/Bar/WiFiMenu.qml b/Modules/Bar/WiFiMenu.qml index 446fe12..73cc8fa 100644 --- a/Modules/Bar/WiFiMenu.qml +++ b/Modules/Bar/WiFiMenu.qml @@ -149,14 +149,14 @@ NLoader { baseSize: Style.baseWidgetSize * 0.75 checked: Settings.data.network.wifiEnabled onToggled: checked => { - Settings.data.network.wifiEnabled = checked - NetworkService.setWifiEnabled(checked) + Settings.data.network.wifiEnabled = checked + NetworkService.setWifiEnabled(checked) - // If enabling WiFi while menu is open, refresh after a delay - if (checked) { - wifiEnableRefreshTimer.start() - } - } + // If enabling WiFi while menu is open, refresh after a delay + if (checked) { + wifiEnableRefreshTimer.start() + } + } } NIconButton { diff --git a/Modules/Demo/DemoPanel.qml b/Modules/Demo/DemoPanel.qml index 9ef9da3..1f0ea02 100644 --- a/Modules/Demo/DemoPanel.qml +++ b/Modules/Demo/DemoPanel.qml @@ -202,8 +202,8 @@ NLoader { label: "Label" description: "Description" onToggled: checked => { - console.log("[DemoPanel] NToggle:", checked) - } + console.log("[DemoPanel] NToggle:", checked) + } } NDivider { diff --git a/Modules/SettingsPanel/Tabs/AudioTab.qml b/Modules/SettingsPanel/Tabs/AudioTab.qml index d8118de..61ef625 100644 --- a/Modules/SettingsPanel/Tabs/AudioTab.qml +++ b/Modules/SettingsPanel/Tabs/AudioTab.qml @@ -125,10 +125,10 @@ ColumnLayout { description: "Mute or unmute the default audio output" checked: AudioService.muted onToggled: checked => { - if (AudioService.sink && AudioService.sink.audio) { - AudioService.sink.audio.muted = checked - } - } + if (AudioService.sink && AudioService.sink.audio) { + AudioService.sink.audio.muted = checked + } + } } } } diff --git a/Modules/SettingsPanel/Tabs/BarTab.qml b/Modules/SettingsPanel/Tabs/BarTab.qml index f26a103..66ce55e 100644 --- a/Modules/SettingsPanel/Tabs/BarTab.qml +++ b/Modules/SettingsPanel/Tabs/BarTab.qml @@ -45,8 +45,8 @@ ColumnLayout { description: "Display the title of the currently focused window below the bar" checked: Settings.data.bar.showActiveWindow onToggled: checked => { - Settings.data.bar.showActiveWindow = checked - } + Settings.data.bar.showActiveWindow = checked + } } NToggle { @@ -54,8 +54,8 @@ ColumnLayout { description: "Display system information (CPU, RAM, Temperature)" checked: Settings.data.bar.showSystemInfo onToggled: checked => { - Settings.data.bar.showSystemInfo = checked - } + Settings.data.bar.showSystemInfo = checked + } } NToggle { @@ -63,8 +63,8 @@ ColumnLayout { description: "Display media controls and information" checked: Settings.data.bar.showMedia onToggled: checked => { - Settings.data.bar.showMedia = checked - } + Settings.data.bar.showMedia = checked + } } } } diff --git a/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml b/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml index 73a97eb..0c467cf 100644 --- a/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml +++ b/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml @@ -131,11 +131,11 @@ ColumnLayout { description: "Automatically generate colors based on your active wallpaper using Matugen" checked: Settings.data.colorSchemes.useWallpaperColors onToggled: checked => { - Settings.data.colorSchemes.useWallpaperColors = checked - if (Settings.data.colorSchemes.useWallpaperColors) { - ColorSchemesService.changedWallpaper() - } - } + Settings.data.colorSchemes.useWallpaperColors = checked + if (Settings.data.colorSchemes.useWallpaperColors) { + ColorSchemesService.changedWallpaper() + } + } } NDivider { diff --git a/Modules/SettingsPanel/Tabs/DisplayTab.qml b/Modules/SettingsPanel/Tabs/DisplayTab.qml index cf53a74..7dad934 100644 --- a/Modules/SettingsPanel/Tabs/DisplayTab.qml +++ b/Modules/SettingsPanel/Tabs/DisplayTab.qml @@ -91,12 +91,12 @@ Item { description: "Enable the top bar on this monitor" checked: (Settings.data.bar.monitors || []).indexOf(modelData.name) !== -1 onToggled: checked => { - if (checked) { - Settings.data.bar.monitors = addMonitor(Settings.data.bar.monitors, modelData.name) - } else { - Settings.data.bar.monitors = removeMonitor(Settings.data.bar.monitors, modelData.name) - } - } + if (checked) { + Settings.data.bar.monitors = addMonitor(Settings.data.bar.monitors, modelData.name) + } else { + Settings.data.bar.monitors = removeMonitor(Settings.data.bar.monitors, modelData.name) + } + } } NToggle { @@ -104,14 +104,14 @@ Item { description: "Enable notifications on this monitor" checked: (Settings.data.notifications.monitors || []).indexOf(modelData.name) !== -1 onToggled: checked => { - if (checked) { - Settings.data.notifications.monitors = addMonitor(Settings.data.notifications.monitors, - modelData.name) - } else { - Settings.data.notifications.monitors = removeMonitor(Settings.data.notifications.monitors, - modelData.name) - } - } + if (checked) { + Settings.data.notifications.monitors = addMonitor( + Settings.data.notifications.monitors, modelData.name) + } else { + Settings.data.notifications.monitors = removeMonitor( + Settings.data.notifications.monitors, modelData.name) + } + } } NToggle { @@ -119,12 +119,13 @@ Item { description: "Enable the dock on this monitor" checked: (Settings.data.dock.monitors || []).indexOf(modelData.name) !== -1 onToggled: checked => { - if (checked) { - Settings.data.dock.monitors = addMonitor(Settings.data.dock.monitors, modelData.name) - } else { - Settings.data.dock.monitors = removeMonitor(Settings.data.dock.monitors, modelData.name) - } - } + if (checked) { + Settings.data.dock.monitors = addMonitor(Settings.data.dock.monitors, modelData.name) + } else { + Settings.data.dock.monitors = removeMonitor(Settings.data.dock.monitors, + modelData.name) + } + } } } } diff --git a/Modules/SettingsPanel/Tabs/GeneralTab.qml b/Modules/SettingsPanel/Tabs/GeneralTab.qml index f42766c..ba683cb 100644 --- a/Modules/SettingsPanel/Tabs/GeneralTab.qml +++ b/Modules/SettingsPanel/Tabs/GeneralTab.qml @@ -97,8 +97,8 @@ ColumnLayout { description: "Display rounded corners on the edge of the screen" checked: Settings.data.general.showScreenCorners onToggled: checked => { - Settings.data.general.showScreenCorners = checked - } + Settings.data.general.showScreenCorners = checked + } } NToggle { @@ -106,8 +106,8 @@ ColumnLayout { description: "Dim the desktop when panels or menus are open" checked: Settings.data.general.dimDesktop onToggled: checked => { - Settings.data.general.dimDesktop = checked - } + Settings.data.general.dimDesktop = checked + } } NToggle { @@ -115,8 +115,8 @@ ColumnLayout { description: "Automatically hide the dock when not in use" checked: Settings.data.dock.autoHide onToggled: checked => { - Settings.data.dock.autoHide = checked - } + Settings.data.dock.autoHide = checked + } } } } diff --git a/Modules/SettingsPanel/Tabs/NetworkTab.qml b/Modules/SettingsPanel/Tabs/NetworkTab.qml index ab1e0b9..f9f2ebd 100644 --- a/Modules/SettingsPanel/Tabs/NetworkTab.qml +++ b/Modules/SettingsPanel/Tabs/NetworkTab.qml @@ -47,8 +47,8 @@ ColumnLayout { description: "Enable WiFi connectivity" checked: Settings.data.network.wifiEnabled onToggled: checked => { - Settings.data.network.wifiEnabled = checked - } + Settings.data.network.wifiEnabled = checked + } } NToggle { @@ -56,8 +56,8 @@ ColumnLayout { description: "Enable Bluetooth connectivity" checked: Settings.data.network.bluetoothEnabled onToggled: checked => { - Settings.data.network.bluetoothEnabled = checked - } + Settings.data.network.bluetoothEnabled = checked + } } } } diff --git a/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml b/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml index 46afb28..d6f2d49 100644 --- a/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml +++ b/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml @@ -67,8 +67,8 @@ ColumnLayout { description: "Record mouse cursor in the video" checked: Settings.data.screenRecorder.showCursor onToggled: checked => { - Settings.data.screenRecorder.showCursor = checked - } + Settings.data.screenRecorder.showCursor = checked + } } } } diff --git a/Modules/SettingsPanel/Tabs/TimeWeatherTab.qml b/Modules/SettingsPanel/Tabs/TimeWeatherTab.qml index dbafbf4..7f0b708 100644 --- a/Modules/SettingsPanel/Tabs/TimeWeatherTab.qml +++ b/Modules/SettingsPanel/Tabs/TimeWeatherTab.qml @@ -83,8 +83,8 @@ ColumnLayout { description: "Display time in 12-hour format (AM/PM) instead of 24-hour" checked: Settings.data.location.use12HourClock onToggled: checked => { - Settings.data.location.use12HourClock = checked - } + Settings.data.location.use12HourClock = checked + } } NToggle { @@ -92,8 +92,8 @@ ColumnLayout { description: "Display date as DD/MM instead of MM/DD" checked: Settings.data.location.reverseDayMonth onToggled: checked => { - Settings.data.location.reverseDayMonth = checked - } + Settings.data.location.reverseDayMonth = checked + } } } @@ -121,8 +121,8 @@ ColumnLayout { description: "Display temperature in Fahrenheit instead of Celsius" checked: Settings.data.location.useFahrenheit onToggled: checked => { - Settings.data.location.useFahrenheit = checked - } + Settings.data.location.useFahrenheit = checked + } } } } diff --git a/Modules/SettingsPanel/Tabs/WallpaperTab.qml b/Modules/SettingsPanel/Tabs/WallpaperTab.qml index 777e6d0..5672c3f 100644 --- a/Modules/SettingsPanel/Tabs/WallpaperTab.qml +++ b/Modules/SettingsPanel/Tabs/WallpaperTab.qml @@ -89,8 +89,8 @@ ColumnLayout { description: "Automatically select random wallpapers from the folder" checked: Settings.data.wallpaper.isRandom onToggled: checked => { - Settings.data.wallpaper.isRandom = checked - } + Settings.data.wallpaper.isRandom = checked + } } // Interval @@ -159,8 +159,8 @@ ColumnLayout { description: "Use SWWW daemon for advanced wallpaper management" checked: Settings.data.wallpaper.swww.enabled onToggled: checked => { - Settings.data.wallpaper.swww.enabled = checked - } + Settings.data.wallpaper.swww.enabled = checked + } } // SWWW Settings (only visible when useSWWW is enabled) From 9832d3d9bdad6aa34eba2fe2eb588b2d7d31e067 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sat, 16 Aug 2025 13:16:05 +0200 Subject: [PATCH 347/394] Fix dock settings, add hover, add full screen width --- Modules/Dock/Dock.qml | 112 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 96 insertions(+), 16 deletions(-) diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml index a4e1826..80f6ff6 100644 --- a/Modules/Dock/Dock.qml +++ b/Modules/Dock/Dock.qml @@ -22,9 +22,9 @@ NLoader { readonly property real scaling: ScalingService.scale(screen) screen: modelData - // Auto-hide properties + // Auto-hide properties - make reactive to settings changes property bool autoHide: Settings.data.dock.autoHide - property bool hidden: autoHide // Start hidden only if auto-hide is enabled + property bool hidden: autoHide property int hideDelay: 500 property int showDelay: 100 property int hideAnimationDuration: Style.animationFast @@ -54,6 +54,19 @@ NLoader { color: "transparent" implicitHeight: iconSize * 1.4 * scaling + // Watch for autoHide setting changes + onAutoHideChanged: { + if (!autoHide) { + // If auto-hide is disabled, show the dock + hidden = false + hideTimer.stop() + showTimer.stop() + } else { + // If auto-hide is enabled, start hidden + hidden = true + } + } + // Timer for auto-hide delay Timer { id: hideTimer @@ -80,24 +93,30 @@ NLoader { } } - // Mouse area at screen bottom to detect entry and keep dock visible MouseArea { id: screenEdgeMouseArea - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - height: 10 * scaling + x: 0 + y: modelData.geometry.height - (fullHeight + 10 * scaling) + width: screen.width + height: fullHeight + 10 * scaling hoverEnabled: true propagateComposedEvents: true - onEntered: if (autoHide && hidden) - showTimer.start() - onExited: if (autoHide && !hidden && !dockHovered && !anyAppHovered) - hideTimer.start() + onEntered: { + if (autoHide && hidden) { + showTimer.start() + } + } + onExited: { + if (autoHide && !hidden && !dockHovered && !anyAppHovered && !contextMenuVisible) { + hideTimer.start() + } + } } margins.bottom: hidden ? -(fullHeight - peekHeight) : 0 + // Global click handler to close context menu MouseArea { anchors.fill: parent enabled: contextMenuVisible @@ -134,8 +153,10 @@ NLoader { } onExited: { dockHovered = false - if (autoHide && !anyAppHovered && !contextMenuVisible) + // Only start hide timer if we're not hovering over any app or context menu + if (autoHide && !anyAppHovered && !contextMenuVisible) { hideTimer.start() + } } } @@ -178,12 +199,29 @@ NLoader { width: iconSize * scaling height: iconSize * scaling color: "transparent" + radius: Style.radiusMedium * scaling property bool isActive: ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData property bool hovered: appMouseArea.containsMouse property string appId: modelData ? modelData.appId : "" property string appTitle: modelData ? modelData.title : "" + // Hover background + Rectangle { + id: hoverBackground + anchors.fill: parent + color: appButton.hovered ? Colors.mSurfaceVariant : "transparent" + radius: parent.radius + opacity: appButton.hovered ? 0.8 : 0 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutQuad + } + } + } + // The icon Image { id: appIcon @@ -196,6 +234,15 @@ NLoader { mipmap: false antialiasing: false fillMode: Image.PreserveAspectFit + + scale: appButton.hovered ? 1.1 : 1.0 + + Behavior on scale { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutBack + } + } } // Fall back if no icon @@ -206,6 +253,15 @@ NLoader { font.family: "Material Symbols Rounded" font.pointSize: iconSize * 0.7 * scaling color: appButton.isActive ? Colors.mPrimary : Colors.mOnSurfaceVariant + + scale: appButton.hovered ? 1.1 : 1.0 + + Behavior on scale { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutBack + } + } } MouseArea { @@ -231,8 +287,10 @@ NLoader { onExited: { anyAppHovered = false appTooltip.hide() - if (autoHide && !dockHovered && !contextMenuVisible) + // Only start hide timer if we're not hovering over the dock or context menu + if (autoHide && !dockHovered && !contextMenuVisible) { hideTimer.start() + } } onClicked: function (mouse) { @@ -285,7 +343,17 @@ NLoader { contextMenuVisible = false contextMenuTarget = null contextMenuToplevel = null - hidden = true // Hide dock when context menu closes + if (autoHide) { + // Stop any pending show/hide timers to prevent flickering + showTimer.stop() + hideTimer.stop() + // Add a small delay before hiding to prevent immediate show/hide cycle + Qt.callLater(function() { + if (autoHide && !dockHovered && !anyAppHovered) { + hidden = true + } + }) + } } } @@ -317,7 +385,7 @@ NLoader { anchors.centerIn: parent text: "Close" font.pointSize: Style.fontSizeMedium * scaling - color: Colors.mOnSurface + color: closeMouseArea.containsMouse ? Colors.mOnTertiary : Colors.mOnSurface } MouseArea { @@ -330,7 +398,19 @@ NLoader { if (contextMenuToplevel?.close) contextMenuToplevel.close() contextMenuVisible = false - hidden = true + contextMenuTarget = null + contextMenuToplevel = null + if (autoHide) { + // Stop any pending show/hide timers to prevent flickering + showTimer.stop() + hideTimer.stop() + // Add a small delay before hiding to prevent immediate show/hide cycle + Qt.callLater(function() { + if (autoHide && !dockHovered && !anyAppHovered) { + hidden = true + } + }) + } } } From 51e758a8884015f27a6a47d92750b844a495c1fe Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sat, 16 Aug 2025 14:00:03 +0200 Subject: [PATCH 348/394] Add BrightnessTab --- Commons/Settings.qml | 1 + Modules/Bar/Brightness.qml | 3 +- Modules/SettingsPanel/SettingsPanel.qml | 12 +- Modules/SettingsPanel/Tabs/AudioTab.qml | 6 +- Modules/SettingsPanel/Tabs/BrightnessTab.qml | 250 ++++++++++++++++++ .../SettingsPanel/Tabs/ScreenRecorderTab.qml | 14 +- 6 files changed, 274 insertions(+), 12 deletions(-) create mode 100644 Modules/SettingsPanel/Tabs/BrightnessTab.qml diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 32430e4..fca47ad 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -75,6 +75,7 @@ Singleton { property bool showActiveWindow: true property bool showSystemInfo: false property bool showMedia: false + property bool hideBrightness: false property list monitors: [] } diff --git a/Modules/Bar/Brightness.qml b/Modules/Bar/Brightness.qml index d336c14..e1c1319 100644 --- a/Modules/Bar/Brightness.qml +++ b/Modules/Bar/Brightness.qml @@ -10,6 +10,7 @@ Item { width: pill.width height: pill.height + visible: !Settings.data.bar.hideBrightness // Used to avoid opening the pill on Quickshell startup property bool firstBrightnessReceived: false @@ -67,7 +68,7 @@ Item { } } onClicked: { - settingsPanel.requestedTab = SettingsPanel.Tab.Display + settingsPanel.requestedTab = SettingsPanel.Tab.Brightness settingsPanel.isLoaded = true } } diff --git a/Modules/SettingsPanel/SettingsPanel.qml b/Modules/SettingsPanel/SettingsPanel.qml index aa600cb..2217ff1 100644 --- a/Modules/SettingsPanel/SettingsPanel.qml +++ b/Modules/SettingsPanel/SettingsPanel.qml @@ -16,6 +16,7 @@ NLoader { About, AudioService, Bar, + Brightness, ColorScheme, Display, General, @@ -76,6 +77,10 @@ NLoader { id: audioTab Tabs.AudioTab {} } + Component { + id: brightnessTab + Tabs.BrightnessTab {} + } Component { id: displayTab Tabs.DisplayTab {} @@ -121,9 +126,14 @@ NLoader { "source": barTab }, { "id": SettingsPanel.Tab.AudioService, - "label": "AudioService", + "label": "Audio", "icon": "volume_up", "source": audioTab + }, { + "id": SettingsPanel.Tab.Brightness, + "label": "Brightness", + "icon": "brightness_6", + "source": brightnessTab }, { "id": SettingsPanel.Tab.Display, "label": "Display", diff --git a/Modules/SettingsPanel/Tabs/AudioTab.qml b/Modules/SettingsPanel/Tabs/AudioTab.qml index 61ef625..0efca92 100644 --- a/Modules/SettingsPanel/Tabs/AudioTab.qml +++ b/Modules/SettingsPanel/Tabs/AudioTab.qml @@ -45,7 +45,7 @@ ColumnLayout { Layout.fillWidth: true NText { - text: "AudioService" + text: "Audio" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold color: Colors.mOnSurface @@ -145,7 +145,7 @@ ColumnLayout { Layout.fillWidth: true NText { - text: "AudioService Devices" + text: "Audio Devices" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold color: Colors.mOnSurface @@ -243,7 +243,7 @@ ColumnLayout { Layout.fillWidth: true NText { - text: "AudioService Visualizer" + text: "Audio Visualizer" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold color: Colors.mOnSurface diff --git a/Modules/SettingsPanel/Tabs/BrightnessTab.qml b/Modules/SettingsPanel/Tabs/BrightnessTab.qml new file mode 100644 index 0000000..dcc8871 --- /dev/null +++ b/Modules/SettingsPanel/Tabs/BrightnessTab.qml @@ -0,0 +1,250 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import qs.Commons +import qs.Services +import qs.Widgets + +Item { + property real scaling: 1 + readonly property string tabIcon: "brightness_6" + readonly property string tabLabel: "Brightness" + Layout.fillWidth: true + Layout.fillHeight: true + + ScrollView { + anchors.fill: parent + clip: true + ScrollBar.vertical.policy: ScrollBar.AsNeeded + ScrollBar.horizontal.policy: ScrollBar.AsNeeded + contentWidth: parent.width + + ColumnLayout { + width: parent.width + ColumnLayout { + spacing: Style.marginLarge * scaling + Layout.margins: Style.marginLarge * scaling + Layout.fillWidth: true + + NText { + text: "Brightness Settings" + font.pointSize: Style.fontSizeXL * scaling + font.weight: Style.fontWeightBold + color: Colors.mOnSurface + } + + NText { + text: "Configure brightness controls and monitor settings." + font.pointSize: Style.fontSize * scaling + color: Colors.mOnSurfaceVariant + } + + // Bar Visibility Section + ColumnLayout { + spacing: Style.marginSmall * scaling + Layout.fillWidth: true + Layout.topMargin: Style.marginLarge * scaling + + NText { + text: "Bar Integration" + font.pointSize: Style.fontSizeLarge * scaling + font.weight: Style.fontWeightBold + color: Colors.mOnSurface + } + + NToggle { + label: "Show Brightness Icon" + description: "Display the brightness control icon in the top bar" + checked: !Settings.data.bar.hideBrightness + onToggled: checked => { + Settings.data.bar.hideBrightness = !checked + } + } + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginLarge * scaling + Layout.bottomMargin: Style.marginLarge * scaling + } + + // Brightness Step Section + ColumnLayout { + spacing: Style.marginSmall * scaling + Layout.fillWidth: true + + NText { + text: "Brightness Step Size" + font.pointSize: Style.fontSizeLarge * scaling + font.weight: Style.fontWeightBold + color: Colors.mOnSurface + } + + NText { + text: "Adjust the step size for brightness changes (scroll wheel, keyboard shortcuts)" + font.pointSize: Style.fontSizeSmall * scaling + color: Colors.mOnSurfaceVariant + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginMedium * scaling + + NSlider { + Layout.fillWidth: true + from: 1 + to: 50 + value: Settings.data.brightness.brightnessStep + stepSize: 1 + onPressedChanged: { + if (!pressed) { + Settings.data.brightness.brightnessStep = value + } + } + } + + NText { + text: Settings.data.brightness.brightnessStep + "%" + Layout.alignment: Qt.AlignVCenter + color: Colors.mOnSurface + font.pointSize: Style.fontSizeMedium * scaling + font.weight: Style.fontWeightBold + } + } + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginLarge * scaling + Layout.bottomMargin: Style.marginLarge * scaling + } + + // Monitor Overview Section + ColumnLayout { + spacing: Style.marginSmall * scaling + Layout.fillWidth: true + + NText { + text: "Monitor Brightness Overview" + font.pointSize: Style.fontSizeLarge * scaling + font.weight: Style.fontWeightBold + color: Colors.mOnSurface + } + + NText { + text: "Current brightness levels for all detected monitors" + font.pointSize: Style.fontSizeSmall * scaling + color: Colors.mOnSurfaceVariant + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + // Single monitor display using the same data source as the bar icon + Rectangle { + Layout.fillWidth: true + radius: Style.radiusMedium * scaling + color: Colors.mSurface + border.color: Colors.mOutline + border.width: Math.max(1, Style.borderThin * scaling) + implicitHeight: contentCol.implicitHeight + Style.marginXL * 2 * scaling + + ColumnLayout { + id: contentCol + anchors.fill: parent + anchors.margins: Style.marginLarge * scaling + spacing: Style.marginMedium * scaling + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginMedium * scaling + + NText { + text: "Primary Monitor" + font.pointSize: Style.fontSizeLarge * scaling + font.weight: Style.fontWeightBold + color: Colors.mSecondary + } + + Item { Layout.fillWidth: true } + + NText { + text: BrightnessService.currentMethod === "ddcutil" ? "External (DDC)" : "Internal" + font.pointSize: Style.fontSizeSmall * scaling + color: Colors.mOnSurfaceVariant + Layout.alignment: Qt.AlignRight + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginMedium * scaling + + NText { + text: "Brightness:" + font.pointSize: Style.fontSizeMedium * scaling + color: Colors.mOnSurface + } + + NSlider { + Layout.fillWidth: true + from: 0 + to: 100 + value: BrightnessService.brightness + stepSize: 1 + enabled: BrightnessService.available + onPressedChanged: { + if (!pressed && BrightnessService.available) { + BrightnessService.setBrightness(value) + } + } + } + + NText { + text: BrightnessService.available ? Math.round(BrightnessService.brightness) + "%" : "N/A" + font.pointSize: Style.fontSizeMedium * scaling + font.weight: Style.fontWeightBold + color: BrightnessService.available ? Colors.mPrimary : Colors.mOnSurfaceVariant + Layout.alignment: Qt.AlignRight + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginMedium * scaling + + NText { + text: "Method:" + font.pointSize: Style.fontSizeSmall * scaling + color: Colors.mOnSurfaceVariant + } + + NText { + text: BrightnessService.currentMethod || "Unknown" + font.pointSize: Style.fontSizeSmall * scaling + color: Colors.mOnSurface + Layout.alignment: Qt.AlignLeft + } + + Item { Layout.fillWidth: true } + + NText { + text: BrightnessService.available ? "Available" : "Unavailable" + font.pointSize: Style.fontSizeSmall * scaling + color: BrightnessService.available ? Colors.mPrimary : Colors.mError + Layout.alignment: Qt.AlignRight + } + } + } + } + } + + Item { + Layout.fillHeight: true + } + } + } + } +} \ No newline at end of file diff --git a/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml b/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml index d6f2d49..534d451 100644 --- a/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml +++ b/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml @@ -207,23 +207,23 @@ ColumnLayout { Layout.bottomMargin: Style.marginLarge * scaling } - // AudioService Settings + // Audio Settings ColumnLayout { spacing: Style.marginLarge * scaling Layout.fillWidth: true NText { - text: "AudioService Settings" + text: "Audio Settings" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold color: Colors.mOnSurface Layout.bottomMargin: Style.marginSmall * scaling } - // AudioService Source + // Audio Source NComboBox { - label: "AudioService Source" - description: "AudioService source to capture during recording" + label: "Audio Source" + description: "Audio source to capture during recording" model: ListModel { ListElement { key: "default_output" @@ -244,9 +244,9 @@ ColumnLayout { } } - // AudioService Codec + // Audio Codec NComboBox { - label: "AudioService Codec" + label: "Audio Codec" description: "Opus is recommended for best performance and smallest audio size" model: ListModel { ListElement { From 3e001f12822707cf99eea35887a88635f9be6a13 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sat, 16 Aug 2025 14:02:38 +0200 Subject: [PATCH 349/394] Remove Dock context menu for now --- Modules/Dock/Dock.qml | 143 ++---------------------------------------- 1 file changed, 6 insertions(+), 137 deletions(-) diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml index 80f6ff6..794f43b 100644 --- a/Modules/Dock/Dock.qml +++ b/Modules/Dock/Dock.qml @@ -37,11 +37,6 @@ NLoader { property bool dockHovered: false property bool anyAppHovered: false - // Context menu properties - property bool contextMenuVisible: false - property var contextMenuTarget: null - property var contextMenuToplevel: null - // Dock is only shown if explicitely toggled visible: modelData ? Settings.data.dock.monitors.includes(modelData.name) : false @@ -108,7 +103,7 @@ NLoader { } } onExited: { - if (autoHide && !hidden && !dockHovered && !anyAppHovered && !contextMenuVisible) { + if (autoHide && !hidden && !dockHovered && !anyAppHovered) { hideTimer.start() } } @@ -116,17 +111,6 @@ NLoader { margins.bottom: hidden ? -(fullHeight - peekHeight) : 0 - // Global click handler to close context menu - MouseArea { - anchors.fill: parent - enabled: contextMenuVisible - onClicked: { - contextMenuVisible = false - contextMenuTarget = null - contextMenuToplevel = null - } - } - Rectangle { id: dockContainer width: dock.width + 48 * scaling @@ -153,8 +137,8 @@ NLoader { } onExited: { dockHovered = false - // Only start hide timer if we're not hovering over any app or context menu - if (autoHide && !anyAppHovered && !contextMenuVisible) { + // Only start hide timer if we're not hovering over any app + if (autoHide && !anyAppHovered) { hideTimer.start() } } @@ -269,7 +253,7 @@ NLoader { anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + acceptedButtons: Qt.LeftButton | Qt.MiddleButton onEntered: { anyAppHovered = true @@ -287,8 +271,8 @@ NLoader { onExited: { anyAppHovered = false appTooltip.hide() - // Only start hide timer if we're not hovering over the dock or context menu - if (autoHide && !dockHovered && !contextMenuVisible) { + // Only start hide timer if we're not hovering over the dock + if (autoHide && !dockHovered) { hideTimer.start() } } @@ -300,12 +284,6 @@ NLoader { if (mouse.button === Qt.LeftButton && modelData?.activate) { modelData.activate() } - if (mouse.button === Qt.RightButton) { - appTooltip.hide() - contextMenuTarget = appButton - contextMenuToplevel = modelData - contextMenuVisible = true - } } } @@ -324,115 +302,6 @@ NLoader { } } } - - // Context Menu - PanelWindow { - id: contextMenuWindow - visible: contextMenuVisible - screen: dockWindow.screen - exclusionMode: ExclusionMode.Ignore - anchors.bottom: true - anchors.left: true - anchors.right: true - color: "transparent" - focusable: false - - MouseArea { - anchors.fill: parent - onClicked: { - contextMenuVisible = false - contextMenuTarget = null - contextMenuToplevel = null - if (autoHide) { - // Stop any pending show/hide timers to prevent flickering - showTimer.stop() - hideTimer.stop() - // Add a small delay before hiding to prevent immediate show/hide cycle - Qt.callLater(function() { - if (autoHide && !dockHovered && !anyAppHovered) { - hidden = true - } - }) - } - } - } - - Rectangle { - id: contextMenuContainer - width: Style.baseWidgetSize * 2.5 * scaling - height: Style.baseWidgetSize * scaling - radius: Style.radiusTiny * scaling - color: closeMouseArea.containsMouse ? Colors.mTertiary : Colors.mSurface - border.color: Colors.mOutline - border.width: Math.max(1, Style.borderThin * scaling) - - x: { - if (!contextMenuTarget) - return 0 - const pos = contextMenuTarget.mapToItem(null, 0, 0) - let xPos = pos.x + (contextMenuTarget.width - width) / 2 - return Math.max(0, Math.min(xPos, dockWindow.width - width)) - } - - y: { - if (!contextMenuTarget) - return 0 - const pos = contextMenuTarget.mapToItem(null, 0, 0) - return pos.y - height + 32 - } - - Text { - anchors.centerIn: parent - text: "Close" - font.pointSize: Style.fontSizeMedium * scaling - color: closeMouseArea.containsMouse ? Colors.mOnTertiary : Colors.mOnSurface - } - - MouseArea { - id: closeMouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - - onClicked: { - if (contextMenuToplevel?.close) - contextMenuToplevel.close() - contextMenuVisible = false - contextMenuTarget = null - contextMenuToplevel = null - if (autoHide) { - // Stop any pending show/hide timers to prevent flickering - showTimer.stop() - hideTimer.stop() - // Add a small delay before hiding to prevent immediate show/hide cycle - Qt.callLater(function() { - if (autoHide && !dockHovered && !anyAppHovered) { - hidden = true - } - }) - } - } - } - - // Animation - scale: contextMenuVisible ? 1 : 0.9 - opacity: contextMenuVisible ? 1 : 0 - transformOrigin: Item.Bottom - - Behavior on scale { - NumberAnimation { - duration: Style.animationFast - easing.type: Easing.OutBack - } - } - - Behavior on opacity { - NumberAnimation { - duration: Style.animationFast - } - } - } - } } } } From 56ad08816edf782002317791b90e3c02336473b3 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sat, 16 Aug 2025 14:25:54 +0200 Subject: [PATCH 350/394] More fixes for Network, NotificationHistory etc --- Modules/Bar/WiFi.qml | 3 +- Modules/Bar/WiFiMenu.qml | 16 ++------- Modules/Dock/Dock.qml | 2 +- .../Notification/NotificationHistoryPanel.qml | 35 +++++++++++++++++++ Modules/SettingsPanel/Tabs/NetworkTab.qml | 1 + Services/NetworkService.qml | 15 +++++--- 6 files changed, 52 insertions(+), 20 deletions(-) diff --git a/Modules/Bar/WiFi.qml b/Modules/Bar/WiFi.qml index cd8a620..9e2011b 100644 --- a/Modules/Bar/WiFi.qml +++ b/Modules/Bar/WiFi.qml @@ -13,6 +13,7 @@ NIconButton { readonly property bool wifiEnabled: Settings.data.network.wifiEnabled sizeMultiplier: 0.8 showBorder: false + visible: wifiEnabled icon: { let connected = false let signalStrength = 0 @@ -23,7 +24,7 @@ NIconButton { break } } - return connected ? NetworkService.signalIcon(signalStrength) : "wifi_off" + return connected ? NetworkService.signalIcon(signalStrength) : "wifi" } tooltipText: "WiFi Networks" onClicked: { diff --git a/Modules/Bar/WiFiMenu.qml b/Modules/Bar/WiFiMenu.qml index 73cc8fa..87b9aa0 100644 --- a/Modules/Bar/WiFiMenu.qml +++ b/Modules/Bar/WiFiMenu.qml @@ -87,7 +87,7 @@ NLoader { border.color: Colors.mOutlineVariant border.width: Math.max(1, Style.borderThin * scaling) width: 340 * scaling - height: 320 * scaling + height: 500 * scaling anchors.top: parent.top anchors.right: parent.right anchors.topMargin: Style.marginTiny * scaling @@ -145,19 +145,7 @@ NLoader { Layout.fillWidth: true } - NToggle { - baseSize: Style.baseWidgetSize * 0.75 - checked: Settings.data.network.wifiEnabled - onToggled: checked => { - Settings.data.network.wifiEnabled = checked - NetworkService.setWifiEnabled(checked) - // If enabling WiFi while menu is open, refresh after a delay - if (checked) { - wifiEnableRefreshTimer.start() - } - } - } NIconButton { icon: "refresh" @@ -245,7 +233,7 @@ NLoader { clip: true delegate: Item { - width: parent.width + width: parent ? parent.width : 0 height: modelData.ssid === passwordPromptSsid && showPasswordPrompt ? 108 * scaling : Style.baseWidgetSize * 1.5 * scaling diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml index 794f43b..f359dba 100644 --- a/Modules/Dock/Dock.qml +++ b/Modules/Dock/Dock.qml @@ -91,7 +91,7 @@ NLoader { MouseArea { id: screenEdgeMouseArea x: 0 - y: modelData.geometry.height - (fullHeight + 10 * scaling) + y: modelData && modelData.geometry ? modelData.geometry.height - (fullHeight + 10 * scaling) : 0 width: screen.width height: fullHeight + 10 * scaling hoverEnabled: true diff --git a/Modules/Notification/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml index f508b82..8587e9e 100644 --- a/Modules/Notification/NotificationHistoryPanel.qml +++ b/Modules/Notification/NotificationHistoryPanel.qml @@ -149,6 +149,40 @@ NLoader { NDivider {} + // Empty state when no notifications + Item { + Layout.fillWidth: true + Layout.fillHeight: true + visible: NotificationService.historyModel.count === 0 + + ColumnLayout { + anchors.centerIn: parent + spacing: Style.marginMedium * scaling + + NText { + text: "notifications_off" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXXL * scaling + color: Colors.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "No notifications" + font.pointSize: Style.fontSizeLarge * scaling + color: Colors.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "Notifications will appear here when you receive them" + font.pointSize: Style.fontSizeNormal * scaling + color: Colors.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + } + } + ListView { id: notificationList Layout.fillWidth: true @@ -157,6 +191,7 @@ NLoader { spacing: Style.marginMedium * scaling clip: true boundsBehavior: Flickable.StopAtBounds + visible: NotificationService.historyModel.count > 0 delegate: Rectangle { width: notificationList ? (notificationList.width - 20) : 380 * scaling diff --git a/Modules/SettingsPanel/Tabs/NetworkTab.qml b/Modules/SettingsPanel/Tabs/NetworkTab.qml index f9f2ebd..a3cd797 100644 --- a/Modules/SettingsPanel/Tabs/NetworkTab.qml +++ b/Modules/SettingsPanel/Tabs/NetworkTab.qml @@ -48,6 +48,7 @@ ColumnLayout { checked: Settings.data.network.wifiEnabled onToggled: checked => { Settings.data.network.wifiEnabled = checked + NetworkService.setWifiEnabled(checked) } } diff --git a/Services/NetworkService.qml b/Services/NetworkService.qml index 07e5847..0799f3b 100644 --- a/Services/NetworkService.qml +++ b/Services/NetworkService.qml @@ -52,10 +52,12 @@ Singleton { isLoading = true enableWifiProcess.running = true } else { - // Store the currently connected network before disabling + // Disconnect from current network and store it for reconnection for (const ssid in networks) { if (networks[ssid].connected) { lastConnectedNetwork = ssid + // Disconnect from the current network before disabling WiFi + disconnectNetwork(ssid) break } } @@ -246,6 +248,11 @@ Singleton { command: ["nmcli", "connection", "down", connectionName] onRunningChanged: { if (!running) { + // Clear connection status when disconnecting + root.connectingSsid = "" + root.connectStatus = "" + root.connectStatusSsid = "" + root.connectError = "" root.refreshNetworks() } } @@ -450,9 +457,9 @@ Singleton { onStreamFinished: { root.connectingSsid = "" root.connectStatus = "success" - root.connectStatusSsid = root.pendingConnect ? root.pendingConnect.ssid : profileName + root.connectStatusSsid = root.pendingConnect ? root.pendingConnect.ssid : upConnectionProcess.profileName root.connectError = "" - root.lastConnectedNetwork = profileName + root.lastConnectedNetwork = upConnectionProcess.profileName root.pendingConnect = null root.refreshNetworks() } @@ -461,7 +468,7 @@ Singleton { onStreamFinished: { root.connectingSsid = "" root.connectStatus = "error" - root.connectStatusSsid = root.pendingConnect ? root.pendingConnect.ssid : profileName + root.connectStatusSsid = root.pendingConnect ? root.pendingConnect.ssid : upConnectionProcess.profileName root.connectError = text root.pendingConnect = null } From 2fd20c69f94b631d08f0efdbe6057340b8360b23 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sat, 16 Aug 2025 15:35:10 +0200 Subject: [PATCH 351/394] Add bluetooth panel (wip), add clock / clock+date setting --- Commons/Settings.qml | 1 + Commons/Time.qml | 16 +- Modules/Bar/Bar.qml | 6 +- Modules/Bar/Bluetooth.qml | 58 +++ Modules/Bar/BluetoothMenu.qml | 397 ++++++++++++++++ Modules/SettingsPanel/Tabs/NetworkTab.qml | 1 + Modules/SettingsPanel/Tabs/TimeWeatherTab.qml | 9 + Services/BluetoothService.qml | 424 ++++++++++++++++++ Services/BrightnessService.qml | 8 +- 9 files changed, 911 insertions(+), 9 deletions(-) create mode 100644 Modules/Bar/Bluetooth.qml create mode 100644 Modules/Bar/BluetoothMenu.qml create mode 100644 Services/BluetoothService.qml diff --git a/Commons/Settings.qml b/Commons/Settings.qml index fca47ad..ba38f94 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -96,6 +96,7 @@ Singleton { property bool useFahrenheit: false property bool reverseDayMonth: false property bool use12HourClock: false + property bool showDateWithClock: false } // screen recorder diff --git a/Commons/Time.qml b/Commons/Time.qml index 8f8d3bf..e027116 100644 --- a/Commons/Time.qml +++ b/Commons/Time.qml @@ -9,8 +9,20 @@ Singleton { id: root property var date: new Date() - property string time: Settings.data.location.use12HourClock ? Qt.formatDateTime(date, "h:mm AP") : Qt.formatDateTime( - date, "HH:mm") + property string time: { + let timeFormat = Settings.data.location.use12HourClock ? "h:mm AP" : "HH:mm" + let timeString = Qt.formatDateTime(date, timeFormat) + + if (Settings.data.location.showDateWithClock) { + let dayName = date.toLocaleDateString(Qt.locale(), "ddd") + dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1) + let day = date.getDate() + let month = date.toLocaleDateString(Qt.locale(), "MMM") + return timeString + " - " + dayName + ", " + day + " " + month + } + + return timeString + } readonly property string dateString: { let now = date let dayName = now.toLocaleDateString(Qt.locale(), "ddd") diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 741c842..32d438c 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -109,9 +109,9 @@ Variants { anchors.verticalCenter: parent.verticalCenter } - // Bluetooth { - // anchors.verticalCenter: parent.verticalCenter - // } + Bluetooth { + anchors.verticalCenter: parent.verticalCenter + } Battery { anchors.verticalCenter: parent.verticalCenter } diff --git a/Modules/Bar/Bluetooth.qml b/Modules/Bar/Bluetooth.qml new file mode 100644 index 0000000..b381fb0 --- /dev/null +++ b/Modules/Bar/Bluetooth.qml @@ -0,0 +1,58 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Services +import qs.Widgets + +NIconButton { + id: root + + readonly property bool bluetoothEnabled: Settings.data.network.bluetoothEnabled + sizeMultiplier: 0.8 + showBorder: false + visible: bluetoothEnabled + + Component.onCompleted: { + console.log("[Bluetooth] Component loaded, bluetoothEnabled:", bluetoothEnabled) + console.log("[Bluetooth] BluetoothService available:", typeof BluetoothService !== 'undefined') + if (typeof BluetoothService !== 'undefined') { + console.log("[Bluetooth] Connected devices:", BluetoothService.connectedDevices.length) + } + } + icon: { + // Show different icons based on connection status + if (BluetoothService.connectedDevices.length > 0) { + return "bluetooth_connected" + } else if (BluetoothService.isDiscovering) { + return "bluetooth_searching" + } else { + return "bluetooth" + } + } + tooltipText: "Bluetooth Devices" + onClicked: { + if (!bluetoothMenuLoader.active) { + bluetoothMenuLoader.isLoaded = true + } + if (bluetoothMenuLoader.item) { + if (bluetoothMenuLoader.item.visible) { + // Panel is visible, hide it with animation + if (bluetoothMenuLoader.item.hide) { + bluetoothMenuLoader.item.hide() + } else { + bluetoothMenuLoader.item.visible = false + } + } else { + // Panel is hidden, show it + bluetoothMenuLoader.item.visible = true + } + } + } + + BluetoothMenu { + id: bluetoothMenuLoader + } +} \ No newline at end of file diff --git a/Modules/Bar/BluetoothMenu.qml b/Modules/Bar/BluetoothMenu.qml new file mode 100644 index 0000000..cfe4dd7 --- /dev/null +++ b/Modules/Bar/BluetoothMenu.qml @@ -0,0 +1,397 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Services +import qs.Widgets + +// Loader for Bluetooth menu +NLoader { + id: root + + content: Component { + NPanel { + id: bluetoothPanel + + function hide() { + bluetoothMenuRect.scaleValue = 0.8 + bluetoothMenuRect.opacityValue = 0.0 + hideTimer.start() + } + + // Connect to NPanel's dismissed signal to handle external close events + Connections { + target: bluetoothPanel + ignoreUnknownSignals: true + function onDismissed() { + // Start hide animation + bluetoothMenuRect.scaleValue = 0.8 + bluetoothMenuRect.opacityValue = 0.0 + // Hide after animation completes + hideTimer.start() + } + } + + // Also handle visibility changes from external sources + onVisibleChanged: { + if (visible && Settings.data.network.bluetoothEnabled) { + // Always refresh devices when menu opens to get fresh device objects + BluetoothService.refreshDevices() + } else if (bluetoothMenuRect.opacityValue > 0) { + // Start hide animation + bluetoothMenuRect.scaleValue = 0.8 + bluetoothMenuRect.opacityValue = 0.0 + // Hide after animation completes + hideTimer.start() + } + } + + // Timer to hide panel after animation + Timer { + id: hideTimer + interval: Style.animationSlow + repeat: false + onTriggered: { + bluetoothPanel.visible = false + bluetoothPanel.dismissed() + } + } + + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + + Rectangle { + id: bluetoothMenuRect + color: Colors.mSurface + radius: Style.radiusLarge * scaling + border.color: Colors.mOutlineVariant + border.width: Math.max(1, Style.borderThin * scaling) + width: 340 * scaling + height: 500 * scaling + anchors.top: parent.top + anchors.right: parent.right + anchors.topMargin: Style.marginTiny * scaling + anchors.rightMargin: Style.marginTiny * scaling + + // Animation properties + property real scaleValue: 0.8 + property real opacityValue: 0.0 + + scale: scaleValue + opacity: opacityValue + + // Animate in when component is completed + Component.onCompleted: { + scaleValue = 1.0 + opacityValue = 1.0 + } + + // Animation behaviors + Behavior on scale { + NumberAnimation { + duration: Style.animationSlow + easing.type: Easing.OutExpo + } + } + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutQuad + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginLarge * scaling + spacing: Style.marginMedium * scaling + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginMedium * scaling + + NText { + text: "bluetooth" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXL * scaling + color: Colors.mPrimary + } + + NText { + text: "Bluetooth" + font.pointSize: Style.fontSizeLarge * scaling + font.bold: true + color: Colors.mOnSurface + Layout.fillWidth: true + } + + NIconButton { + icon: "refresh" + tooltipText: "Refresh Devices" + sizeMultiplier: 0.8 + enabled: Settings.data.network.bluetoothEnabled && !BluetoothService.isDiscovering + onClicked: { + BluetoothService.refreshDevices() + } + } + + NIconButton { + icon: "close" + tooltipText: "Close" + sizeMultiplier: 0.8 + onClicked: { + bluetoothPanel.hide() + } + } + } + + NDivider {} + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + // Loading indicator + ColumnLayout { + anchors.centerIn: parent + visible: Settings.data.network.bluetoothEnabled && BluetoothService.isDiscovering + spacing: Style.marginMedium * scaling + + NBusyIndicator { + running: BluetoothService.isDiscovering + color: Colors.mPrimary + size: Style.baseWidgetSize * scaling + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "Scanning for devices..." + font.pointSize: Style.fontSizeNormal * scaling + color: Colors.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + } + + // Bluetooth disabled message + ColumnLayout { + anchors.centerIn: parent + visible: !Settings.data.network.bluetoothEnabled + spacing: Style.marginMedium * scaling + + NText { + text: "bluetooth_disabled" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXXL * scaling + color: Colors.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "Bluetooth is disabled" + font.pointSize: Style.fontSizeLarge * scaling + color: Colors.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "Enable Bluetooth to see available devices" + font.pointSize: Style.fontSizeNormal * scaling + color: Colors.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + } + + // Device list + ListView { + id: deviceList + anchors.fill: parent + visible: Settings.data.network.bluetoothEnabled && !BluetoothService.isDiscovering + model: [] + spacing: Style.marginMedium * scaling + clip: true + + // Combine all devices into a single list for the ListView + property var allDevices: { + const devices = [] + + // Add connected devices first + for (const device of BluetoothService.connectedDevices) { + devices.push({ + device: device, + type: 'connected', + section: 'Connected Devices' + }) + } + + // Add paired devices + for (const device of BluetoothService.pairedDevices) { + devices.push({ + device: device, + type: 'paired', + section: 'Paired Devices' + }) + } + + // Add available devices + for (const device of BluetoothService.availableDevices) { + devices.push({ + device: device, + type: 'available', + section: 'Available Devices' + }) + } + + return devices + } + + // Update model when devices change + onAllDevicesChanged: { + deviceList.model = allDevices + } + + // Also watch for changes in the service arrays + Connections { + target: BluetoothService + function onConnectedDevicesChanged() { + deviceList.model = deviceList.allDevices + } + function onPairedDevicesChanged() { + deviceList.model = deviceList.allDevices + } + function onAvailableDevicesChanged() { + deviceList.model = deviceList.allDevices + } + } + + delegate: Item { + width: parent ? parent.width : 0 + height: Style.baseWidgetSize * 1.5 * scaling + + Rectangle { + anchors.fill: parent + radius: Style.radiusMedium * scaling + color: modelData.device.connected ? Colors.mPrimary : (deviceMouseArea.containsMouse ? Colors.mTertiary : "transparent") + + RowLayout { + anchors.fill: parent + anchors.margins: Style.marginSmall * scaling + spacing: Style.marginSmall * scaling + + NText { + text: BluetoothService.getDeviceIcon(modelData.device) + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXL * scaling + color: modelData.device.connected ? Colors.mSurface : (deviceMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurface) + } + + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginTiny * scaling + + NText { + text: modelData.device.name || modelData.device.deviceName || "Unknown Device" + font.pointSize: Style.fontSizeNormal * scaling + elide: Text.ElideRight + Layout.fillWidth: true + color: modelData.device.connected ? Colors.mSurface : (deviceMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurface) + } + + NText { + text: { + if (modelData.device.connected) { + return "Connected" + } else if (modelData.device.paired) { + return "Paired" + } else { + return "Available" + } + } + font.pointSize: Style.fontSizeSmall * scaling + color: modelData.device.connected ? Colors.mSurface : (deviceMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurfaceVariant) + } + + NText { + text: BluetoothService.getBatteryText(modelData.device) + font.pointSize: Style.fontSizeSmall * scaling + color: modelData.device.connected ? Colors.mSurface : (deviceMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurfaceVariant) + visible: modelData.device.batteryAvailable + } + } + + Item { + Layout.preferredWidth: Style.baseWidgetSize * 0.7 * scaling + Layout.preferredHeight: Style.baseWidgetSize * 0.7 * scaling + visible: modelData.device.pairing || modelData.device.state === 2 // Connecting state + + NBusyIndicator { + visible: modelData.device.pairing || modelData.device.state === 2 + running: modelData.device.pairing || modelData.device.state === 2 + color: Colors.mPrimary + anchors.centerIn: parent + size: Style.baseWidgetSize * 0.7 * scaling + } + } + + NText { + visible: modelData.device.connected + text: "connected" + font.pointSize: Style.fontSizeSmall * scaling + color: modelData.device.connected ? Colors.mSurface : (deviceMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurface) + } + } + + MouseArea { + id: deviceMouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + if (modelData.device.connected) { + BluetoothService.disconnectDevice(modelData.device) + } else if (modelData.device.paired) { + BluetoothService.connectDevice(modelData.device) + } else { + BluetoothService.pairDevice(modelData.device) + } + } + } + } + } + } + + // Empty state when no devices found + ColumnLayout { + anchors.centerIn: parent + visible: Settings.data.network.bluetoothEnabled && + !BluetoothService.isDiscovering && + deviceList.count === 0 + spacing: Style.marginMedium * scaling + + NText { + text: "bluetooth_disabled" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXXL * scaling + color: Colors.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "No Bluetooth devices" + font.pointSize: Style.fontSizeLarge * scaling + color: Colors.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "Click the refresh button to discover devices" + font.pointSize: Style.fontSizeNormal * scaling + color: Colors.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/Modules/SettingsPanel/Tabs/NetworkTab.qml b/Modules/SettingsPanel/Tabs/NetworkTab.qml index a3cd797..f9e66f2 100644 --- a/Modules/SettingsPanel/Tabs/NetworkTab.qml +++ b/Modules/SettingsPanel/Tabs/NetworkTab.qml @@ -58,6 +58,7 @@ ColumnLayout { checked: Settings.data.network.bluetoothEnabled onToggled: checked => { Settings.data.network.bluetoothEnabled = checked + BluetoothService.setBluetoothEnabled(checked) } } } diff --git a/Modules/SettingsPanel/Tabs/TimeWeatherTab.qml b/Modules/SettingsPanel/Tabs/TimeWeatherTab.qml index 7f0b708..157b916 100644 --- a/Modules/SettingsPanel/Tabs/TimeWeatherTab.qml +++ b/Modules/SettingsPanel/Tabs/TimeWeatherTab.qml @@ -95,6 +95,15 @@ ColumnLayout { Settings.data.location.reverseDayMonth = checked } } + + NToggle { + label: "Show Date with Clock" + description: "Display date alongside time (e.g., 18:12 - Sat, 23 Aug)" + checked: Settings.data.location.showDateWithClock + onToggled: checked => { + Settings.data.location.showDateWithClock = checked + } + } } NDivider { diff --git a/Services/BluetoothService.qml b/Services/BluetoothService.qml new file mode 100644 index 0000000..11530df --- /dev/null +++ b/Services/BluetoothService.qml @@ -0,0 +1,424 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Bluetooth +import qs.Commons + +Singleton { + id: root + + // Bluetooth state properties + property bool isEnabled: Settings.data.network.bluetoothEnabled + property bool isDiscovering: false + property var connectedDevices: [] + property var pairedDevices: [] + property var availableDevices: [] + property string lastConnectedDevice: "" + property string connectStatus: "" + property string connectStatusDevice: "" + property string connectError: "" + + // Timer for refreshing device lists + property Timer refreshTimer: Timer { + interval: 5000 // Refresh every 5 seconds when discovery is active + repeat: true + running: root.isEnabled && Bluetooth.defaultAdapter && Bluetooth.defaultAdapter.discovering + onTriggered: root.refreshDevices() + } + + Component.onCompleted: { + console.log("[Bluetooth] Service started") + + if (isEnabled && Bluetooth.defaultAdapter) { + // Ensure adapter is enabled + if (!Bluetooth.defaultAdapter.enabled) { + Bluetooth.defaultAdapter.enabled = true + } + + // Start discovery to find devices + if (!Bluetooth.defaultAdapter.discovering) { + Bluetooth.defaultAdapter.discovering = true + } + + // Refresh devices after a short delay to allow discovery to start + Qt.callLater(function() { + refreshDevices() + }) + } + } + + // Function to enable/disable Bluetooth + function setBluetoothEnabled(enabled) { + + if (enabled) { + // Store the currently connected devices before enabling + for (const device of connectedDevices) { + if (device.connected) { + lastConnectedDevice = device.name || device.deviceName + break + } + } + + // Enable Bluetooth + if (Bluetooth.defaultAdapter) { + Bluetooth.defaultAdapter.enabled = true + + // Start discovery to find devices + if (!Bluetooth.defaultAdapter.discovering) { + Bluetooth.defaultAdapter.discovering = true + } + + // Refresh devices after enabling + Qt.callLater(refreshDevices) + } else { + console.warn("[Bluetooth] No Bluetooth adapter found!") + } + } else { + // Disconnect from current devices before disabling + for (const device of connectedDevices) { + if (device.connected) { + device.disconnect() + } + } + + // Disable Bluetooth + if (Bluetooth.defaultAdapter) { + console.log("[Bluetooth] Disabling Bluetooth adapter") + Bluetooth.defaultAdapter.enabled = false + } + } + + Settings.data.network.bluetoothEnabled = enabled + isEnabled = enabled + } + + // Function to refresh device lists + function refreshDevices() { + if (!isEnabled || !Bluetooth.defaultAdapter) { + connectedDevices = [] + pairedDevices = [] + availableDevices = [] + return + } + + // Remove duplicate check since we already did it above + + const connected = [] + const paired = [] + const available = [] + + let devices = null + + // Try adapter devices first + if (Bluetooth.defaultAdapter.enabled && Bluetooth.defaultAdapter.devices) { + devices = Bluetooth.defaultAdapter.devices + } + + // Fallback to global devices list + if (!devices && Bluetooth.devices) { + devices = Bluetooth.devices + } + + if (!devices) { + connectedDevices = [] + pairedDevices = [] + availableDevices = [] + return + } + + // Use Qt model methods to iterate through the ObjectModel + let deviceFound = false + + try { + // Get the row count using the Qt model method + const rowCount = devices.rowCount() + + if (rowCount > 0) { + // Iterate through each row using the Qt model data() method + for (let i = 0; i < rowCount; i++) { + try { + // Create a model index for this row + const modelIndex = devices.index(i, 0) + if (!modelIndex.valid) continue + + // Get the device object using the Qt.UserRole (typically 256) + const device = devices.data(modelIndex, 256) // Qt.UserRole + if (!device) { + // Try alternative role values + const deviceAlt = devices.data(modelIndex, 0) // Qt.DisplayRole + if (deviceAlt) { + device = deviceAlt + } else { + continue + } + } + + deviceFound = true + + if (device.connected) { + connected.push(device) + } else if (device.paired) { + paired.push(device) + } else { + available.push(device) + } + + } catch (e) { + // Silent error handling + } + } + } + + // Alternative method: try the values property if available + if (!deviceFound && devices.values) { + try { + const values = devices.values + if (values && typeof values === 'object') { + // Try to iterate through values if it's iterable + if (values.length !== undefined) { + for (let i = 0; i < values.length; i++) { + const device = values[i] + if (device) { + deviceFound = true + if (device.connected) { + connected.push(device) + } else if (device.paired) { + paired.push(device) + } else { + available.push(device) + } + } + } + } + } + } catch (e) { + // Silent error handling + } + } + + } catch (e) { + console.warn("[Bluetooth] Error accessing device model:", e) + } + + if (!deviceFound) { + console.log("[Bluetooth] No devices found") + } + + connectedDevices = connected + pairedDevices = paired + availableDevices = available + } + + // Function to start discovery + function startDiscovery() { + if (!isEnabled || !Bluetooth.defaultAdapter) return + + isDiscovering = true + Bluetooth.defaultAdapter.discovering = true + } + + // Function to stop discovery + function stopDiscovery() { + if (!Bluetooth.defaultAdapter) return + + isDiscovering = false + Bluetooth.defaultAdapter.discovering = false + } + + // Function to connect to a device + function connectDevice(device) { + if (!device) return + + // Check if device is still valid (not stale from previous session) + if (!device.connect || typeof device.connect !== 'function') { + console.warn("[Bluetooth] Device object is stale, refreshing devices") + refreshDevices() + return + } + + connectStatus = "connecting" + connectStatusDevice = device.name || device.deviceName + connectError = "" + + try { + device.connect() + } catch (error) { + console.error("[Bluetooth] Error connecting to device:", error) + connectStatus = "error" + connectError = error.toString() + Qt.callLater(refreshDevices) + } + } + + // Function to disconnect from a device + function disconnectDevice(device) { + if (!device) return + + // Check if device is still valid (not stale from previous session) + if (!device.disconnect || typeof device.disconnect !== 'function') { + console.warn("[Bluetooth] Device object is stale, refreshing devices") + refreshDevices() + return + } + + try { + device.disconnect() + // Clear connection status + connectStatus = "" + connectStatusDevice = "" + connectError = "" + } catch (error) { + console.warn("[Bluetooth] Error disconnecting device:", error) + Qt.callLater(refreshDevices) + } + } + + // Function to pair with a device + function pairDevice(device) { + if (!device) return + + // Check if device is still valid (not stale from previous session) + if (!device.pair || typeof device.pair !== 'function') { + console.warn("[Bluetooth] Device object is stale, refreshing devices") + refreshDevices() + return + } + + try { + device.pair() + } catch (error) { + console.warn("[Bluetooth] Error pairing device:", error) + Qt.callLater(refreshDevices) + } + } + + // Function to forget a device + function forgetDevice(device) { + if (!device) return + + // Check if device is still valid (not stale from previous session) + if (!device.forget || typeof device.forget !== 'function') { + console.warn("[Bluetooth] Device object is stale, refreshing devices") + refreshDevices() + return + } + + // Store device info before forgetting (in case device object becomes invalid) + const deviceName = device.name || device.deviceName || "Unknown Device" + + try { + device.forget() + + // Clear any connection status that might be related to this device + if (connectStatusDevice === deviceName) { + connectStatus = "" + connectStatusDevice = "" + connectError = "" + } + + // Refresh devices after a delay to ensure the forget operation is complete + Qt.callLater(refreshDevices, 1000) + + } catch (error) { + console.warn("[Bluetooth] Error forgetting device:", error) + Qt.callLater(refreshDevices, 500) + } + } + + // Function to get device icon + function getDeviceIcon(device) { + if (!device) return "bluetooth" + + // Use device icon if available, otherwise fall back to device type + if (device.icon) { + return device.icon + } + + // Fallback icons based on common device types + const name = (device.name || device.deviceName || "").toLowerCase() + if (name.includes("headphone") || name.includes("earbud") || name.includes("airpods")) { + return "headphones" + } else if (name.includes("speaker")) { + return "speaker" + } else if (name.includes("keyboard")) { + return "keyboard" + } else if (name.includes("mouse")) { + return "mouse" + } else if (name.includes("phone") || name.includes("mobile")) { + return "smartphone" + } else if (name.includes("laptop") || name.includes("computer")) { + return "laptop" + } + + return "bluetooth" + } + + // Function to get device status text + function getDeviceStatus(device) { + if (!device) return "" + + if (device.connected) { + return "Connected" + } else if (device.pairing) { + return "Pairing..." + } else if (device.paired) { + return "Paired" + } else { + return "Available" + } + } + + // Function to get battery level text + function getBatteryText(device) { + if (!device || !device.batteryAvailable) return "" + + const percentage = Math.round(device.battery * 100) + return `${percentage}%` + } + + // Watch for Bluetooth adapter changes + Connections { + target: Bluetooth.defaultAdapter + ignoreUnknownSignals: true + + function onEnabledChanged() { + root.isEnabled = Bluetooth.defaultAdapter.enabled + Settings.data.network.bluetoothEnabled = root.isEnabled + if (root.isEnabled) { + Qt.callLater(refreshDevices) + } else { + connectedDevices = [] + pairedDevices = [] + availableDevices = [] + } + } + + function onDiscoveringChanged() { + root.isDiscovering = Bluetooth.defaultAdapter.discovering + if (Bluetooth.defaultAdapter.discovering) { + Qt.callLater(refreshDevices) + } + } + + function onStateChanged() { + if (Bluetooth.defaultAdapter.state >= 4) { + Qt.callLater(refreshDevices) + } + } + + function onDevicesChanged() { + Qt.callLater(refreshDevices) + } + } + + // Watch for global device changes + Connections { + target: Bluetooth + ignoreUnknownSignals: true + + function onDevicesChanged() { + Qt.callLater(refreshDevices) + } + } +} \ No newline at end of file diff --git a/Services/BrightnessService.qml b/Services/BrightnessService.qml index 85eb9fd..d2ffa31 100644 --- a/Services/BrightnessService.qml +++ b/Services/BrightnessService.qml @@ -156,13 +156,13 @@ Singleton { readonly property Process initProc: Process { stdout: StdioCollector { onStreamFinished: { - console.log("[BrightnessService] Raw brightness data for", monitor.modelData.name + ":", text.trim()) + console.log("[Brightness] Raw brightness data for", monitor.modelData.name + ":", text.trim()) if (monitor.isAppleDisplay) { var val = parseInt(text.trim()) if (!isNaN(val)) { monitor.brightness = val / 101 - console.log("[BrightnessService] Apple display brightness:", monitor.brightness) + console.log("[Brightness] Apple display brightness:", monitor.brightness) } } else if (monitor.isDdc) { var parts = text.trim().split(" ") @@ -171,7 +171,7 @@ Singleton { var max = parseInt(parts[1]) if (!isNaN(current) && !isNaN(max) && max > 0) { monitor.brightness = current / max - console.log("[BrightnessService] DDC brightness:", current + "/" + max + " =", monitor.brightness) + console.log("[Brightness] DDC brightness:", current + "/" + max + " =", monitor.brightness) } } } else { @@ -182,7 +182,7 @@ Singleton { var max = parseInt(parts[1]) if (!isNaN(current) && !isNaN(max) && max > 0) { monitor.brightness = current / max - console.log("[BrightnessService] Internal brightness:", current + "/" + max + " =", monitor.brightness) + console.log("[Brightness] Internal brightness:", current + "/" + max + " =", monitor.brightness) } } } From 43686ea2602e9ce1f1a250289f7d222007e74497 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sat, 16 Aug 2025 09:48:06 -0400 Subject: [PATCH 352/394] Media service, logging --- Services/MediaService.qml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Services/MediaService.qml b/Services/MediaService.qml index 2c5bd1b..be08459 100644 --- a/Services/MediaService.qml +++ b/Services/MediaService.qml @@ -50,7 +50,7 @@ Singleton { function findActivePlayer() { let availablePlayers = getAvailablePlayers() if (availablePlayers.length === 0) { - console.log("[MediaPlayer] No active player found") + console.log("[Media] No active player found") return null } @@ -68,7 +68,7 @@ Singleton { if (newPlayer !== currentPlayer) { currentPlayer = newPlayer currentPosition = currentPlayer ? currentPlayer.position : 0 - console.log("[MediaPlayer] Switching player") + console.log("[Media] Switching player") } } @@ -149,7 +149,7 @@ Singleton { Connections { target: Mpris.players function onValuesChanged() { - console.log("[MediaPlayer] Players changed") + console.log("[Media] Players changed") updateCurrentPlayer() } } From 4794477be379e1826ab1b9db5c47b6878a6b4ab3 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sat, 16 Aug 2025 09:48:53 -0400 Subject: [PATCH 353/394] Settings: Removed duplicate brightness step slider in Display Tab --- Modules/SettingsPanel/Tabs/DisplayTab.qml | 51 ----------------------- 1 file changed, 51 deletions(-) diff --git a/Modules/SettingsPanel/Tabs/DisplayTab.qml b/Modules/SettingsPanel/Tabs/DisplayTab.qml index 7dad934..a8007e9 100644 --- a/Modules/SettingsPanel/Tabs/DisplayTab.qml +++ b/Modules/SettingsPanel/Tabs/DisplayTab.qml @@ -132,57 +132,6 @@ Item { } } - NDivider { - Layout.fillWidth: true - Layout.topMargin: Style.marginLarge * 2 * scaling - Layout.bottomMargin: Style.marginLarge * scaling - } - - // Brightness Section - ColumnLayout { - spacing: Style.marginSmall * scaling - Layout.fillWidth: true - Layout.topMargin: Style.marginSmall * scaling - - NText { - text: "Brightness Step Size" - font.weight: Style.fontWeightBold - color: Colors.mOnSurface - } - - NText { - text: "Adjust the step size for brightness changes (scroll wheel, ipc bind)" - font.pointSize: Style.fontSizeSmall * scaling - color: Colors.mOnSurface - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - - RowLayout { - Layout.fillWidth: true - spacing: Style.marginMedium * scaling - - NSlider { - Layout.fillWidth: true - from: 1 - to: 50 - value: Settings.data.brightness.brightnessStep - stepSize: 1 - onPressedChanged: { - if (!pressed) { - Settings.data.brightness.brightnessStep = value - } - } - } - - NText { - text: Settings.data.brightness.brightnessStep + "%" - Layout.alignment: Qt.AlignVCenter - color: Colors.mOnSurface - } - } - } - Item { Layout.fillHeight: true } From e800bc161dcf8a8ca7730fe5e8ed048d980c5efd Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sat, 16 Aug 2025 19:31:22 -0400 Subject: [PATCH 354/394] Using a custom logger with colors and timestamp instead of console.xxx --- Commons/Colors.qml | 4 +- Commons/Logger.qml | 33 ++++ Commons/Settings.qml | 4 +- Commons/Time.qml | 4 +- Modules/AppLauncher/AppLauncher.qml | 16 +- Modules/Background/Overview.qml | 4 +- Modules/Bar/Bluetooth.qml | 10 +- Modules/Bar/BluetoothMenu.qml | 39 +++-- Modules/Bar/Tray.qml | 2 +- Modules/Bar/TrayMenu.qml | 4 +- Modules/Bar/Volume.qml | 2 +- Modules/Bar/WiFiMenu.qml | 2 - Modules/Demo/DemoPanel.qml | 4 +- Modules/Dock/Dock.qml | 10 +- Modules/LockScreen/LockScreen.qml | 28 ++-- .../Notification/NotificationHistoryPanel.qml | 2 +- Modules/SettingsPanel/Tabs/BrightnessTab.qml | 14 +- Modules/SettingsPanel/Tabs/ColorSchemeTab.qml | 2 +- Modules/SidePanel/Cards/MediaCard.qml | 2 +- Modules/SidePanel/PowerMenu.qml | 4 +- Services/AudioService.qml | 8 +- Services/BluetoothService.qml | 158 +++++++++--------- Services/BrightnessService.qml | 8 +- Services/ColorSchemesService.qml | 12 +- Services/GitHubService.qml | 32 ++-- Services/LocationService.qml | 30 ++-- Services/MediaService.qml | 6 +- Services/NetworkService.qml | 10 +- Services/NiriService.qml | 10 +- Services/NotificationService.qml | 4 +- Services/ScreenRecorderService.qml | 6 +- Services/WallpapersService.qml | 26 +-- Services/WorkspacesService.qml | 12 +- Widgets/NLoader.qml | 2 +- 34 files changed, 278 insertions(+), 236 deletions(-) create mode 100644 Commons/Logger.qml diff --git a/Commons/Colors.qml b/Commons/Colors.qml index b6d09e2..a040d94 100644 --- a/Commons/Colors.qml +++ b/Commons/Colors.qml @@ -95,11 +95,11 @@ Singleton { path: Settings.configDir + "colors.json" watchChanges: true onFileChanged: { - console.log("[Colors] Reloading colors from disk") + Logger.log("Colors", "Reloading colors from disk") reload() } onAdapterUpdated: { - console.log("[Colors] Writing colors to disk") + Logger.log("Colors", "Writing colors to disk") writeAdapter() } onLoadFailed: function (error) { diff --git a/Commons/Logger.qml b/Commons/Logger.qml new file mode 100644 index 0000000..342c16a --- /dev/null +++ b/Commons/Logger.qml @@ -0,0 +1,33 @@ +pragma Singleton + +import Quickshell +import qs.Commons + +Singleton { + id: root + + function _formatMessage(...args) { + var t = Time.getFormattedTimestamp() + if (args.length > 1) { + var module = args.shift() + return `\x1b[36m${t}\x1b[0m \x1b[35m[${module}]\x1b[0m ` + args.join(" ") + } else { + return `\x1b[36m${t}\x1b[0m ` + args.join(" ") + } + } + + function log(...args) { + var msg = _formatMessage(...args) + console.log(msg) + } + + function warn(...args) { + var msg = _formatMessage(...args) + console.warn(msg) + } + + function error(...args) { + var msg = _formatMessage(...args) + console.error(msg) + } +} diff --git a/Commons/Settings.qml b/Commons/Settings.qml index ba38f94..faeabd8 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -49,11 +49,11 @@ Singleton { reload() } onLoaded: function () { - console.log("[Settings] Loaded") + Logger.log("Settings", "Loaded") Qt.callLater(function () { // Only set wallpaper on initial load, not on reloads if (isInitialLoad && adapter.wallpaper.current !== "") { - console.log("[Settings] Set current wallpaper", adapter.wallpaper.current) + Logger.log("Settings", "Set current wallpaper", adapter.wallpaper.current) WallpapersService.setCurrentWallpaper(adapter.wallpaper.current, true) } isInitialLoad = false diff --git a/Commons/Time.qml b/Commons/Time.qml index e027116..f464a4b 100644 --- a/Commons/Time.qml +++ b/Commons/Time.qml @@ -12,7 +12,7 @@ Singleton { property string time: { let timeFormat = Settings.data.location.use12HourClock ? "h:mm AP" : "HH:mm" let timeString = Qt.formatDateTime(date, timeFormat) - + if (Settings.data.location.showDateWithClock) { let dayName = date.toLocaleDateString(Qt.locale(), "ddd") dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1) @@ -20,7 +20,7 @@ Singleton { let month = date.toLocaleDateString(Qt.locale(), "MMM") return timeString + " - " + dayName + ", " + day + " " + month } - + return timeString } readonly property string dateString: { diff --git a/Modules/AppLauncher/AppLauncher.qml b/Modules/AppLauncher/AppLauncher.qml index e0a4f7c..8652e47 100644 --- a/Modules/AppLauncher/AppLauncher.qml +++ b/Modules/AppLauncher/AppLauncher.qml @@ -72,9 +72,9 @@ NLoader { property string searchText: "" property int selectedIndex: 0 property var filteredEntries: { - console.log("[AppLauncher] Total desktop entries:", desktopEntries ? desktopEntries.length : 0) + Logger.log("AppLauncher", "Total desktop entries:", desktopEntries ? desktopEntries.length : 0) if (!desktopEntries || desktopEntries.length === 0) { - console.log("[AppLauncher] No desktop entries available") + Logger.log("AppLauncher", "No desktop entries available") return [] } @@ -86,7 +86,7 @@ NLoader { return true }) - console.log("[AppLauncher] Visible entries:", visibleEntries.length) + Logger.log("AppLauncher", "Visible entries:", visibleEntries.length) var query = searchText ? searchText.toLowerCase() : "" var results = [] @@ -225,16 +225,16 @@ NLoader { })) } - console.log("[AppLauncher] Filtered entries:", results.length) + Logger.log("AppLauncher", "Filtered entries:", results.length) return results } Component.onCompleted: { - console.log("[AppLauncher] Component completed") - console.log("[AppLauncher] DesktopEntries available:", typeof DesktopEntries !== 'undefined') + Logger.log("AppLauncher", "Component completed") + Logger.log("AppLauncher", "DesktopEntries available:", typeof DesktopEntries !== 'undefined') if (typeof DesktopEntries !== 'undefined') { - console.log("[AppLauncher] DesktopEntries.entries:", - DesktopEntries.entries ? DesktopEntries.entries.length : 'undefined') + Logger.log("AppLauncher", "DesktopEntries.entries:", + DesktopEntries.entries ? DesktopEntries.entries.length : 'undefined') } // Start clipboard refresh immediately on open updateClipboardHistory() diff --git a/Modules/Background/Overview.qml b/Modules/Background/Overview.qml index 3c3d680..a2e7e10 100644 --- a/Modules/Background/Overview.qml +++ b/Modules/Background/Overview.qml @@ -11,9 +11,9 @@ NLoader { Component.onCompleted: { if (WorkspacesService.isNiri) { - console.log("[Overview] Loading Overview component (Niri detected)") + Logger.log("Overview", "Loading Overview component (Niri detected)") } else { - console.log("[Overview] Skipping Overview component (Niri not detected)") + Logger.log("Overview", "Skipping Overview component (Niri not detected)") } } diff --git a/Modules/Bar/Bluetooth.qml b/Modules/Bar/Bluetooth.qml index b381fb0..c8792d9 100644 --- a/Modules/Bar/Bluetooth.qml +++ b/Modules/Bar/Bluetooth.qml @@ -14,12 +14,12 @@ NIconButton { sizeMultiplier: 0.8 showBorder: false visible: bluetoothEnabled - + Component.onCompleted: { - console.log("[Bluetooth] Component loaded, bluetoothEnabled:", bluetoothEnabled) - console.log("[Bluetooth] BluetoothService available:", typeof BluetoothService !== 'undefined') + Logger.log("Bluetooth", "Component loaded, bluetoothEnabled:", bluetoothEnabled) + Logger.log("Bluetooth", "BluetoothService available:", typeof BluetoothService !== 'undefined') if (typeof BluetoothService !== 'undefined') { - console.log("[Bluetooth] Connected devices:", BluetoothService.connectedDevices.length) + Logger.log("Bluetooth", "Connected devices:", BluetoothService.connectedDevices.length) } } icon: { @@ -55,4 +55,4 @@ NIconButton { BluetoothMenu { id: bluetoothMenuLoader } -} \ No newline at end of file +} diff --git a/Modules/Bar/BluetoothMenu.qml b/Modules/Bar/BluetoothMenu.qml index cfe4dd7..560d929 100644 --- a/Modules/Bar/BluetoothMenu.qml +++ b/Modules/Bar/BluetoothMenu.qml @@ -214,34 +214,34 @@ NLoader { // Combine all devices into a single list for the ListView property var allDevices: { const devices = [] - + // Add connected devices first for (const device of BluetoothService.connectedDevices) { devices.push({ - device: device, - type: 'connected', - section: 'Connected Devices' - }) + "device": device, + "type": 'connected', + "section": 'Connected Devices' + }) } - + // Add paired devices for (const device of BluetoothService.pairedDevices) { devices.push({ - device: device, - type: 'paired', - section: 'Paired Devices' - }) + "device": device, + "type": 'paired', + "section": 'Paired Devices' + }) } - + // Add available devices for (const device of BluetoothService.availableDevices) { devices.push({ - device: device, - type: 'available', - section: 'Available Devices' - }) + "device": device, + "type": 'available', + "section": 'Available Devices' + }) } - + return devices } @@ -362,9 +362,8 @@ NLoader { // Empty state when no devices found ColumnLayout { anchors.centerIn: parent - visible: Settings.data.network.bluetoothEnabled && - !BluetoothService.isDiscovering && - deviceList.count === 0 + visible: Settings.data.network.bluetoothEnabled && !BluetoothService.isDiscovering + && deviceList.count === 0 spacing: Style.marginMedium * scaling NText { @@ -394,4 +393,4 @@ NLoader { } } } -} \ No newline at end of file +} diff --git a/Modules/Bar/Tray.qml b/Modules/Bar/Tray.qml index af612e9..76f78bf 100644 --- a/Modules/Bar/Tray.qml +++ b/Modules/Bar/Tray.qml @@ -100,7 +100,7 @@ Item { trayPanel.show() } else { - console.log("[Tray] no menu available for", modelData.id, "or trayMenu not set") + Logger.log("Tray", "No menu available for", modelData.id, "or trayMenu not set") } } } diff --git a/Modules/Bar/TrayMenu.qml b/Modules/Bar/TrayMenu.qml index d9741e3..a190a9f 100644 --- a/Modules/Bar/TrayMenu.qml +++ b/Modules/Bar/TrayMenu.qml @@ -44,7 +44,7 @@ PopupWindow { function showAt(item, x, y) { if (!item) { - console.warn("CustomTrayMenu: anchorItem is undefined, won't show menu.") + Logger.warn("TrayMenu", "anchorItem is undefined, won't show menu.") return } anchorItem = item @@ -267,7 +267,7 @@ PopupWindow { function showAt(item, x, y) { if (!item) { - console.warn("subMenuComponent: anchorItem is undefined, not showing menu.") + Logger.warn("TrayMenu", "SubComponent anchorItem is undefined, not showing menu.") return } anchorItem = item diff --git a/Modules/Bar/Volume.qml b/Modules/Bar/Volume.qml index 4293e0c..a6d364b 100644 --- a/Modules/Bar/Volume.qml +++ b/Modules/Bar/Volume.qml @@ -26,7 +26,7 @@ Item { Connections { target: AudioService.sink?.audio ? AudioService.sink?.audio : null function onVolumeChanged() { - // console.log("[Bar:Volume] onVolumeChanged") + // Logger.log("Bar:Volume", "onVolumeChanged") if (!firstVolumeReceived) { // Ignore the first volume change firstVolumeReceived = true diff --git a/Modules/Bar/WiFiMenu.qml b/Modules/Bar/WiFiMenu.qml index 87b9aa0..807cf35 100644 --- a/Modules/Bar/WiFiMenu.qml +++ b/Modules/Bar/WiFiMenu.qml @@ -145,8 +145,6 @@ NLoader { Layout.fillWidth: true } - - NIconButton { icon: "refresh" tooltipText: "Refresh Networks" diff --git a/Modules/Demo/DemoPanel.qml b/Modules/Demo/DemoPanel.qml index 1f0ea02..d3c0b60 100644 --- a/Modules/Demo/DemoPanel.qml +++ b/Modules/Demo/DemoPanel.qml @@ -202,7 +202,7 @@ NLoader { label: "Label" description: "Description" onToggled: checked => { - console.log("[DemoPanel] NToggle:", checked) + Logger.log("DemoPanel", "NToggle:", checked) } } @@ -255,7 +255,7 @@ NLoader { } currentKey: "dog" onSelected: function (key) { - console.log("[DemoPanel] NComboBox: selected ", key) + Logger.log("DemoPanel", "NComboBox: selected ", key) } } diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml index f359dba..195c927 100644 --- a/Modules/Dock/Dock.qml +++ b/Modules/Dock/Dock.qml @@ -197,7 +197,7 @@ NLoader { color: appButton.hovered ? Colors.mSurfaceVariant : "transparent" radius: parent.radius opacity: appButton.hovered ? 0.8 : 0 - + Behavior on opacity { NumberAnimation { duration: Style.animationFast @@ -218,9 +218,9 @@ NLoader { mipmap: false antialiasing: false fillMode: Image.PreserveAspectFit - + scale: appButton.hovered ? 1.1 : 1.0 - + Behavior on scale { NumberAnimation { duration: Style.animationFast @@ -237,9 +237,9 @@ NLoader { font.family: "Material Symbols Rounded" font.pointSize: iconSize * 0.7 * scaling color: appButton.isActive ? Colors.mPrimary : Colors.mOnSurfaceVariant - + scale: appButton.hovered ? 1.1 : 1.0 - + Behavior on scale { NumberAnimation { duration: Style.animationFast diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index bdfd843..3e8ab76 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -24,39 +24,39 @@ WlSessionLock { locked: false function unlockAttempt() { - console.log("Unlock attempt started") + Logger.log("LockScreen", "Unlock attempt started") // Real PAM authentication if (!pamAvailable) { lock.errorMessage = "PAM authentication not available." - console.log("PAM not available") + Logger.log("LockScreen", "PAM not available") return } if (!lock.password) { lock.errorMessage = "Password required." - console.log("No password entered") + Logger.log("LockScreen", "No password entered") return } - console.log("Starting PAM authentication...") + Logger.log("LockScreen", "Starting PAM authentication") lock.authenticating = true lock.errorMessage = "" - console.log("[LockScreen] About to create PAM context with userName:", Quickshell.env("USER")) + Logger.log("LockScreen", "About to create PAM context with userName:", Quickshell.env("USER")) var pam = Qt.createQmlObject( 'import Quickshell.Services.Pam; PamContext { config: "login"; user: "' + Quickshell.env("USER") + '" }', lock) - console.log("PamContext created", pam) + Logger.log("LockScreen", "PamContext created", pam) pam.onCompleted.connect(function (result) { - console.log("PAM completed with result:", result) + Logger.log("LockScreen", "PAM completed with result:", result) lock.authenticating = false if (result === PamResult.Success) { - console.log("Authentication successful, unlocking...") + Logger.log("LockScreen", "Authentication successful, unlocking") lock.locked = false lock.password = "" lock.errorMessage = "" } else { - console.log("Authentication failed") + Logger.log("LockScreen", "Authentication failed") lock.errorMessage = "Authentication failed." lock.password = "" } @@ -64,7 +64,7 @@ WlSessionLock { }) pam.onError.connect(function (error) { - console.log("PAM error:", error) + Logger.log("LockScreen", "PAM error:", error) lock.authenticating = false lock.errorMessage = pam.message || "Authentication error." lock.password = "" @@ -72,22 +72,22 @@ WlSessionLock { }) pam.onPamMessage.connect(function () { - console.log("PAM message:", pam.message, "isError:", pam.messageIsError) + Logger.log("LockScreen", "PAM message:", pam.message, "isError:", pam.messageIsError) if (pam.messageIsError) { lock.errorMessage = pam.message } }) pam.onResponseRequiredChanged.connect(function () { - console.log("PAM response required:", pam.responseRequired) + Logger.log("LockScreen", "PAM response required:", pam.responseRequired) if (pam.responseRequired && lock.authenticating) { - console.log("Responding to PAM with password") + Logger.log("LockScreen", "Responding to PAM with password") pam.respond(lock.password) } }) var started = pam.start() - console.log("PAM start result:", started) + Logger.log("LockScreen", "PAM start result:", started) } WlSessionLockSurface { diff --git a/Modules/Notification/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml index 8587e9e..959b347 100644 --- a/Modules/Notification/NotificationHistoryPanel.qml +++ b/Modules/Notification/NotificationHistoryPanel.qml @@ -248,7 +248,7 @@ NLoader { sizeMultiplier: 0.7 onClicked: { - console.log("[NotificationHistory] Removing notification:", summary) + Logger.log("NotificationHistory", "Removing notification:", summary) NotificationService.historyModel.remove(index) NotificationService.saveHistory() } diff --git a/Modules/SettingsPanel/Tabs/BrightnessTab.qml b/Modules/SettingsPanel/Tabs/BrightnessTab.qml index dcc8871..d195f29 100644 --- a/Modules/SettingsPanel/Tabs/BrightnessTab.qml +++ b/Modules/SettingsPanel/Tabs/BrightnessTab.qml @@ -58,8 +58,8 @@ Item { description: "Display the brightness control icon in the top bar" checked: !Settings.data.bar.hideBrightness onToggled: checked => { - Settings.data.bar.hideBrightness = !checked - } + Settings.data.bar.hideBrightness = !checked + } } } @@ -168,7 +168,9 @@ Item { color: Colors.mSecondary } - Item { Layout.fillWidth: true } + Item { + Layout.fillWidth: true + } NText { text: BrightnessService.currentMethod === "ddcutil" ? "External (DDC)" : "Internal" @@ -228,7 +230,9 @@ Item { Layout.alignment: Qt.AlignLeft } - Item { Layout.fillWidth: true } + Item { + Layout.fillWidth: true + } NText { text: BrightnessService.available ? "Available" : "Unavailable" @@ -247,4 +251,4 @@ Item { } } } -} \ No newline at end of file +} diff --git a/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml b/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml index 0c467cf..9eb8e4e 100644 --- a/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml +++ b/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml @@ -92,7 +92,7 @@ ColumnLayout { var jsonData = JSON.parse(text()) root.schemeLoaded(schemeName, jsonData) } catch (e) { - console.warn("Failed to parse JSON for scheme:", schemeName, e) + Logger.warn("Failed to parse JSON for scheme:", schemeName, e) root.schemeLoaded(schemeName, null) // Load defaults on parse error } } diff --git a/Modules/SidePanel/Cards/MediaCard.qml b/Modules/SidePanel/Cards/MediaCard.qml index 76d6f72..4c1e603 100644 --- a/Modules/SidePanel/Cards/MediaCard.qml +++ b/Modules/SidePanel/Cards/MediaCard.qml @@ -18,7 +18,7 @@ NBox { // Height can be overridden by parent layout (SidePanel binds it to stats card) //implicitHeight: content.implicitHeight + Style.marginLarge * 2 * scaling // Component.onCompleted: { - // console.log(MediaService.trackArtUrl) + // Logger.logMediaService.trackArtUrl) // } ColumnLayout { anchors.fill: parent diff --git a/Modules/SidePanel/PowerMenu.qml b/Modules/SidePanel/PowerMenu.qml index fbbd3e5..5fa5bf5 100644 --- a/Modules/SidePanel/PowerMenu.qml +++ b/Modules/SidePanel/PowerMenu.qml @@ -103,7 +103,7 @@ NPanel { hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { - console.log("Lock screen requested") + Logger.log("PowerMenu", "Lock screen requested") // Lock the screen lockScreen.locked = true powerMenu.visible = false @@ -353,7 +353,7 @@ NPanel { } else if (WorkspacesService.isHyprland) { logoutProcessHyprland.running = true } else { - console.warn("No supported compositor detected for logout") + Logger.warn("PowerMenu", "No supported compositor detected for logout") } } diff --git a/Services/AudioService.qml b/Services/AudioService.qml index e31f17e..f11813d 100644 --- a/Services/AudioService.qml +++ b/Services/AudioService.qml @@ -53,7 +53,7 @@ Singleton { function onMutedChanged() { root._muted = (sink?.audio.muted ?? true) - console.log("[AudioService] onMuteChanged:", root._muted) + Logger.log("AudioService", "OnMuteChanged:", root._muted) } } @@ -70,9 +70,9 @@ Singleton { // Clamp it accordingly sink.audio.muted = false sink.audio.volume = Math.max(0, Math.min(1, newVolume)) - //console.log("[AudioService] setVolume", sink.audio.volume); + //Logger.log("AudioService", "SetVolume", sink.audio.volume); } else { - console.warn("[AudioService] No sink available") + Logger.warn("AudioService", "No sink available") } } @@ -80,7 +80,7 @@ Singleton { if (sink?.ready && sink?.audio) { sink.audio.muted = muted } else { - console.warn("[AudioService] No sink available") + Logger.warn("AudioService", "No sink available") } } diff --git a/Services/BluetoothService.qml b/Services/BluetoothService.qml index 11530df..5baa609 100644 --- a/Services/BluetoothService.qml +++ b/Services/BluetoothService.qml @@ -28,21 +28,21 @@ Singleton { } Component.onCompleted: { - console.log("[Bluetooth] Service started") - + Logger.log("Bluetooth", "Service started") + if (isEnabled && Bluetooth.defaultAdapter) { // Ensure adapter is enabled if (!Bluetooth.defaultAdapter.enabled) { Bluetooth.defaultAdapter.enabled = true } - + // Start discovery to find devices if (!Bluetooth.defaultAdapter.discovering) { Bluetooth.defaultAdapter.discovering = true } - + // Refresh devices after a short delay to allow discovery to start - Qt.callLater(function() { + Qt.callLater(function () { refreshDevices() }) } @@ -50,7 +50,7 @@ Singleton { // Function to enable/disable Bluetooth function setBluetoothEnabled(enabled) { - + if (enabled) { // Store the currently connected devices before enabling for (const device of connectedDevices) { @@ -59,20 +59,20 @@ Singleton { break } } - + // Enable Bluetooth if (Bluetooth.defaultAdapter) { Bluetooth.defaultAdapter.enabled = true - + // Start discovery to find devices if (!Bluetooth.defaultAdapter.discovering) { Bluetooth.defaultAdapter.discovering = true } - + // Refresh devices after enabling Qt.callLater(refreshDevices) } else { - console.warn("[Bluetooth] No Bluetooth adapter found!") + Logger.warn("Bluetooth", "No adapter found") } } else { // Disconnect from current devices before disabling @@ -81,14 +81,14 @@ Singleton { device.disconnect() } } - + // Disable Bluetooth if (Bluetooth.defaultAdapter) { - console.log("[Bluetooth] Disabling Bluetooth adapter") + Logger.log("Bluetooth", "Disabling adapter") Bluetooth.defaultAdapter.enabled = false } } - + Settings.data.network.bluetoothEnabled = enabled isEnabled = enabled } @@ -101,25 +101,24 @@ Singleton { availableDevices = [] return } - - // Remove duplicate check since we already did it above + // Remove duplicate check since we already did it above const connected = [] const paired = [] const available = [] let devices = null - + // Try adapter devices first if (Bluetooth.defaultAdapter.enabled && Bluetooth.defaultAdapter.devices) { devices = Bluetooth.defaultAdapter.devices } - + // Fallback to global devices list if (!devices && Bluetooth.devices) { devices = Bluetooth.devices } - + if (!devices) { connectedDevices = [] pairedDevices = [] @@ -129,19 +128,20 @@ Singleton { // Use Qt model methods to iterate through the ObjectModel let deviceFound = false - + try { // Get the row count using the Qt model method const rowCount = devices.rowCount() - + if (rowCount > 0) { // Iterate through each row using the Qt model data() method - for (let i = 0; i < rowCount; i++) { + for (var i = 0; i < rowCount; i++) { try { // Create a model index for this row const modelIndex = devices.index(i, 0) - if (!modelIndex.valid) continue - + if (!modelIndex.valid) + continue + // Get the device object using the Qt.UserRole (typically 256) const device = devices.data(modelIndex, 256) // Qt.UserRole if (!device) { @@ -153,9 +153,9 @@ Singleton { continue } } - + deviceFound = true - + if (device.connected) { connected.push(device) } else if (device.paired) { @@ -163,13 +163,13 @@ Singleton { } else { available.push(device) } - } catch (e) { + // Silent error handling } } } - + // Alternative method: try the values property if available if (!deviceFound && devices.values) { try { @@ -177,7 +177,7 @@ Singleton { if (values && typeof values === 'object') { // Try to iterate through values if it's iterable if (values.length !== undefined) { - for (let i = 0; i < values.length; i++) { + for (var i = 0; i < values.length; i++) { const device = values[i] if (device) { deviceFound = true @@ -193,18 +193,18 @@ Singleton { } } } catch (e) { + // Silent error handling } } - } catch (e) { - console.warn("[Bluetooth] Error accessing device model:", e) + Logger.warn("Bluetooth", "Error accessing device model:", e) } if (!deviceFound) { - console.log("[Bluetooth] No devices found") + Logger.log("Bluetooth", "No device found") } - + connectedDevices = connected pairedDevices = paired availableDevices = available @@ -212,39 +212,42 @@ Singleton { // Function to start discovery function startDiscovery() { - if (!isEnabled || !Bluetooth.defaultAdapter) return - + if (!isEnabled || !Bluetooth.defaultAdapter) + return + isDiscovering = true Bluetooth.defaultAdapter.discovering = true } // Function to stop discovery function stopDiscovery() { - if (!Bluetooth.defaultAdapter) return - + if (!Bluetooth.defaultAdapter) + return + isDiscovering = false Bluetooth.defaultAdapter.discovering = false } // Function to connect to a device function connectDevice(device) { - if (!device) return - + if (!device) + return + // Check if device is still valid (not stale from previous session) if (!device.connect || typeof device.connect !== 'function') { - console.warn("[Bluetooth] Device object is stale, refreshing devices") + Logger.warn("Bluetooth", "Device object is stale, refreshing devices") refreshDevices() return } - + connectStatus = "connecting" connectStatusDevice = device.name || device.deviceName connectError = "" - + try { device.connect() } catch (error) { - console.error("[Bluetooth] Error connecting to device:", error) + Logger.error("Bluetooth", "Error connecting to device:", error) connectStatus = "error" connectError = error.toString() Qt.callLater(refreshDevices) @@ -253,15 +256,16 @@ Singleton { // Function to disconnect from a device function disconnectDevice(device) { - if (!device) return - + if (!device) + return + // Check if device is still valid (not stale from previous session) if (!device.disconnect || typeof device.disconnect !== 'function') { - console.warn("[Bluetooth] Device object is stale, refreshing devices") + Logger.warn("Bluetooth", "Device object is stale, refreshing devices") refreshDevices() return } - + try { device.disconnect() // Clear connection status @@ -269,72 +273,74 @@ Singleton { connectStatusDevice = "" connectError = "" } catch (error) { - console.warn("[Bluetooth] Error disconnecting device:", error) + Logger.warn("Bluetooth", "Error disconnecting device:", error) Qt.callLater(refreshDevices) } } // Function to pair with a device function pairDevice(device) { - if (!device) return - + if (!device) + return + // Check if device is still valid (not stale from previous session) if (!device.pair || typeof device.pair !== 'function') { - console.warn("[Bluetooth] Device object is stale, refreshing devices") + Logger.warn("Bluetooth", "Device object is stale, refreshing devices") refreshDevices() return } - + try { device.pair() } catch (error) { - console.warn("[Bluetooth] Error pairing device:", error) + Logger.warn("Bluetooth", "Error pairing device:", error) Qt.callLater(refreshDevices) } } // Function to forget a device function forgetDevice(device) { - if (!device) return - + if (!device) + return + // Check if device is still valid (not stale from previous session) if (!device.forget || typeof device.forget !== 'function') { - console.warn("[Bluetooth] Device object is stale, refreshing devices") + Logger.warn("Bluetooth", "Device object is stale, refreshing devices") refreshDevices() return } - + // Store device info before forgetting (in case device object becomes invalid) const deviceName = device.name || device.deviceName || "Unknown Device" - + try { device.forget() - + // Clear any connection status that might be related to this device if (connectStatusDevice === deviceName) { connectStatus = "" connectStatusDevice = "" connectError = "" } - + // Refresh devices after a delay to ensure the forget operation is complete Qt.callLater(refreshDevices, 1000) - } catch (error) { - console.warn("[Bluetooth] Error forgetting device:", error) + Logger.warn("Bluetooth", "Error forgetting device:", error) Qt.callLater(refreshDevices, 500) } } // Function to get device icon function getDeviceIcon(device) { - if (!device) return "bluetooth" - + if (!device) + return "bluetooth" + // Use device icon if available, otherwise fall back to device type if (device.icon) { return device.icon } - + // Fallback icons based on common device types const name = (device.name || device.deviceName || "").toLowerCase() if (name.includes("headphone") || name.includes("earbud") || name.includes("airpods")) { @@ -350,14 +356,15 @@ Singleton { } else if (name.includes("laptop") || name.includes("computer")) { return "laptop" } - + return "bluetooth" } // Function to get device status text function getDeviceStatus(device) { - if (!device) return "" - + if (!device) + return "" + if (device.connected) { return "Connected" } else if (device.pairing) { @@ -371,8 +378,9 @@ Singleton { // Function to get battery level text function getBatteryText(device) { - if (!device || !device.batteryAvailable) return "" - + if (!device || !device.batteryAvailable) + return "" + const percentage = Math.round(device.battery * 100) return `${percentage}%` } @@ -381,7 +389,7 @@ Singleton { Connections { target: Bluetooth.defaultAdapter ignoreUnknownSignals: true - + function onEnabledChanged() { root.isEnabled = Bluetooth.defaultAdapter.enabled Settings.data.network.bluetoothEnabled = root.isEnabled @@ -393,20 +401,20 @@ Singleton { availableDevices = [] } } - + function onDiscoveringChanged() { root.isDiscovering = Bluetooth.defaultAdapter.discovering if (Bluetooth.defaultAdapter.discovering) { Qt.callLater(refreshDevices) } } - + function onStateChanged() { if (Bluetooth.defaultAdapter.state >= 4) { Qt.callLater(refreshDevices) } } - + function onDevicesChanged() { Qt.callLater(refreshDevices) } @@ -416,9 +424,9 @@ Singleton { Connections { target: Bluetooth ignoreUnknownSignals: true - + function onDevicesChanged() { Qt.callLater(refreshDevices) } } -} \ No newline at end of file +} diff --git a/Services/BrightnessService.qml b/Services/BrightnessService.qml index d2ffa31..306bca4 100644 --- a/Services/BrightnessService.qml +++ b/Services/BrightnessService.qml @@ -156,13 +156,13 @@ Singleton { readonly property Process initProc: Process { stdout: StdioCollector { onStreamFinished: { - console.log("[Brightness] Raw brightness data for", monitor.modelData.name + ":", text.trim()) + Logger.log("Brightness", "Raw brightness data for", monitor.modelData.name + ":", text.trim()) if (monitor.isAppleDisplay) { var val = parseInt(text.trim()) if (!isNaN(val)) { monitor.brightness = val / 101 - console.log("[Brightness] Apple display brightness:", monitor.brightness) + Logger.log("Brightness", "Apple display brightness:", monitor.brightness) } } else if (monitor.isDdc) { var parts = text.trim().split(" ") @@ -171,7 +171,7 @@ Singleton { var max = parseInt(parts[1]) if (!isNaN(current) && !isNaN(max) && max > 0) { monitor.brightness = current / max - console.log("[Brightness] DDC brightness:", current + "/" + max + " =", monitor.brightness) + Logger.log("Brightness", "DDC brightness:", current + "/" + max + " =", monitor.brightness) } } } else { @@ -182,7 +182,7 @@ Singleton { var max = parseInt(parts[1]) if (!isNaN(current) && !isNaN(max) && max > 0) { monitor.brightness = current / max - console.log("[Brightness] Internal brightness:", current + "/" + max + " =", monitor.brightness) + Logger.log("Brightness", "Internal brightness:", current + "/" + max + " =", monitor.brightness) } } } diff --git a/Services/ColorSchemesService.qml b/Services/ColorSchemesService.qml index 85d9c2c..29af4c4 100644 --- a/Services/ColorSchemesService.qml +++ b/Services/ColorSchemesService.qml @@ -10,7 +10,7 @@ Singleton { id: root Component.onCompleted: { - console.log("[ColorSchemes] Service started") + Logger.log("ColorSchemes", "Service started") loadColorSchemes() } @@ -20,7 +20,7 @@ Singleton { property string colorsJsonFilePath: Settings.configDir + "colors.json" function loadColorSchemes() { - console.log("[ColorSchemes] Load ColorSchemes") + Logger.log("ColorSchemes", "Load ColorSchemes") scanning = true schemes = [] // Unsetting, then setting the folder will re-trigger the parsing! @@ -34,7 +34,7 @@ Singleton { function changedWallpaper() { if (Settings.data.colorSchemes.useWallpaperColors) { - console.log("[ColorSchemes] Starting color generation process") + Logger.log("ColorSchemes", "Starting color generation from wallpaper") generateColorsProcess.running = true // Invalidate potential predefined scheme Settings.data.colorSchemes.predefinedScheme = "" @@ -55,7 +55,7 @@ Singleton { } schemes = files scanning = false - console.log("[ColorSchemes] Loaded", schemes.length, "schemes") + Logger.log("ColorSchemes", "Loaded", schemes.length, "schemes") } } } @@ -67,13 +67,13 @@ Singleton { running: false stdout: StdioCollector { onStreamFinished: { - console.log("[ColorSchemes] Generated colors from wallpaper") + Logger.log("ColorSchemes", "Completed colors generation") } } stderr: StdioCollector { onStreamFinished: { if (this.text !== "") { - console.error(this.text) + Logger.error(this.text) } } } diff --git a/Services/GitHubService.qml b/Services/GitHubService.qml index 2e329e5..d0159f7 100644 --- a/Services/GitHubService.qml +++ b/Services/GitHubService.qml @@ -52,11 +52,11 @@ Singleton { function loadFromCache() { const now = Time.timestamp if (!data.timestamp || (now >= data.timestamp + githubUpdateFrequency)) { - console.log("[GitHub] Cache expired or missing, fetching new data") + Logger.log("GitHub", "Cache expired or missing, fetching new data") fetchFromGitHub() return } - console.log("[GitHub] Loading cached GitHub data (age:", Math.round((now - data.timestamp) / 60), "minutes)") + Logger.log("GitHub", "Loading cached GitHub data (age:", Math.round((now - data.timestamp) / 60), "minutes)") if (data.version) { root.latestVersion = data.version @@ -69,7 +69,7 @@ Singleton { // -------------------------------- function fetchFromGitHub() { if (isFetchingData) { - console.warn("[GitHub] GitHub data is still fetching") + Logger.warn("GitHub", "GitHub data is still fetching") return } @@ -81,8 +81,8 @@ Singleton { // -------------------------------- function saveData() { data.timestamp = Time.timestamp - console.log("[GitHub] Saving data to cache file:", githubDataFile) - console.log("[GitHub] Data to save - version:", data.version, "contributors:", data.contributors.length) + Logger.log("GitHub", "Saving data to cache file:", githubDataFile) + Logger.log("GitHub", "Data to save - version:", data.version, "contributors:", data.contributors.length) // Ensure cache directory exists Quickshell.execDetached(["mkdir", "-p", Settings.cacheDir]) @@ -90,7 +90,7 @@ Singleton { Qt.callLater(() => { // Use direct ID reference to the FileView githubDataFileView.writeAdapter() - console.log("[GitHub] Cache file written successfully") + Logger.log("GitHub", "Cache file written successfully") }) } @@ -119,15 +119,15 @@ Singleton { const version = data.tag_name root.data.version = version root.latestVersion = version - console.log("[GitHub] Latest version fetched from GitHub:", version) + Logger.log("GitHub", "Latest version fetched from GitHub:", version) } else { - console.log("[GitHub] No tag_name in GitHub response") + Logger.log("GitHub", "No tag_name in GitHub response") } } else { - console.log("[GitHub] Empty response from GitHub API") + Logger.log("GitHub", "Empty response from GitHub API") } } catch (e) { - console.error("[GitHub] Failed to parse version:", e) + Logger.error("GitHub", "Failed to parse version:", e) } // Check if both processes are done @@ -145,21 +145,21 @@ Singleton { onStreamFinished: { try { const response = text - console.log("[GitHub] Raw contributors response length:", response ? response.length : 0) + Logger.log("GitHub", "Raw contributors response length:", response ? response.length : 0) if (response && response.trim()) { const data = JSON.parse(response) - console.log("[GitHub] Parsed contributors data type:", typeof data, "length:", - Array.isArray(data) ? data.length : "not array") + Logger.log("GitHub", "Parsed contributors data type:", typeof data, "length:", + Array.isArray(data) ? data.length : "not array") root.data.contributors = data || [] root.contributors = root.data.contributors - console.log("[GitHub] Contributors fetched from GitHub:", root.contributors.length) + Logger.log("GitHub", "Contributors fetched from GitHub:", root.contributors.length) } else { - console.log("[GitHub] Empty response from GitHub API for contributors") + Logger.log("GitHub", "Empty response from GitHub API for contributors") root.data.contributors = [] root.contributors = [] } } catch (e) { - console.error("[GitHub] Failed to parse contributors:", e) + Logger.error("GitHub", "Failed to parse contributors:", e) root.data.contributors = [] root.contributors = [] } diff --git a/Services/LocationService.qml b/Services/LocationService.qml index b70e372..23243d2 100644 --- a/Services/LocationService.qml +++ b/Services/LocationService.qml @@ -50,12 +50,12 @@ Singleton { function init() { // does nothing but ensure the singleton is created // do not remove - console.log("[Location] Service started") + Logger.log("Location", "Service started") } // -------------------------------- function resetWeather() { - console.log("[Location] Resetting weather data") + Logger.log("Location", "Resetting weather data") data.latitude = "" data.longitude = "" @@ -70,12 +70,12 @@ Singleton { // -------------------------------- function updateWeather() { if (isFetchingWeather) { - console.warn("[Location] Weather is still fetching") + Logger.warn("Location", "Weather is still fetching") return } if (data.latitude === "") { - console.warn("[Location] Why is my latitude empty") + Logger.warn("Location", "Why is my latitude empty") } if ((data.weatherLastFetch === "") || (data.weather === null) || (data.latitude === "") || (data.longitude === "") @@ -91,7 +91,7 @@ Singleton { if ((data.latitude === "") || (data.longitude === "") || (data.name !== Settings.data.location.name)) { _geocodeLocation(Settings.data.location.name, function (latitude, longitude) { - console.log("[Location] Geocoded", Settings.data.location.name, "to:", latitude, "/", longitude) + Logger.log("Location", " Geocoded", Settings.data.location.name, "to:", latitude, "/", longitude) // Save location name data.name = Settings.data.location.name @@ -109,7 +109,7 @@ Singleton { // -------------------------------- function _geocodeLocation(locationName, callback, errorCallback) { - console.log("[Location] Geocoding from api.open-meteo.com") + Logger.log("Location", "Geocoding from api.open-meteo.com") var geoUrl = "https://geocoding-api.open-meteo.com/v1/search?name=" + encodeURIComponent( locationName) + "&language=en&format=json" var xhr = new XMLHttpRequest() @@ -118,17 +118,17 @@ Singleton { if (xhr.status === 200) { try { var geoData = JSON.parse(xhr.responseText) - // console.log(JSON.stringify(geoData)) + // Logger.logJSON.stringify(geoData)) if (geoData.results && geoData.results.length > 0) { callback(geoData.results[0].latitude, geoData.results[0].longitude) } else { - errorCallback("[Location] could not resolve location name") + errorCallback("Location", "could not resolve location name") } } catch (e) { - errorCallback("[Location] Failed to parse geocoding data: " + e) + errorCallback("Location", "Failed to parse geocoding data: " + e) } } else { - errorCallback("[Location] Geocoding error: " + xhr.status) + errorCallback("Location", "Geocoding error: " + xhr.status) } } } @@ -138,7 +138,7 @@ Singleton { // -------------------------------- function _fetchWeather(latitude, longitude, errorCallback) { - console.log("[Location] Fetching weather from api.open-meteo.com") + Logger.log("Location", "Fetching weather from api.open-meteo.com") var url = "https://api.open-meteo.com/v1/forecast?latitude=" + latitude + "&longitude=" + longitude + "¤t_weather=true¤t=relativehumidity_2m,surface_pressure&daily=temperature_2m_max,temperature_2m_min,weathercode&timezone=auto" var xhr = new XMLHttpRequest() @@ -155,12 +155,12 @@ Singleton { data.longitude = weatherData.longitude.toString() isFetchingWeather = false - console.log("[Location] Cached weather to disk") + Logger.log("Location", "Cached weather to disk") } catch (e) { - errorCallback("[Location] Failed to parse weather data") + errorCallback("Location", "Failed to parse weather data") } } else { - errorCallback("[Location] Weather fetch error: " + xhr.status) + errorCallback("Location", "Weather fetch error: " + xhr.status) } } } @@ -170,7 +170,7 @@ Singleton { // -------------------------------- function errorCallback(message) { - console.error(message) + Logger.error(message) isFetchingWeather = false } diff --git a/Services/MediaService.qml b/Services/MediaService.qml index be08459..c44738d 100644 --- a/Services/MediaService.qml +++ b/Services/MediaService.qml @@ -50,7 +50,7 @@ Singleton { function findActivePlayer() { let availablePlayers = getAvailablePlayers() if (availablePlayers.length === 0) { - console.log("[Media] No active player found") + Logger.log("Media", "No active player found") return null } @@ -68,7 +68,7 @@ Singleton { if (newPlayer !== currentPlayer) { currentPlayer = newPlayer currentPosition = currentPlayer ? currentPlayer.position : 0 - console.log("[Media] Switching player") + Logger.log("Media", "Switching player") } } @@ -149,7 +149,7 @@ Singleton { Connections { target: Mpris.players function onValuesChanged() { - console.log("[Media] Players changed") + Logger.log("Media", "Players changed") updateCurrentPlayer() } } diff --git a/Services/NetworkService.qml b/Services/NetworkService.qml index 0799f3b..65378f0 100644 --- a/Services/NetworkService.qml +++ b/Services/NetworkService.qml @@ -18,7 +18,7 @@ Singleton { property bool isLoading: false Component.onCompleted: { - console.log("[Network] Service started") + Logger.log("Network", "Service started") // Only refresh networks if WiFi is enabled if (Settings.data.network.wifiEnabled) { refreshNetworks() @@ -165,7 +165,7 @@ Singleton { stderr: StdioCollector { onStreamFinished: { if (text.trim() !== "") { - console.warn("Error enabling WiFi:", text) + Logger.warn("Network", "Error enabling WiFi:", text) } } } @@ -235,7 +235,7 @@ Singleton { stderr: StdioCollector { onStreamFinished: { if (text.trim() !== "") { - console.warn("Error disabling WiFi:", text) + Logger.warn("Network", "Error disabling WiFi:", text) } } } @@ -274,7 +274,7 @@ Singleton { const parts = line.split(":") if (parts.length < 2) { - console.warn("Malformed nmcli output line:", line) + Logger.warn("Network", "Malformed nmcli output line:", line) continue } @@ -313,7 +313,7 @@ Singleton { const parts = line.split(":") if (parts.length < 4) { - console.warn("Malformed nmcli output line:", line) + Logger.warn("Network", "Malformed nmcli output line:", line) continue } const ssid = parts[0] diff --git a/Services/NiriService.qml b/Services/NiriService.qml index 992991e..4a8f403 100644 --- a/Services/NiriService.qml +++ b/Services/NiriService.qml @@ -63,7 +63,7 @@ Singleton { root.workspaces = workspacesList } catch (e) { - console.error("Failed to parse workspaces:", e, line) + Logger.error("NiriService", "Failed to parse workspaces:", e, line) } } } @@ -104,7 +104,7 @@ Singleton { } } } catch (e) { - console.error("Error parsing windows event:", e) + Logger.error("NiriService", "Error parsing windows event:", e) } } else if (event.WorkspaceActivated) { workspaceProcess.running = true @@ -120,17 +120,17 @@ Singleton { root.focusedWindowIndex = -1 } } catch (e) { - console.error("Error parsing window focus event:", e) + Logger.error("NiriService", "Error parsing window focus event:", e) } } else if (event.OverviewOpenedOrClosed) { try { root.inOverview = event.OverviewOpenedOrClosed.is_open === true } catch (e) { - console.error("Error parsing overview state:", e) + Logger.error("NiriService", "Error parsing overview state:", e) } } } catch (e) { - console.error("Error parsing event stream:", e, data) + Logger.error("NiriService", "Error parsing event stream:", e, data) } } } diff --git a/Services/NotificationService.qml b/Services/NotificationService.qml index 65ac02e..3f4f44e 100644 --- a/Services/NotificationService.qml +++ b/Services/NotificationService.qml @@ -165,7 +165,7 @@ QtObject { }) } } catch (e) { - console.error("[Notifications] Failed to load history:", e) + Logger.error("Notifications", "Failed to load history:", e) } } @@ -190,7 +190,7 @@ QtObject { historyFileView.writeAdapter() }) } catch (e) { - console.error("[Notifications] Failed to save history:", e) + Logger.error("Notifications", "Failed to save history:", e) } } diff --git a/Services/ScreenRecorderService.qml b/Services/ScreenRecorderService.qml index ac9d9fa..ae28b67 100644 --- a/Services/ScreenRecorderService.qml +++ b/Services/ScreenRecorderService.qml @@ -34,9 +34,9 @@ Singleton { + " -k " + settings.videoCodec + " -a " + settings.audioSource + " -q " + settings.quality + " -cursor " + (settings.showCursor ? "yes" : "no") + " -cr " + settings.colorRange + " -o " + outputPath - //console.log("[ScreenRecorder]", command) + //Logger.log("ScreenRecorder", command) Quickshell.execDetached(["sh", "-c", command]) - console.log("[ScreenRecorder] Started recording") + Logger.log("ScreenRecorder", "Started recording") } // Stop recording using Quickshell.execDetached @@ -46,7 +46,7 @@ Singleton { } Quickshell.execDetached(["sh", "-c", "pkill -SIGINT -f 'gpu-screen-recorder.*portal'"]) - console.log("[ScreenRecorder] Finished recording:", outputPath) + Logger.log("ScreenRecorder", "Finished recording:", outputPath) // Just in case, force kill after 3 seconds killTimer.running = true diff --git a/Services/WallpapersService.qml b/Services/WallpapersService.qml index 13a8f11..49503c2 100644 --- a/Services/WallpapersService.qml +++ b/Services/WallpapersService.qml @@ -10,7 +10,7 @@ Singleton { id: root Component.onCompleted: { - console.log("[Wallpapers] Service started") + Logger.log("Wallpapers", "Service started") loadWallpapers() // Wallpaper is set when the settings are loaded. @@ -26,7 +26,7 @@ Singleton { property var randomChoices: ["simple", "fade", "left", "right", "top", "bottom", "wipe", "wave", "grow", "center", "any", "outer"] function loadWallpapers() { - console.log("[Wallpapers] Load Wallpapers") + Logger.log("Wallpapers", "Load Wallpapers") scanning = true wallpaperList = [] // Unsetting, then setting the folder will re-trigger the parsing! @@ -35,7 +35,7 @@ Singleton { } function changeWallpaper(path) { - console.log("[Wallpapers] Changing to:", path) + Logger.log("Wallpapers", "Changing to:", path) setCurrentWallpaper(path, false) } @@ -58,7 +58,7 @@ Singleton { } else { // Fallback: update the settings directly for non-SWWW mode - //console.log("[Wallpapers] Not using Swww, setting wallpaper directly") + //Logger.log("Wallpapers", "Not using Swww, setting wallpaper directly") } if (randomWallpaperTimer.running) { @@ -98,7 +98,7 @@ Singleton { function startSWWWDaemon() { if (Settings.data.wallpaper.swww.enabled) { - console.log("[SWWW] Requesting swww-daemon") + Logger.log("Swww", "Requesting swww-daemon") startDaemonProcess.running = true } } @@ -128,7 +128,7 @@ Singleton { } wallpaperList = files scanning = false - console.log("[Wallpapers] List refreshed, count:", wallpaperList.length) + Logger.log("Wallpapers", "List refreshed, count:", wallpaperList.length) } } } @@ -145,10 +145,10 @@ Singleton { } onExited: function (exitCode, exitStatus) { - console.log("[SWWW] Process finished with exit code:", exitCode, "status:", exitStatus) + Logger.log("Swww", "Process finished with exit code:", exitCode, "status:", exitStatus) if (exitCode !== 0) { - console.log("[SWWW] Process failed. Make sure swww-daemon is running with: swww-daemon") - console.log("[SWWW] You can start it with: swww-daemon --format xrgb") + Logger.log("Swww", "Process failed. Make sure swww-daemon is running with: swww-daemon") + Logger.log("Swww", "You can start it with: swww-daemon --format xrgb") } } } @@ -159,15 +159,15 @@ Singleton { running: false onStarted: { - console.log("[SWWW] Daemon start process initiated") + Logger.log("Swww", "Daemon start process initiated") } onExited: function (exitCode, exitStatus) { - console.log("[SWWW] Daemon start process finished with exit code:", exitCode) + Logger.log("Swww", "Daemon start process finished with exit code:", exitCode) if (exitCode === 0) { - console.log("[SWWW] Daemon started successfully") + Logger.log("Swww", "Daemon started successfully") } else { - console.log("[SWWW] Failed to start daemon, may already be running") + Logger.log("Swww", "Failed to start daemon, may already be running") } } } diff --git a/Services/WorkspacesService.qml b/Services/WorkspacesService.qml index ffa50eb..fe031e3 100644 --- a/Services/WorkspacesService.qml +++ b/Services/WorkspacesService.qml @@ -41,7 +41,7 @@ Singleton { return } } catch (e) { - console.error("[Workspaces] Error detecting compositor:", e) + Logger.error("Workspaces", "Error detecting compositor:", e) } } @@ -54,7 +54,7 @@ Singleton { // updateHyprlandWorkspaces(); return true } catch (e) { - console.error("Error initializing Hyprland:", e) + Logger.error("Error initializing Hyprland:", e) isHyprland = false return false } @@ -98,7 +98,7 @@ Singleton { } workspacesChanged() } catch (e) { - console.error("[Workspaces] Error updating Hyprland workspaces:", e) + Logger.error("Workspaces", "Error updating Hyprland workspaces:", e) } } @@ -138,16 +138,16 @@ Singleton { try { Hyprland.dispatch(`workspace ${workspaceId}`) } catch (e) { - console.error("Error switching Hyprland workspace:", e) + Logger.error("Error switching Hyprland workspace:", e) } } else if (isNiri) { try { Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", workspaceId.toString()]) } catch (e) { - console.error("Error switching Niri workspace:", e) + Logger.error("Error switching Niri workspace:", e) } } else { - console.warn("No supported compositor detected for workspace switching") + Logger.warn("No supported compositor detected for workspace switching") } } } diff --git a/Widgets/NLoader.qml b/Widgets/NLoader.qml index 9539d80..322499a 100644 --- a/Widgets/NLoader.qml +++ b/Widgets/NLoader.qml @@ -18,7 +18,7 @@ Loader { sourceComponent: content // onLoaded: { - // console.log("[NLoader] onLoaded:", item.toString()); + // Logger.log("NLoader", "OnLoaded:", item.toString()); // } onActiveChanged: { if (active && item && item.show) { From df9533d4ab6d7c816e7153346f273b0b0a6bc713 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sat, 16 Aug 2025 19:49:42 -0400 Subject: [PATCH 355/394] Even better logging --- Commons/Logger.qml | 7 ++++--- Modules/SettingsPanel/Tabs/ColorSchemeTab.qml | 2 +- Services/WorkspacesService.qml | 12 ++++++------ 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Commons/Logger.qml b/Commons/Logger.qml index 342c16a..dbdb9c4 100644 --- a/Commons/Logger.qml +++ b/Commons/Logger.qml @@ -9,10 +9,11 @@ Singleton { function _formatMessage(...args) { var t = Time.getFormattedTimestamp() if (args.length > 1) { - var module = args.shift() - return `\x1b[36m${t}\x1b[0m \x1b[35m[${module}]\x1b[0m ` + args.join(" ") + const maxLength = 14 + var module = args.shift().substring(0, maxLength).padStart(maxLength, ".") + return `\x1b[36m[${t}]\x1b[0m \x1b[35m${module}\x1b[0m ` + args.join(" ") } else { - return `\x1b[36m${t}\x1b[0m ` + args.join(" ") + return `[\x1b[36m[${t}]\x1b[0m ` + args.join(" ") } } diff --git a/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml b/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml index 9eb8e4e..ed7acbe 100644 --- a/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml +++ b/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml @@ -92,7 +92,7 @@ ColumnLayout { var jsonData = JSON.parse(text()) root.schemeLoaded(schemeName, jsonData) } catch (e) { - Logger.warn("Failed to parse JSON for scheme:", schemeName, e) + Logger.warn("ColorSchemeTab", "Failed to parse JSON for scheme:", schemeName, e) root.schemeLoaded(schemeName, null) // Load defaults on parse error } } diff --git a/Services/WorkspacesService.qml b/Services/WorkspacesService.qml index fe031e3..3ac0598 100644 --- a/Services/WorkspacesService.qml +++ b/Services/WorkspacesService.qml @@ -41,7 +41,7 @@ Singleton { return } } catch (e) { - Logger.error("Workspaces", "Error detecting compositor:", e) + Logger.error("WorkspacesService", "Error detecting compositor:", e) } } @@ -54,7 +54,7 @@ Singleton { // updateHyprlandWorkspaces(); return true } catch (e) { - Logger.error("Error initializing Hyprland:", e) + Logger.error("WorkspacesService", "Error initializing Hyprland:", e) isHyprland = false return false } @@ -98,7 +98,7 @@ Singleton { } workspacesChanged() } catch (e) { - Logger.error("Workspaces", "Error updating Hyprland workspaces:", e) + Logger.error("WorkspacesService", "Error updating Hyprland workspaces:", e) } } @@ -138,16 +138,16 @@ Singleton { try { Hyprland.dispatch(`workspace ${workspaceId}`) } catch (e) { - Logger.error("Error switching Hyprland workspace:", e) + Logger.error("WorkspacesService", "Error switching Hyprland workspace:", e) } } else if (isNiri) { try { Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", workspaceId.toString()]) } catch (e) { - Logger.error("Error switching Niri workspace:", e) + Logger.error("WorkspacesService", "Error switching Niri workspace:", e) } } else { - Logger.warn("No supported compositor detected for workspace switching") + Logger.warn("WorkspacesService", "No supported compositor detected for workspace switching") } } } From ffd9ee8efe4ada6df7ebf62b4d841e325c1b90f1 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sat, 16 Aug 2025 19:52:09 -0400 Subject: [PATCH 356/394] NotificationHistory: each notification needs a proper bg so its easier to understand the UI --- Modules/Notification/NotificationHistoryPanel.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Notification/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml index 959b347..29f8c79 100644 --- a/Modules/Notification/NotificationHistoryPanel.qml +++ b/Modules/Notification/NotificationHistoryPanel.qml @@ -197,7 +197,7 @@ NLoader { width: notificationList ? (notificationList.width - 20) : 380 * scaling height: Math.max(80, notificationContent.height + 30) radius: Style.radiusMedium * scaling - color: notificationMouseArea.containsMouse ? Colors.mPrimary : Colors.mSurface + color: notificationMouseArea.containsMouse ? Colors.mPrimary : Colors.mSurfaceVariant RowLayout { anchors { From 4fcdb1543d8a3eeb59f3057241d32eb11f77da3d Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sat, 16 Aug 2025 22:39:11 -0400 Subject: [PATCH 357/394] Renamed Colors to Color - some work on the active window --- Commons/{Colors.qml => Color.qml} | 2 + Commons/Settings.qml | 6 +- Commons/Style.qml | 1 + Modules/AppLauncher/AppLauncher.qml | 49 ++++--- Modules/Audio/CircularSpectrum.qml | 4 +- Modules/Audio/LinearSpectrum.qml | 4 +- Modules/Background/Overview.qml | 2 +- Modules/Background/ScreenCorners.qml | 2 +- Modules/Bar/ActiveWindow.qml | 124 ++++++++++-------- Modules/Bar/Bar.qml | 6 +- Modules/Bar/Battery.qml | 8 +- Modules/Bar/BluetoothMenu.qml | 38 +++--- Modules/Bar/Brightness.qml | 6 +- Modules/Bar/MediaMini.qml | 6 +- Modules/Bar/NotificationHistory.qml | 1 + Modules/Bar/SystemMonitor.qml | 13 +- Modules/Bar/Tray.qml | 1 + Modules/Bar/TrayMenu.qml | 20 +-- Modules/Bar/Volume.qml | 4 +- Modules/Bar/WiFiMenu.qml | 48 +++---- Modules/Bar/Workspace.qml | 22 ++-- Modules/Calendar/Calendar.qml | 12 +- Modules/Demo/DemoPanel.qml | 22 ++-- Modules/Dock/Dock.qml | 8 +- Modules/LockScreen/LockScreen.qml | 69 +++++----- Modules/Notification/Notification.qml | 14 +- .../Notification/NotificationHistoryPanel.qml | 22 ++-- Modules/SettingsPanel/SettingsPanel.qml | 18 +-- Modules/SettingsPanel/Tabs/AboutTab.qml | 30 ++--- Modules/SettingsPanel/Tabs/AudioTab.qml | 20 +-- Modules/SettingsPanel/Tabs/BarTab.qml | 22 +++- Modules/SettingsPanel/Tabs/BrightnessTab.qml | 38 +++--- Modules/SettingsPanel/Tabs/ColorSchemeTab.qml | 10 +- Modules/SettingsPanel/Tabs/DisplayTab.qml | 12 +- Modules/SettingsPanel/Tabs/GeneralTab.qml | 6 +- Modules/SettingsPanel/Tabs/NetworkTab.qml | 2 +- .../SettingsPanel/Tabs/ScreenRecorderTab.qml | 6 +- Modules/SettingsPanel/Tabs/TimeWeatherTab.qml | 6 +- .../Tabs/WallpaperSelectorTab.qml | 36 ++--- Modules/SettingsPanel/Tabs/WallpaperTab.qml | 24 ++-- Modules/SidePanel/Cards/MediaCard.qml | 40 +++--- Modules/SidePanel/Cards/ProfileCard.qml | 4 +- Modules/SidePanel/Cards/WeatherCard.qml | 8 +- Modules/SidePanel/PowerMenu.qml | 34 ++--- Modules/SidePanel/SidePanel.qml | 4 +- Widgets/NBox.qml | 4 +- Widgets/NBusyIndicator.qml | 2 +- Widgets/NCard.qml | 4 +- Widgets/NCircleStat.qml | 16 +-- Widgets/NComboBox.qml | 16 +-- Widgets/NDivider.qml | 2 +- Widgets/NIconButton.qml | 6 +- Widgets/NPanel.qml | 2 +- Widgets/NPill.qml | 10 +- Widgets/NRadioButton.qml | 4 +- Widgets/NSlider.qml | 14 +- Widgets/NText.qml | 2 +- Widgets/NTextInput.qml | 14 +- Widgets/NToggle.qml | 12 +- Widgets/NTooltip.qml | 4 +- 60 files changed, 491 insertions(+), 455 deletions(-) rename Commons/{Colors.qml => Color.qml} (99%) diff --git a/Commons/Colors.qml b/Commons/Color.qml similarity index 99% rename from Commons/Colors.qml rename to Commons/Color.qml index a040d94..5830d4a 100644 --- a/Commons/Colors.qml +++ b/Commons/Color.qml @@ -34,6 +34,8 @@ Singleton { property color mOutlineVariant: customColors.mOutlineVariant property color mShadow: customColors.mShadow + property color transparent: "transparent" + // ----------- function applyOpacity(color, opacity) { // Convert color to string and apply opacity diff --git a/Commons/Settings.qml b/Commons/Settings.qml index faeabd8..51a5a55 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -49,7 +49,7 @@ Singleton { reload() } onLoaded: function () { - Logger.log("Settings", "Loaded") + Logger.log("Settings", "OnLoaded") Qt.callLater(function () { // Only set wallpaper on initial load, not on reloads if (isInitialLoad && adapter.wallpaper.current !== "") { @@ -75,7 +75,9 @@ Singleton { property bool showActiveWindow: true property bool showSystemInfo: false property bool showMedia: false - property bool hideBrightness: false + property bool showBrightness: true + property bool showNotificationsHistory: true + property bool showTray: true property list monitors: [] } diff --git a/Commons/Style.qml b/Commons/Style.qml index c3f56a6..db80b1d 100644 --- a/Commons/Style.qml +++ b/Commons/Style.qml @@ -15,6 +15,7 @@ Singleton { // Font size property real fontSizeTiny: 7 property real fontSizeSmall: 9 + property real fontSizeReduced: 10 property real fontSizeMedium: 11 property real fontSizeInter: 12 property real fontSizeLarge: 13 diff --git a/Modules/AppLauncher/AppLauncher.qml b/Modules/AppLauncher/AppLauncher.qml index 8652e47..31c9b10 100644 --- a/Modules/AppLauncher/AppLauncher.qml +++ b/Modules/AppLauncher/AppLauncher.qml @@ -255,19 +255,19 @@ NLoader { width: Math.min(700 * scaling, parent.width * 0.75) height: Math.min(550 * scaling, parent.height * 0.8) radius: Style.radiusLarge * scaling - color: Colors.mSurface - border.color: Colors.mOutline + color: Color.mSurface + border.color: Color.mOutline border.width: Style.borderThin * scaling // Subtle gradient background gradient: Gradient { GradientStop { position: 0.0 - color: Qt.lighter(Colors.mSurface, 1.02) + color: Qt.lighter(Color.mSurface, 1.02) } GradientStop { position: 1.0 - color: Qt.darker(Colors.mSurface, 1.1) + color: Qt.darker(Color.mSurface, 1.1) } } @@ -282,8 +282,8 @@ NLoader { Layout.preferredHeight: Style.barHeight * scaling Layout.bottomMargin: Style.marginMedium * scaling radius: Style.radiusMedium * scaling - color: Colors.mSurface - border.color: searchInput.activeFocus ? Colors.mPrimary : Colors.mOutline + color: Color.mSurface + border.color: searchInput.activeFocus ? Color.mPrimary : Color.mOutline border.width: Math.max( 1, searchInput.activeFocus ? Style.borderMedium * scaling : Style.borderThin * scaling) @@ -297,14 +297,14 @@ NLoader { text: "search" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeLarger * scaling - color: searchInput.activeFocus ? Colors.mPrimary : Colors.mOnSurface + color: searchInput.activeFocus ? Color.mPrimary : Color.mOnSurface } TextField { id: searchInput placeholderText: "Search applications..." - color: Colors.mOnSurface - placeholderTextColor: Colors.mOnSurface + color: Color.mOnSurface + placeholderTextColor: Color.mOnSurface background: null font.pointSize: Style.fontSizeLarge * scaling Layout.fillWidth: true @@ -312,8 +312,8 @@ NLoader { searchText = text selectedIndex = 0 // Reset selection when search changes } - selectedTextColor: Colors.mOnSurface - selectionColor: Colors.mPrimary + selectedTextColor: Color.mOnSurface + selectionColor: Color.mPrimary padding: 0 verticalAlignment: TextInput.AlignVCenter leftPadding: 0 @@ -322,14 +322,14 @@ NLoader { bottomPadding: 0 font.bold: true Component.onCompleted: { - contentItem.cursorColor = Colors.mOnSurface + contentItem.cursorColor = Color.mOnSurface contentItem.verticalAlignment = TextInput.AlignVCenter // Focus the search bar by default Qt.callLater(() => { searchInput.forceActiveFocus() }) } - onActiveFocusChanged: contentItem.cursorColor = Colors.mOnSurface + onActiveFocusChanged: contentItem.cursorColor = Color.mOnSurface Keys.onDownPressed: selectNext() Keys.onUpPressed: selectPrev() @@ -372,9 +372,9 @@ NLoader { height: 65 * scaling radius: Style.radiusMedium * scaling property bool isSelected: index === selectedIndex - color: (appCardArea.containsMouse || isSelected) ? Qt.darker(Colors.mPrimary, - 1.1) : Colors.mSurface - border.color: (appCardArea.containsMouse || isSelected) ? Colors.mPrimary : "transparent" + color: (appCardArea.containsMouse || isSelected) ? Qt.darker(Color.mPrimary, + 1.1) : Color.mSurface + border.color: (appCardArea.containsMouse || isSelected) ? Color.mPrimary : "transparent" border.width: Math.max(1, (appCardArea.containsMouse || isSelected) ? Style.borderMedium * scaling : 0) @@ -406,8 +406,8 @@ NLoader { Layout.preferredWidth: Style.baseWidgetSize * 1.25 * scaling Layout.preferredHeight: Style.baseWidgetSize * 1.25 * scaling radius: Style.radiusSmall * scaling - color: appCardArea.containsMouse ? Qt.darker(Colors.mPrimary, - 1.1) : Colors.mSurfaceVariant + color: appCardArea.containsMouse ? Qt.darker(Color.mPrimary, + 1.1) : Color.mSurfaceVariant property bool iconLoaded: (modelData.isCalculator || modelData.isClipboard || modelData.isCommand) || (iconImg.status === Image.Ready && iconImg.source !== "" @@ -442,7 +442,7 @@ NLoader { anchors.fill: parent anchors.margins: Style.marginTiny * scaling radius: Style.radiusTiny * scaling - color: Colors.mPrimary + color: Color.mPrimary opacity: Style.opacityMedium visible: !parent.iconLoaded } @@ -454,7 +454,7 @@ NLoader { text: modelData.name ? modelData.name.charAt(0).toUpperCase() : "?" font.pointSize: Style.fontSizeXL * scaling font.weight: Font.Bold - color: Colors.mPrimary + color: Color.mPrimary } Behavior on color { @@ -473,7 +473,7 @@ NLoader { text: modelData.name || "Unknown" font.pointSize: Style.fontSizeLarge * scaling font.weight: Font.Bold - color: Colors.mOnSurface + color: Color.mOnSurface elide: Text.ElideRight Layout.fillWidth: true } @@ -481,8 +481,7 @@ NLoader { NText { text: modelData.isCalculator ? (modelData.expr + " = " + modelData.result) : modelData.isClipboard ? modelData.content : modelData.isCommand ? modelData.content : (modelData.genericName || modelData.comment || "") font.pointSize: Style.fontSizeMedium * scaling - color: (appCardArea.containsMouse - || isSelected) ? Colors.mOnSurface : Colors.mOnSurface + color: (appCardArea.containsMouse || isSelected) ? Color.mOnSurface : Color.mOnSurface elide: Text.ElideRight Layout.fillWidth: true visible: text !== "" @@ -509,7 +508,7 @@ NLoader { NText { text: searchText.trim() !== "" ? "No applications found" : "No applications available" font.pointSize: Style.fontSizeLarge * scaling - color: Colors.mOnSurface + color: Color.mOnSurface horizontalAlignment: Text.AlignHCenter Layout.fillWidth: true visible: filteredEntries.length === 0 @@ -523,7 +522,7 @@ NLoader { ">calc") ? `${filteredEntries.length} result${filteredEntries.length !== 1 ? 's' : ''}` : `${filteredEntries.length} application${filteredEntries.length !== 1 ? 's' : ''}` font.pointSize: Style.fontSizeSmall * scaling - color: Colors.mOnSurface + color: Color.mOnSurface horizontalAlignment: Text.AlignHCenter Layout.fillWidth: true visible: searchText.trim() !== "" diff --git a/Modules/Audio/CircularSpectrum.qml b/Modules/Audio/CircularSpectrum.qml index efb6f7a..e4d1754 100644 --- a/Modules/Audio/CircularSpectrum.qml +++ b/Modules/Audio/CircularSpectrum.qml @@ -7,8 +7,8 @@ Item { id: root property int innerRadius: 32 * scaling property int outerRadius: 64 * scaling - property color fillColor: Colors.mPrimary - property color strokeColor: Colors.mOnSurface + property color fillColor: Color.mPrimary + property color strokeColor: Color.mOnSurface property int strokeWidth: 0 * scaling property var values: [] property int usableOuter: 64 diff --git a/Modules/Audio/LinearSpectrum.qml b/Modules/Audio/LinearSpectrum.qml index 32768b3..ea8ef71 100644 --- a/Modules/Audio/LinearSpectrum.qml +++ b/Modules/Audio/LinearSpectrum.qml @@ -4,8 +4,8 @@ import qs.Services Item { id: root - property color fillColor: Colors.mPrimary - property color strokeColor: Colors.mOnSurface + property color fillColor: Color.mPrimary + property color strokeColor: Color.mOnSurface property int strokeWidth: 0 property var values: [] diff --git a/Modules/Background/Overview.qml b/Modules/Background/Overview.qml index a2e7e10..c3bea3a 100644 --- a/Modules/Background/Overview.qml +++ b/Modules/Background/Overview.qml @@ -63,7 +63,7 @@ NLoader { Rectangle { anchors.fill: parent - color: Qt.rgba(Colors.mSurface.r, Colors.mSurface.g, Colors.mSurface.b, 0.5) + color: Qt.rgba(Color.mSurface.r, Color.mSurface.g, Color.mSurface.b, 0.5) } } } diff --git a/Modules/Background/ScreenCorners.qml b/Modules/Background/ScreenCorners.qml index 0ddf297..185eb77 100644 --- a/Modules/Background/ScreenCorners.qml +++ b/Modules/Background/ScreenCorners.qml @@ -20,7 +20,7 @@ NLoader { screen: modelData // Visible ring color - property color ringColor: Colors.mSurface + property color ringColor: Color.mSurface // The amount subtracted from full size for the inner cutout // Inner size = full size - borderWidth (per axis) property int borderWidth: Style.borderMedium diff --git a/Modules/Bar/ActiveWindow.qml b/Modules/Bar/ActiveWindow.qml index 7fd9cfb..62550db 100644 --- a/Modules/Bar/ActiveWindow.qml +++ b/Modules/Bar/ActiveWindow.qml @@ -1,5 +1,6 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import Quickshell import qs.Commons import qs.Services @@ -39,57 +40,68 @@ Row { } } - // Window icon - NText { - id: windowIcon - text: "desktop_windows" - font.family: "Material Symbols Outlined" - font.pointSize: Style.fontSizeLarge * scaling - verticalAlignment: Text.AlignVCenter - anchors.verticalCenter: parent.verticalCenter - color: Colors.mPrimary - visible: getDisplayText() !== "" - } - - // Window title container - Item { - id: titleContainer - width: titleText.width - height: titleText.height + Rectangle { + width: row.width + Style.marginSmall * scaling * 2 + height: row.height + color: Color.mSurfaceVariant + radius: Style.radiusSmall * scaling anchors.verticalCenter: parent.verticalCenter - Behavior on width { - NumberAnimation { - duration: Style.animationNormal - easing.type: Easing.OutCubic - } - } - - NText { - id: titleText - text: getDisplayText() - font.pointSize: Style.fontSizeSmall * scaling - font.weight: Style.fontWeightBold - elide: Text.ElideRight - anchors.verticalCenter: parent.verticalCenter - verticalAlignment: Text.AlignVCenter - } - - // Mouse area for hover detection - MouseArea { - id: titleContainerMouseArea + Item { + id: mainContainer anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.IBeamCursor - onEntered: { - titleText.text = getDisplayText() + anchors.leftMargin: Style.marginSmall * scaling + anchors.rightMargin: Style.marginSmall * scaling + + Row { + id: row + anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginTiny * scaling + + // Window icon + NText { + id: windowIcon + text: "dialogs" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeLarge * scaling + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + visible: getDisplayText() !== "" + } + + NText { + id: titleText + width: (showingFullTitle || mouseArea.containsMouse) ? 300 * scaling : 100 * scaling + text: getDisplayText() + font.pointSize: Style.fontSizeReduced * scaling + font.weight: Style.fontWeightBold + elide: Text.ElideRight + anchors.verticalCenter: parent.verticalCenter + verticalAlignment: Text.AlignVCenter + color: Color.mTertiary + Behavior on width { + NumberAnimation { + duration: Style.animationSlow + easing.type: Easing.OutBack + } + } + } } - onExited: { - titleText.text = getDisplayText() + // Mouse area for hover detection + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + onEntered: { + titleText.text = getDisplayText() + } + onExited: { + titleText.text = getDisplayText() + } } } } - function getDisplayText() { // Check if Niri service is available if (typeof NiriService === "undefined") { @@ -117,27 +129,29 @@ Row { const programName = title.split(/[\s\-_]/)[0] if (programName.length <= 2 || programName === title) { - return truncateTitle(title) + return title } - if (showingFullTitle || titleContainerMouseArea.containsMouse || isGenericName(programName)) { - return truncateTitle(title) + if (isGenericName(programName)) { + return title } return programName } - // Use appId for program name, show full title on hover or window switch - if (showingFullTitle || titleContainerMouseArea.containsMouse) { - return truncateTitle(title || appId) - } + return title - return appId + // // Use appId for program name, show full title on hover or window switch + // if (showingFullTitle || mouseArea.containsMouse) { + // return truncateTitle(title || appId, 50) + // } else { + // return truncateTitle(title || appId, 20) + // } } - function truncateTitle(title) { - if (title.length > 50) { - return title.substring(0, 47) + "..." + function truncateTitle(title, length) { + if (title.length > length) { + return title.substring(0, length - 3) + "..." } return title } diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 32d438c..a3388a0 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -39,7 +39,7 @@ Variants { id: bar anchors.fill: parent - color: Colors.mSurface + color: Color.mSurface layer.enabled: true } @@ -55,9 +55,9 @@ Variants { SystemMonitor {} - MediaMini {} - ActiveWindow {} + + MediaMini {} } // Center diff --git a/Modules/Bar/Battery.qml b/Modules/Bar/Battery.qml index 59d8ed3..dc9fba1 100644 --- a/Modules/Bar/Battery.qml +++ b/Modules/Bar/Battery.qml @@ -52,10 +52,10 @@ NPill { icon: root.batteryIcon() text: Math.round(root.percent) + "%" - pillColor: Colors.mSurfaceVariant - iconCircleColor: Colors.mPrimary - iconTextColor: Colors.mSurface - textColor: charging ? Colors.mPrimary : Colors.mOnSurface + pillColor: Color.mSurfaceVariant + iconCircleColor: Color.mPrimary + iconTextColor: Color.mSurface + textColor: charging ? Color.mPrimary : Color.mOnSurface tooltipText: { let lines = [] diff --git a/Modules/Bar/BluetoothMenu.qml b/Modules/Bar/BluetoothMenu.qml index 560d929..bff9ecf 100644 --- a/Modules/Bar/BluetoothMenu.qml +++ b/Modules/Bar/BluetoothMenu.qml @@ -63,9 +63,9 @@ NLoader { Rectangle { id: bluetoothMenuRect - color: Colors.mSurface + color: Color.mSurface radius: Style.radiusLarge * scaling - border.color: Colors.mOutlineVariant + border.color: Color.mOutlineVariant border.width: Math.max(1, Style.borderThin * scaling) width: 340 * scaling height: 500 * scaling @@ -115,14 +115,14 @@ NLoader { text: "bluetooth" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXL * scaling - color: Colors.mPrimary + color: Color.mPrimary } NText { text: "Bluetooth" font.pointSize: Style.fontSizeLarge * scaling font.bold: true - color: Colors.mOnSurface + color: Color.mOnSurface Layout.fillWidth: true } @@ -160,7 +160,7 @@ NLoader { NBusyIndicator { running: BluetoothService.isDiscovering - color: Colors.mPrimary + color: Color.mPrimary size: Style.baseWidgetSize * scaling Layout.alignment: Qt.AlignHCenter } @@ -168,7 +168,7 @@ NLoader { NText { text: "Scanning for devices..." font.pointSize: Style.fontSizeNormal * scaling - color: Colors.mOnSurfaceVariant + color: Color.mOnSurfaceVariant Layout.alignment: Qt.AlignHCenter } } @@ -183,21 +183,21 @@ NLoader { text: "bluetooth_disabled" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXXL * scaling - color: Colors.mOnSurfaceVariant + color: Color.mOnSurfaceVariant Layout.alignment: Qt.AlignHCenter } NText { text: "Bluetooth is disabled" font.pointSize: Style.fontSizeLarge * scaling - color: Colors.mOnSurfaceVariant + color: Color.mOnSurfaceVariant Layout.alignment: Qt.AlignHCenter } NText { text: "Enable Bluetooth to see available devices" font.pointSize: Style.fontSizeNormal * scaling - color: Colors.mOnSurfaceVariant + color: Color.mOnSurfaceVariant Layout.alignment: Qt.AlignHCenter } } @@ -271,7 +271,7 @@ NLoader { Rectangle { anchors.fill: parent radius: Style.radiusMedium * scaling - color: modelData.device.connected ? Colors.mPrimary : (deviceMouseArea.containsMouse ? Colors.mTertiary : "transparent") + color: modelData.device.connected ? Color.mPrimary : (deviceMouseArea.containsMouse ? Color.mTertiary : "transparent") RowLayout { anchors.fill: parent @@ -282,7 +282,7 @@ NLoader { text: BluetoothService.getDeviceIcon(modelData.device) font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXL * scaling - color: modelData.device.connected ? Colors.mSurface : (deviceMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurface) + color: modelData.device.connected ? Color.mSurface : (deviceMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface) } ColumnLayout { @@ -294,7 +294,7 @@ NLoader { font.pointSize: Style.fontSizeNormal * scaling elide: Text.ElideRight Layout.fillWidth: true - color: modelData.device.connected ? Colors.mSurface : (deviceMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurface) + color: modelData.device.connected ? Color.mSurface : (deviceMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface) } NText { @@ -308,13 +308,13 @@ NLoader { } } font.pointSize: Style.fontSizeSmall * scaling - color: modelData.device.connected ? Colors.mSurface : (deviceMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurfaceVariant) + color: modelData.device.connected ? Color.mSurface : (deviceMouseArea.containsMouse ? Color.mSurface : Color.mOnSurfaceVariant) } NText { text: BluetoothService.getBatteryText(modelData.device) font.pointSize: Style.fontSizeSmall * scaling - color: modelData.device.connected ? Colors.mSurface : (deviceMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurfaceVariant) + color: modelData.device.connected ? Color.mSurface : (deviceMouseArea.containsMouse ? Color.mSurface : Color.mOnSurfaceVariant) visible: modelData.device.batteryAvailable } } @@ -327,7 +327,7 @@ NLoader { NBusyIndicator { visible: modelData.device.pairing || modelData.device.state === 2 running: modelData.device.pairing || modelData.device.state === 2 - color: Colors.mPrimary + color: Color.mPrimary anchors.centerIn: parent size: Style.baseWidgetSize * 0.7 * scaling } @@ -337,7 +337,7 @@ NLoader { visible: modelData.device.connected text: "connected" font.pointSize: Style.fontSizeSmall * scaling - color: modelData.device.connected ? Colors.mSurface : (deviceMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurface) + color: modelData.device.connected ? Color.mSurface : (deviceMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface) } } @@ -370,21 +370,21 @@ NLoader { text: "bluetooth_disabled" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXXL * scaling - color: Colors.mOnSurfaceVariant + color: Color.mOnSurfaceVariant Layout.alignment: Qt.AlignHCenter } NText { text: "No Bluetooth devices" font.pointSize: Style.fontSizeLarge * scaling - color: Colors.mOnSurfaceVariant + color: Color.mOnSurfaceVariant Layout.alignment: Qt.AlignHCenter } NText { text: "Click the refresh button to discover devices" font.pointSize: Style.fontSizeNormal * scaling - color: Colors.mOnSurfaceVariant + color: Color.mOnSurfaceVariant Layout.alignment: Qt.AlignHCenter } } diff --git a/Modules/Bar/Brightness.qml b/Modules/Bar/Brightness.qml index e1c1319..a065937 100644 --- a/Modules/Bar/Brightness.qml +++ b/Modules/Bar/Brightness.qml @@ -10,7 +10,7 @@ Item { width: pill.width height: pill.height - visible: !Settings.data.bar.hideBrightness + visible: Settings.data.bar.showBrightness // Used to avoid opening the pill on Quickshell startup property bool firstBrightnessReceived: false @@ -49,8 +49,8 @@ Item { NPill { id: pill icon: getIcon() - iconCircleColor: Colors.mPrimary - collapsedIconColor: Colors.mOnSurface + iconCircleColor: Color.mPrimary + collapsedIconColor: Color.mOnSurface autoHide: false // Important to be false so we can hover as long as we want text: Math.round(BrightnessService.brightness) + "%" tooltipText: "Brightness: " + Math.round( diff --git a/Modules/Bar/MediaMini.qml b/Modules/Bar/MediaMini.qml index bc7eae4..86e4bc8 100644 --- a/Modules/Bar/MediaMini.qml +++ b/Modules/Bar/MediaMini.qml @@ -28,7 +28,6 @@ Item { font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeLarge * scaling verticalAlignment: Text.AlignVCenter - color: Colors.mPrimary MouseArea { id: titleContainerMouseArea @@ -43,11 +42,10 @@ Item { // Track info NText { text: MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - {MediaService.trackArtist}` : "") - color: Colors.mOnSurface - font.pointSize: Style.fontSizeSmall * scaling + font.pointSize: Style.fontSizeReduced * scaling font.weight: Style.fontWeightBold elide: Text.ElideRight - + color: Color.mSecondary verticalAlignment: Text.AlignVCenter Layout.maximumWidth: 200 * scaling Layout.alignment: Qt.AlignVCenter diff --git a/Modules/Bar/NotificationHistory.qml b/Modules/Bar/NotificationHistory.qml index d9fb9cc..ad6f6bd 100644 --- a/Modules/Bar/NotificationHistory.qml +++ b/Modules/Bar/NotificationHistory.qml @@ -10,6 +10,7 @@ import qs.Widgets NIconButton { id: root + visible: Settings.data.bar.showNotificationsHistory sizeMultiplier: 0.8 showBorder: false icon: "notifications" diff --git a/Modules/Bar/SystemMonitor.qml b/Modules/Bar/SystemMonitor.qml index 13d5a26..0946aa4 100644 --- a/Modules/Bar/SystemMonitor.qml +++ b/Modules/Bar/SystemMonitor.qml @@ -24,16 +24,16 @@ Row { font.pointSize: Style.fontSizeLarge * scaling verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter - color: Colors.mPrimary } NText { id: cpuUsageText text: `${SystemStatsService.cpuUsage}%` - font.pointSize: Style.fontSizeSmall * scaling + font.pointSize: Style.fontSizeReduced * scaling font.weight: Style.fontWeightBold anchors.verticalCenter: parent.verticalCenter verticalAlignment: Text.AlignVCenter + color: Color.mPrimary } } @@ -46,17 +46,18 @@ Row { text: "thermometer" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeLarge * scaling - color: Colors.mPrimary + verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter } NText { text: `${SystemStatsService.cpuTemp}°C` - font.pointSize: Style.fontSizeSmall * scaling + font.pointSize: Style.fontSizeReduced * scaling font.weight: Style.fontWeightBold anchors.verticalCenter: parent.verticalCenter verticalAlignment: Text.AlignVCenter + color: Color.mPrimary } } @@ -69,17 +70,17 @@ Row { text: "memory" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeLarge * scaling - color: Colors.mPrimary verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter } NText { text: `${SystemStatsService.memoryUsageGb}G` - font.pointSize: Style.fontSizeSmall * scaling + font.pointSize: Style.fontSizeReduced * scaling font.weight: Style.fontWeightBold anchors.verticalCenter: parent.verticalCenter verticalAlignment: Text.AlignVCenter + color: Color.mPrimary } } } diff --git a/Modules/Bar/Tray.qml b/Modules/Bar/Tray.qml index 76f78bf..83caea2 100644 --- a/Modules/Bar/Tray.qml +++ b/Modules/Bar/Tray.qml @@ -12,6 +12,7 @@ import qs.Widgets Item { readonly property real itemSize: 24 * scaling + visible: Settings.data.bar.showTray width: tray.width height: itemSize diff --git a/Modules/Bar/TrayMenu.qml b/Modules/Bar/TrayMenu.qml index a190a9f..5e342b2 100644 --- a/Modules/Bar/TrayMenu.qml +++ b/Modules/Bar/TrayMenu.qml @@ -73,8 +73,8 @@ PopupWindow { Rectangle { id: bg anchors.fill: parent - color: Colors.mSurface - border.color: Colors.mOutline + color: Color.mSurface + border.color: Color.mOutline border.width: Math.max(1, Style.borderThin * scaling) radius: Style.radiusMedium * scaling z: 0 @@ -112,7 +112,7 @@ PopupWindow { Rectangle { id: bg anchors.fill: parent - color: mouseArea.containsMouse ? Colors.mTertiary : "transparent" + color: mouseArea.containsMouse ? Color.mTertiary : "transparent" radius: Style.radiusSmall * scaling visible: !(modelData?.isSeparator ?? false) @@ -126,7 +126,7 @@ PopupWindow { id: text Layout.fillWidth: true color: (modelData?.enabled - ?? true) ? (mouseArea.containsMouse ? Colors.mOnTertiary : Colors.mOnSurface) : Colors.textDisabled + ?? true) ? (mouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface) : Color.textDisabled text: modelData?.text ?? "" font.pointSize: Style.fontSizeSmall * scaling verticalAlignment: Text.AlignVCenter @@ -148,7 +148,7 @@ PopupWindow { font.pointSize: Style.fontSizeSmall * scaling verticalAlignment: Text.AlignVCenter visible: modelData?.hasChildren ?? false - color: Colors.mOnSurface + color: Color.mOnSurface } } @@ -308,8 +308,8 @@ PopupWindow { Rectangle { id: bg anchors.fill: parent - color: Colors.mSurface - border.color: Colors.mOutline + color: Color.mSurface + border.color: Color.mOutline border.width: Math.max(1, Style.borderThin * scaling) radius: Style.radiusMedium * scaling z: 0 @@ -347,10 +347,10 @@ PopupWindow { Rectangle { id: bg anchors.fill: parent - color: mouseArea.containsMouse ? Colors.mTertiary : "transparent" + color: mouseArea.containsMouse ? Color.mTertiary : "transparent" radius: Style.radiusSmall * scaling visible: !(modelData?.isSeparator ?? false) - property color hoverTextColor: mouseArea.containsMouse ? Colors.mOnSurface : Colors.mOnSurface + property color hoverTextColor: mouseArea.containsMouse ? Color.mOnSurface : Color.mOnSurface RowLayout { anchors.fill: parent @@ -361,7 +361,7 @@ PopupWindow { NText { id: subText Layout.fillWidth: true - color: (modelData?.enabled ?? true) ? bg.hoverTextColor : Colors.textDisabled + color: (modelData?.enabled ?? true) ? bg.hoverTextColor : Color.textDisabled text: modelData?.text ?? "" font.pointSize: Style.fontSizeSmall * scaling verticalAlignment: Text.AlignVCenter diff --git a/Modules/Bar/Volume.qml b/Modules/Bar/Volume.qml index a6d364b..8eb6196 100644 --- a/Modules/Bar/Volume.qml +++ b/Modules/Bar/Volume.qml @@ -49,8 +49,8 @@ Item { NPill { id: pill icon: getIcon() - iconCircleColor: Colors.mPrimary - collapsedIconColor: Colors.mOnSurface + iconCircleColor: Color.mPrimary + collapsedIconColor: Color.mOnSurface autoHide: false // Important to be false so we can hover as long as we want text: Math.floor(AudioService.volume * 100) + "%" tooltipText: "Volume: " + Math.round( diff --git a/Modules/Bar/WiFiMenu.qml b/Modules/Bar/WiFiMenu.qml index 807cf35..d16c020 100644 --- a/Modules/Bar/WiFiMenu.qml +++ b/Modules/Bar/WiFiMenu.qml @@ -82,9 +82,9 @@ NLoader { Rectangle { id: wifiMenuRect - color: Colors.mSurface + color: Color.mSurface radius: Style.radiusLarge * scaling - border.color: Colors.mOutlineVariant + border.color: Color.mOutlineVariant border.width: Math.max(1, Style.borderThin * scaling) width: 340 * scaling height: 500 * scaling @@ -134,14 +134,14 @@ NLoader { text: "wifi" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXL * scaling - color: Colors.mPrimary + color: Color.mPrimary } NText { text: "WiFi" font.pointSize: Style.fontSizeLarge * scaling font.bold: true - color: Colors.mOnSurface + color: Color.mOnSurface Layout.fillWidth: true } @@ -179,7 +179,7 @@ NLoader { NBusyIndicator { running: NetworkService.isLoading - color: Colors.mPrimary + color: Color.mPrimary size: Style.baseWidgetSize * scaling Layout.alignment: Qt.AlignHCenter } @@ -187,7 +187,7 @@ NLoader { NText { text: "Scanning for networks..." font.pointSize: Style.fontSizeNormal * scaling - color: Colors.mOnSurfaceVariant + color: Color.mOnSurfaceVariant Layout.alignment: Qt.AlignHCenter } } @@ -202,21 +202,21 @@ NLoader { text: "wifi_off" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXXL * scaling - color: Colors.mOnSurfaceVariant + color: Color.mOnSurfaceVariant Layout.alignment: Qt.AlignHCenter } NText { text: "WiFi is disabled" font.pointSize: Style.fontSizeLarge * scaling - color: Colors.mOnSurfaceVariant + color: Color.mOnSurfaceVariant Layout.alignment: Qt.AlignHCenter } NText { text: "Enable WiFi to see available networks" font.pointSize: Style.fontSizeNormal * scaling - color: Colors.mOnSurfaceVariant + color: Color.mOnSurfaceVariant Layout.alignment: Qt.AlignHCenter } } @@ -243,7 +243,7 @@ NLoader { Layout.fillWidth: true Layout.preferredHeight: Style.baseWidgetSize * 1.5 * scaling radius: Style.radiusMedium * scaling - color: modelData.connected ? Colors.mPrimary : (networkMouseArea.containsMouse ? Colors.mTertiary : "transparent") + color: modelData.connected ? Color.mPrimary : (networkMouseArea.containsMouse ? Color.mTertiary : "transparent") RowLayout { anchors.fill: parent @@ -254,7 +254,7 @@ NLoader { text: NetworkService.signalIcon(modelData.signal) font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXL * scaling - color: modelData.connected ? Colors.mSurface : (networkMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurface) + color: modelData.connected ? Color.mSurface : (networkMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface) } ColumnLayout { @@ -267,7 +267,7 @@ NLoader { font.pointSize: Style.fontSizeNormal * scaling elide: Text.ElideRight Layout.fillWidth: true - color: modelData.connected ? Colors.mSurface : (networkMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurface) + color: modelData.connected ? Color.mSurface : (networkMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface) } // Security Protocol @@ -276,14 +276,14 @@ NLoader { font.pointSize: Style.fontSizeTiny * scaling elide: Text.ElideRight Layout.fillWidth: true - color: modelData.connected ? Colors.mSurface : (networkMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurface) + color: modelData.connected ? Color.mSurface : (networkMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface) } NText { visible: NetworkService.connectStatusSsid === modelData.ssid && NetworkService.connectStatus === "error" && NetworkService.connectError.length > 0 text: NetworkService.connectError - color: Colors.mError + color: Color.mError font.pointSize: Style.fontSizeSmall * scaling elide: Text.ElideRight Layout.fillWidth: true @@ -300,7 +300,7 @@ NLoader { NBusyIndicator { visible: NetworkService.connectingSsid === modelData.ssid running: NetworkService.connectingSsid === modelData.ssid - color: Colors.mPrimary + color: Color.mPrimary anchors.centerIn: parent size: Style.baseWidgetSize * 0.7 * scaling } @@ -321,7 +321,7 @@ NLoader { text: "error" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeSmall * scaling - color: Colors.mError + color: Color.mError anchors.centerIn: parent } } @@ -330,7 +330,7 @@ NLoader { visible: modelData.connected text: "connected" font.pointSize: Style.fontSizeSmall * scaling - color: modelData.connected ? Colors.mSurface : (networkMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurface) + color: modelData.connected ? Color.mSurface : (networkMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface) } } @@ -362,7 +362,7 @@ NLoader { Layout.preferredHeight: modelData.ssid === passwordPromptSsid && showPasswordPrompt ? 60 : 0 Layout.margins: Style.marginSmall * scaling visible: modelData.ssid === passwordPromptSsid && showPasswordPrompt - color: Colors.mSurfaceVariant + color: Color.mSurfaceVariant radius: Style.radiusSmall * scaling RowLayout { @@ -378,7 +378,7 @@ NLoader { anchors.fill: parent radius: Style.radiusTiny * scaling color: "transparent" - border.color: passwordInputField.activeFocus ? Colors.mPrimary : Colors.mOutline + border.color: passwordInputField.activeFocus ? Color.mPrimary : Color.mOutline border.width: Math.max(1, Style.borderThin * scaling) TextInput { @@ -387,7 +387,7 @@ NLoader { anchors.margins: Style.marginMedium * scaling text: passwordInput font.pointSize: Style.fontSizeMedium * scaling - color: Colors.mOnSurface + color: Color.mOnSurface verticalAlignment: TextInput.AlignVCenter clip: true focus: true @@ -414,7 +414,7 @@ NLoader { Layout.preferredWidth: Style.baseWidgetSize * 2.5 * scaling Layout.preferredHeight: Style.barHeight * scaling radius: Style.radiusMedium * scaling - color: Colors.mPrimary + color: Color.mPrimary Behavior on color { ColorAnimation { @@ -425,7 +425,7 @@ NLoader { NText { anchors.centerIn: parent text: "Connect" - color: Colors.mSurface + color: Color.mSurface font.pointSize: Style.fontSizeSmall * scaling } @@ -437,8 +437,8 @@ NLoader { } cursorShape: Qt.PointingHandCursor hoverEnabled: true - onEntered: parent.color = Qt.darker(Colors.mPrimary, 1.1) - onExited: parent.color = Colors.mPrimary + onEntered: parent.color = Qt.darker(Color.mPrimary, 1.1) + onExited: parent.color = Color.mPrimary } } } diff --git a/Modules/Bar/Workspace.qml b/Modules/Bar/Workspace.qml index 54a5858..60499ce 100644 --- a/Modules/Bar/Workspace.qml +++ b/Modules/Bar/Workspace.qml @@ -16,7 +16,7 @@ Item { property ListModel localWorkspaces: ListModel {} property real masterProgress: 0.0 property bool effectsActive: false - property color effectColor: Colors.mPrimary + property color effectColor: Color.mPrimary property int horizontalPadding: Math.round(16 * scaling) property int spacingBetweenPills: Math.round(8 * scaling) @@ -70,7 +70,7 @@ Item { } function triggerUnifiedWave() { - effectColor = Colors.mPrimary + effectColor = Color.mPrimary masterAnimation.restart() } @@ -106,7 +106,7 @@ Item { const ws = localWorkspaces.get(i) if (ws.isFocused === true) { root.triggerUnifiedWave() - root.workspaceChanged(ws.id, Colors.mPrimary) + root.workspaceChanged(ws.id, Color.mPrimary) break } } @@ -119,12 +119,12 @@ Item { anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter radius: Math.round(12 * scaling) - color: Colors.mSurfaceVariant - border.color: Colors.mOutlineVariant + color: Color.mSurfaceVariant + border.color: Color.mOutlineVariant border.width: Math.max(1, Math.round(1 * scaling)) layer.enabled: true layer.effect: MultiEffect { - shadowColor: Colors.mShadow + shadowColor: Color.mShadow shadowVerticalOffset: 0 shadowHorizontalOffset: 0 shadowOpacity: 0.10 @@ -164,15 +164,15 @@ Item { } color: { if (model.isFocused) - return Colors.mPrimary + return Color.mPrimary if (model.isUrgent) - return Colors.mError + return Color.mError if (model.isActive || model.isOccupied) - return Colors.mSecondary + return Color.mSecondary if (model.isUrgent) - return Colors.mError + return Color.mError - return Colors.mOutline + return Color.mOutline } scale: model.isFocused ? 1.0 : 0.9 z: 0 diff --git a/Modules/Calendar/Calendar.qml b/Modules/Calendar/Calendar.qml index ac7071c..b620964 100644 --- a/Modules/Calendar/Calendar.qml +++ b/Modules/Calendar/Calendar.qml @@ -62,9 +62,9 @@ NLoader { Rectangle { id: calendarRect - color: Colors.mSurface + color: Color.mSurface radius: Style.radiusMedium * scaling - border.color: Colors.mOutline + border.color: Color.mOutline border.width: Math.max(1, Style.borderMedium * scaling) width: 340 * scaling height: 320 * scaling // Reduced height to eliminate bottom space @@ -135,7 +135,7 @@ NLoader { horizontalAlignment: Text.AlignHCenter font.pointSize: Style.fontSizeMedium * scaling font.weight: Style.fontWeightBold - color: Colors.mPrimary + color: Color.mPrimary } NIconButton { @@ -173,7 +173,7 @@ NLoader { let dayIndex = (firstDay + index) % 7 return Qt.locale().dayName(dayIndex, Locale.ShortFormat) } - color: Colors.mSecondary + color: Color.mSecondary font.pointSize: Style.fontSizeMedium * scaling font.weight: Style.fontWeightBold horizontalAlignment: Text.AlignHCenter @@ -211,12 +211,12 @@ NLoader { width: (Style.baseWidgetSize * scaling) height: (Style.baseWidgetSize * scaling) radius: Style.radiusSmall * scaling - color: model.today ? Colors.mPrimary : "transparent" + color: model.today ? Color.mPrimary : "transparent" NText { anchors.centerIn: parent text: model.day - color: model.today ? Colors.onAccent : Colors.mOnSurface + color: model.today ? Color.onAccent : Color.mOnSurface opacity: model.month === grid.month ? Style.opacityHeavy : Style.opacityLight font.pointSize: (Style.fontSizeMedium * scaling) font.weight: model.today ? Style.fontWeightBold : Style.fontWeightRegular diff --git a/Modules/Demo/DemoPanel.qml b/Modules/Demo/DemoPanel.qml index d3c0b60..be28857 100644 --- a/Modules/Demo/DemoPanel.qml +++ b/Modules/Demo/DemoPanel.qml @@ -67,9 +67,9 @@ NLoader { Rectangle { id: bgRect - color: Colors.mSurfaceVariant + color: Color.mSurfaceVariant radius: Style.radiusMedium * scaling - border.color: Colors.mOutlineVariant + border.color: Color.mOutlineVariant border.width: Math.max(1, Style.borderThin * scaling) width: 500 * scaling height: 900 * scaling @@ -114,7 +114,7 @@ NLoader { NText { text: "DemoPanel" - color: Colors.mPrimary + color: Color.mPrimary font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold Layout.alignment: Qt.AlignHCenter @@ -129,7 +129,7 @@ NLoader { spacing: Style.marginLarge * scaling NText { text: "Scaling" - color: Colors.mSecondary + color: Color.mSecondary font.weight: Style.fontWeightBold } NText { @@ -173,7 +173,7 @@ NLoader { spacing: Style.marginLarge * scaling NText { text: "NIconButton" - color: Colors.mSecondary + color: Color.mSecondary font.weight: Style.fontWeightBold } @@ -194,7 +194,7 @@ NLoader { spacing: Style.marginMedium * scaling NText { text: "NToggle" - color: Colors.mSecondary + color: Color.mSecondary font.weight: Style.fontWeightBold } @@ -216,7 +216,7 @@ NLoader { spacing: Style.marginMedium * scaling NText { text: "NComboBox" - color: Colors.mSecondary + color: Color.mSecondary font.weight: Style.fontWeightBold } @@ -269,7 +269,7 @@ NLoader { spacing: Style.marginMedium * scaling NText { text: "NTextInput" - color: Colors.mSecondary + color: Color.mSecondary font.weight: Style.fontWeightBold } @@ -292,7 +292,7 @@ NLoader { spacing: Style.marginMedium * scaling NText { text: "NBusyIndicator" - color: Colors.mSecondary + color: Color.mSecondary font.weight: Style.fontWeightBold } @@ -308,7 +308,7 @@ NLoader { spacing: Style.marginMedium * scaling NText { text: "Brightness Control" - color: Colors.mSecondary + color: Color.mSecondary font.weight: Style.fontWeightBold } @@ -349,7 +349,7 @@ NLoader { NText { text: `Method: ${BrightnessService.currentMethod} | Available: ${BrightnessService.available}` - color: Colors.mOnSurfaceVariant + color: Color.mOnSurfaceVariant font.pointSize: Style.fontSizeSmall * scaling Layout.alignment: Qt.AlignHCenter } diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml index 195c927..d873002 100644 --- a/Modules/Dock/Dock.qml +++ b/Modules/Dock/Dock.qml @@ -115,7 +115,7 @@ NLoader { id: dockContainer width: dock.width + 48 * scaling height: iconSize * 1.4 * scaling - color: Colors.mSurface + color: Color.mSurface anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom topLeftRadius: Style.radiusLarge * scaling @@ -194,7 +194,7 @@ NLoader { Rectangle { id: hoverBackground anchors.fill: parent - color: appButton.hovered ? Colors.mSurfaceVariant : "transparent" + color: appButton.hovered ? Color.mSurfaceVariant : "transparent" radius: parent.radius opacity: appButton.hovered ? 0.8 : 0 @@ -236,7 +236,7 @@ NLoader { text: "question_mark" font.family: "Material Symbols Rounded" font.pointSize: iconSize * 0.7 * scaling - color: appButton.isActive ? Colors.mPrimary : Colors.mOnSurfaceVariant + color: appButton.isActive ? Color.mPrimary : Color.mOnSurfaceVariant scale: appButton.hovered ? 1.1 : 1.0 @@ -291,7 +291,7 @@ NLoader { visible: isActive width: iconSize * 0.75 height: 4 * scaling - color: Colors.mPrimary + color: Color.mPrimary radius: Style.radiusTiny anchors.top: parent.bottom anchors.horizontalCenter: parent.horizontalCenter diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index 3e8ab76..d932fb9 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -142,7 +142,7 @@ WlSessionLock { width: Math.random() * 4 + 2 height: width radius: width * 0.5 - color: Qt.rgba(Colors.mPrimary.r, Colors.mPrimary.g, Colors.mPrimary.b, 0.3) + color: Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.3) x: Math.random() * parent.width y: Math.random() * parent.height @@ -185,7 +185,7 @@ WlSessionLock { font.pointSize: Style.fontSizeXXL * 6 font.weight: Font.Bold font.letterSpacing: -2 - color: Colors.mOnSurface + color: Color.mOnSurface horizontalAlignment: Text.AlignHCenter SequentialAnimation on scale { @@ -209,7 +209,7 @@ WlSessionLock { font.family: "Inter" font.pointSize: Style.fontSizeXL font.weight: Font.Light - color: Colors.mOnSurface + color: Color.mOnSurface horizontalAlignment: Text.AlignHCenter width: timeText.width } @@ -226,7 +226,7 @@ WlSessionLock { height: 120 * scaling radius: width * 0.5 color: "transparent" - border.color: Colors.mPrimary + border.color: Color.mPrimary border.width: Math.max(1, Style.borderThick * scaling) anchors.horizontalCenter: parent.horizontalCenter @@ -237,7 +237,7 @@ WlSessionLock { height: parent.height + 24 * scaling radius: width * 0.5 color: "transparent" - border.color: Qt.rgba(Colors.mPrimary.r, Colors.mPrimary.g, Colors.mPrimary.b, 0.3) + border.color: Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.3) border.width: Math.max(1, Style.borderMedium * scaling) z: -1 @@ -305,8 +305,8 @@ WlSessionLock { id: terminalBackground anchors.fill: parent radius: Style.radiusMedium * scaling - color: Colors.applyOpacity(Colors.mSurface, "E6") - border.color: Colors.mPrimary + color: Color.applyOpacity(Color.mSurface, "E6") + border.color: Color.mPrimary border.width: Math.max(1, Style.borderMedium * scaling) // Scanline effect @@ -315,7 +315,7 @@ WlSessionLock { Rectangle { width: parent.width height: 1 - color: Colors.applyOpacity(Colors.mPrimary, "1A") + color: Color.applyOpacity(Color.mPrimary, "1A") y: index * 10 opacity: Style.opacityMedium @@ -337,7 +337,7 @@ WlSessionLock { Rectangle { width: parent.width height: 40 * scaling - color: Colors.applyOpacity(Colors.mPrimary, "33") + color: Color.applyOpacity(Color.mPrimary, "33") topLeftRadius: Style.radiusSmall * scaling topRightRadius: Style.radiusSmall * scaling @@ -348,7 +348,7 @@ WlSessionLock { Text { text: "SECURE TERMINAL" - color: Colors.mOnSurface + color: Color.mOnSurface font.family: "DejaVu Sans Mono" font.pointSize: Style.fontSizeLarge font.weight: Font.Bold @@ -374,7 +374,7 @@ WlSessionLock { Text { text: "root@noctalia:~$" - color: Colors.mPrimary + color: Color.mPrimary font.family: "DejaVu Sans Mono" font.pointSize: Style.fontSizeLarge font.weight: Font.Bold @@ -383,7 +383,7 @@ WlSessionLock { Text { id: welcomeText text: "" - color: Colors.mOnSurface + color: Color.mOnSurface font.family: "DejaVu Sans Mono" font.pointSize: Style.fontSizeLarge property int currentIndex: 0 @@ -412,7 +412,7 @@ WlSessionLock { Text { text: "root@noctalia:~$" - color: Colors.mPrimary + color: Color.mPrimary font.family: "DejaVu Sans Mono" font.pointSize: Style.fontSizeLarge font.weight: Font.Bold @@ -420,7 +420,7 @@ WlSessionLock { Text { text: "sudo unlock-session" - color: Colors.mOnSurface + color: Color.mOnSurface font.family: "DejaVu Sans Mono" font.pointSize: Style.fontSizeLarge } @@ -433,7 +433,7 @@ WlSessionLock { visible: false font.family: "DejaVu Sans Mono" font.pointSize: Style.fontSizeLarge - color: Colors.mOnSurface + color: Color.mOnSurface echoMode: TextInput.Password passwordCharacter: "*" passwordMaskDelay: 0 @@ -460,7 +460,7 @@ WlSessionLock { Text { id: asterisksText text: "*".repeat(passwordInput.text.length) - color: Colors.mOnSurface + color: Color.mOnSurface font.family: "DejaVu Sans Mono" font.pointSize: Style.fontSizeLarge visible: passwordInput.activeFocus @@ -487,7 +487,7 @@ WlSessionLock { Rectangle { width: 8 * scaling height: 20 * scaling - color: Colors.mPrimary + color: Color.mPrimary visible: passwordInput.activeFocus anchors.left: asterisksText.right anchors.leftMargin: Style.marginTiniest * scaling @@ -510,7 +510,7 @@ WlSessionLock { // Status messages Text { text: lock.authenticating ? "Authenticating..." : (lock.errorMessage !== "" ? "Authentication failed." : "") - color: lock.authenticating ? Colors.mPrimary : (lock.errorMessage !== "" ? Colors.mError : "transparent") + color: lock.authenticating ? Color.mPrimary : (lock.errorMessage !== "" ? Color.mError : "transparent") font.family: "DejaVu Sans Mono" font.pointSize: Style.fontSizeLarge Layout.fillWidth: true @@ -534,8 +534,8 @@ WlSessionLock { width: 120 * scaling height: 40 * scaling radius: Style.radiusSmall * scaling - color: executeButtonArea.containsMouse ? Colors.mPrimary : Colors.applyOpacity(Colors.mPrimary, "33") - border.color: Colors.mPrimary + color: executeButtonArea.containsMouse ? Color.mPrimary : Color.applyOpacity(Color.mPrimary, "33") + border.color: Color.mPrimary border.width: Math.max(1, Style.borderThin * scaling) enabled: !lock.authenticating Layout.alignment: Qt.AlignRight @@ -544,7 +544,7 @@ WlSessionLock { Text { anchors.centerIn: parent text: lock.authenticating ? "EXECUTING" : "EXECUTE" - color: executeButtonArea.containsMouse ? Colors.onAccent : Colors.mPrimary + color: executeButtonArea.containsMouse ? Color.onAccent : Color.mPrimary font.family: "DejaVu Sans Mono" font.pointSize: Style.fontSizeMedium font.weight: Font.Bold @@ -598,7 +598,7 @@ WlSessionLock { anchors.fill: parent radius: parent.radius color: "transparent" - border.color: Colors.applyOpacity(Colors.mPrimary, "4D") + border.color: Color.applyOpacity(Color.mPrimary, "4D") border.width: Math.max(1, Style.borderThin * scaling) z: -1 @@ -634,8 +634,8 @@ WlSessionLock { width: 64 * scaling height: 64 * scaling radius: Style.radiusLarge * scaling - color: Qt.rgba(Colors.mError.r, Colors.mError.g, Colors.mError.b, shutdownArea.containsMouse ? 0.9 : 0.2) - border.color: Colors.mError + color: Qt.rgba(Color.mError.r, Color.mError.g, Color.mError.b, shutdownArea.containsMouse ? 0.9 : 0.2) + border.color: Color.mError border.width: Math.max(1, Style.borderMedium * scaling) // Glow effect @@ -645,7 +645,7 @@ WlSessionLock { height: parent.height + 10 * scaling radius: width * 0.5 color: "transparent" - border.color: Qt.rgba(Colors.mError.r, Colors.mError.g, Colors.mError.b, 0.3) + border.color: Qt.rgba(Color.mError.r, Color.mError.g, Color.mError.b, 0.3) border.width: Math.max(1, Style.borderMedium * scaling) opacity: shutdownArea.containsMouse ? 1 : 0 z: -1 @@ -674,7 +674,7 @@ WlSessionLock { text: "power_settings_new" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXXL * scaling - color: shutdownArea.containsMouse ? Colors.onAccent : Colors.mError + color: shutdownArea.containsMouse ? Color.onAccent : Color.mError } Behavior on color { @@ -691,8 +691,8 @@ WlSessionLock { width: 64 * scaling height: 64 * scaling radius: Style.radiusLarge * scaling - color: Qt.rgba(Colors.mPrimary.r, Colors.mPrimary.g, Colors.mPrimary.b, rebootArea.containsMouse ? 0.9 : 0.2) - border.color: Colors.mPrimary + color: Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, rebootArea.containsMouse ? 0.9 : 0.2) + border.color: Color.mPrimary border.width: Math.max(1, Style.borderMedium * scaling) // Glow effect @@ -702,7 +702,7 @@ WlSessionLock { height: parent.height + 10 * scaling radius: width * 0.5 color: "transparent" - border.color: Qt.rgba(Colors.mPrimary.r, Colors.mPrimary.g, Colors.mPrimary.b, 0.3) + border.color: Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.3) border.width: Math.max(1, Style.borderMedium * scaling) opacity: rebootArea.containsMouse ? 1 : 0 z: -1 @@ -730,7 +730,7 @@ WlSessionLock { text: "refresh" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXXL * scaling - color: rebootArea.containsMouse ? Colors.onAccent : Colors.mPrimary + color: rebootArea.containsMouse ? Color.onAccent : Color.mPrimary } Behavior on color { @@ -747,9 +747,8 @@ WlSessionLock { width: 64 * scaling height: 64 * scaling radius: Style.radiusLarge * scaling - color: Qt.rgba(Colors.mSecondary.r, Colors.mSecondary.g, Colors.mSecondary.b, - logoutArea.containsMouse ? 0.9 : 0.2) - border.color: Colors.mSecondary + color: Qt.rgba(Color.mSecondary.r, Color.mSecondary.g, Color.mSecondary.b, logoutArea.containsMouse ? 0.9 : 0.2) + border.color: Color.mSecondary border.width: Math.max(1, Style.borderMedium * scaling) // Glow effect @@ -759,7 +758,7 @@ WlSessionLock { height: parent.height + 10 * scaling radius: width * 0.5 color: "transparent" - border.color: Qt.rgba(Colors.mSecondary.r, Colors.mSecondary.g, Colors.mSecondary.b, 0.3) + border.color: Qt.rgba(Color.mSecondary.r, Color.mSecondary.g, Color.mSecondary.b, 0.3) border.width: Math.max(1, Style.borderMedium * scaling) opacity: logoutArea.containsMouse ? 1 : 0 z: -1 @@ -789,7 +788,7 @@ WlSessionLock { text: "exit_to_app" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXXL * scaling - color: logoutArea.containsMouse ? Colors.onAccent : Colors.mSecondary + color: logoutArea.containsMouse ? Color.onAccent : Color.mSecondary } Behavior on color { diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index 0420eeb..5b2dd24 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -71,9 +71,9 @@ Variants { height: Math.max(80 * scaling, contentColumn.implicitHeight + (Style.marginMedium * 2 * scaling)) clip: true radius: Style.radiusMedium * scaling - border.color: Colors.mPrimary + border.color: Color.mPrimary border.width: Math.max(1, Style.borderThin * scaling) - color: Colors.mSurface + color: Color.mSurface // Animation properties property real scaleValue: 0.8 @@ -141,14 +141,14 @@ Variants { spacing: Style.marginSmall * scaling NText { text: (model.appName || model.desktopEntry) || "Unknown App" - color: Colors.mSecondary + color: Color.mSecondary font.pointSize: Style.fontSizeSmall * scaling } Rectangle { width: 6 * scaling height: 6 * scaling radius: Style.radiusTiny * scaling - color: (model.urgency === NotificationUrgency.Critical) ? Colors.mError : (model.urgency === NotificationUrgency.Low) ? Colors.mOnSurface : Colors.mPrimary + color: (model.urgency === NotificationUrgency.Critical) ? Color.mError : (model.urgency === NotificationUrgency.Low) ? Color.mOnSurface : Color.mPrimary Layout.alignment: Qt.AlignVCenter } Item { @@ -156,7 +156,7 @@ Variants { } NText { text: NotificationService.formatTimestamp(model.timestamp) - color: Colors.mOnSurface + color: Color.mOnSurface font.pointSize: Style.fontSizeSmall * scaling } } @@ -165,7 +165,7 @@ Variants { text: model.summary || "No summary" font.pointSize: Style.fontSizeLarge * scaling font.weight: Style.fontWeightBold - color: Colors.mOnSurface + color: Color.mOnSurface wrapMode: Text.Wrap width: 300 * scaling maximumLineCount: 3 @@ -175,7 +175,7 @@ Variants { NText { text: model.body || "" font.pointSize: Style.fontSizeSmall * scaling - color: Colors.mOnSurface + color: Color.mOnSurface wrapMode: Text.Wrap width: 300 * scaling maximumLineCount: 5 diff --git a/Modules/Notification/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml index 29f8c79..128e46f 100644 --- a/Modules/Notification/NotificationHistoryPanel.qml +++ b/Modules/Notification/NotificationHistoryPanel.qml @@ -66,9 +66,9 @@ NLoader { Rectangle { id: notificationRect - color: Colors.mSurface + color: Color.mSurface radius: Style.radiusLarge * scaling - border.color: Colors.mOutlineVariant + border.color: Color.mOutlineVariant border.width: Math.max(1, Style.borderThin * scaling) width: 400 * scaling height: 500 * scaling @@ -119,14 +119,14 @@ NLoader { text: "notifications" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXL * scaling - color: Colors.mPrimary + color: Color.mPrimary } NText { text: "Notification History" font.pointSize: Style.fontSizeLarge * scaling font.bold: true - color: Colors.mOnSurface + color: Color.mOnSurface Layout.fillWidth: true } @@ -163,21 +163,21 @@ NLoader { text: "notifications_off" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXXL * scaling - color: Colors.mOnSurfaceVariant + color: Color.mOnSurfaceVariant Layout.alignment: Qt.AlignHCenter } NText { text: "No notifications" font.pointSize: Style.fontSizeLarge * scaling - color: Colors.mOnSurfaceVariant + color: Color.mOnSurfaceVariant Layout.alignment: Qt.AlignHCenter } NText { text: "Notifications will appear here when you receive them" font.pointSize: Style.fontSizeNormal * scaling - color: Colors.mOnSurfaceVariant + color: Color.mOnSurfaceVariant Layout.alignment: Qt.AlignHCenter } } @@ -197,7 +197,7 @@ NLoader { width: notificationList ? (notificationList.width - 20) : 380 * scaling height: Math.max(80, notificationContent.height + 30) radius: Style.radiusMedium * scaling - color: notificationMouseArea.containsMouse ? Colors.mPrimary : Colors.mSurfaceVariant + color: notificationMouseArea.containsMouse ? Color.mPrimary : Color.mSurfaceVariant RowLayout { anchors { @@ -217,7 +217,7 @@ NLoader { text: (summary || "No summary").substring(0, 100) font.pointSize: Style.fontSizeMedium * scaling font.weight: Font.Medium - color: notificationMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurface + color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface wrapMode: Text.Wrap width: parent.width - 60 maximumLineCount: 2 @@ -227,7 +227,7 @@ NLoader { NText { text: (body || "").substring(0, 150) font.pointSize: Style.fontSizeSmall * scaling - color: notificationMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurface + color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface wrapMode: Text.Wrap width: parent.width - 60 maximumLineCount: 3 @@ -237,7 +237,7 @@ NLoader { NText { text: NotificationService.formatTimestamp(timestamp) font.pointSize: Style.fontSizeSmall * scaling - color: notificationMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurface + color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface } } diff --git a/Modules/SettingsPanel/SettingsPanel.qml b/Modules/SettingsPanel/SettingsPanel.qml index 2217ff1..4e980ce 100644 --- a/Modules/SettingsPanel/SettingsPanel.qml +++ b/Modules/SettingsPanel/SettingsPanel.qml @@ -199,9 +199,9 @@ NLoader { Rectangle { id: bgRect - color: Colors.mSurface + color: Color.mSurface radius: Style.radiusLarge * scaling - border.color: Colors.mOutlineVariant + border.color: Color.mOutlineVariant border.width: Math.max(1, Style.borderThin * scaling) layer.enabled: true width: Math.max(screen.width * 0.5, 1280) * scaling @@ -247,8 +247,8 @@ NLoader { id: sidebar Layout.preferredWidth: Style.sliderWidth * 1.3 * scaling Layout.fillHeight: true - color: Colors.mSurfaceVariant - border.color: Colors.mOutlineVariant + color: Color.mSurfaceVariant + border.color: Color.mOutlineVariant border.width: Math.max(1, Style.borderThin * scaling) radius: Style.radiusMedium * scaling @@ -265,10 +265,10 @@ NLoader { width: parent.width height: 32 * scaling radius: Style.radiusSmall * scaling - color: selected ? Colors.mPrimary : (tabItem.hovering ? Colors.mTertiary : "transparent") + color: selected ? Color.mPrimary : (tabItem.hovering ? Color.mTertiary : "transparent") readonly property bool selected: index === currentTabIndex property bool hovering: false - property color tabTextColor: selected ? Colors.mOnPrimary : (tabItem.hovering ? Colors.mOnTertiary : Colors.mOnSurface) + property color tabTextColor: selected ? Color.mOnPrimary : (tabItem.hovering ? Color.mOnTertiary : Color.mOnSurface) RowLayout { anchors.fill: parent anchors.leftMargin: Style.marginSmall * scaling @@ -313,8 +313,8 @@ NLoader { Layout.fillWidth: true Layout.fillHeight: true radius: Style.radiusMedium * scaling - color: Colors.mSurfaceVariant - border.color: Colors.mOutlineVariant + color: Color.mSurfaceVariant + border.color: Color.mOutlineVariant border.width: Math.max(1, Style.borderThin * scaling) clip: true @@ -334,7 +334,7 @@ NLoader { text: panel.tabsModel[currentTabIndex].label font.pointSize: Style.fontSizeLarge * scaling font.weight: Style.fontWeightBold - color: Colors.mPrimary + color: Color.mPrimary Layout.fillWidth: true } NIconButton { diff --git a/Modules/SettingsPanel/Tabs/AboutTab.qml b/Modules/SettingsPanel/Tabs/AboutTab.qml index b9e9cbb..811a406 100644 --- a/Modules/SettingsPanel/Tabs/AboutTab.qml +++ b/Modules/SettingsPanel/Tabs/AboutTab.qml @@ -60,7 +60,7 @@ ColumnLayout { text: "Noctalia: quiet by design" font.pointSize: Style.fontSizeXXL * scaling font.weight: Style.fontWeightBold - color: Colors.mOnSurface + color: Color.mOnSurface Layout.alignment: Qt.AlignCenter Layout.bottomMargin: Style.marginSmall * scaling } @@ -68,7 +68,7 @@ ColumnLayout { NText { text: "It may just be another quickshell setup but it won't get in your way." font.pointSize: Style.fontSizeMedium * scaling - color: Colors.mOnSurface + color: Color.mOnSurface Layout.alignment: Qt.AlignCenter Layout.bottomMargin: Style.marginLarge * scaling } @@ -81,25 +81,25 @@ ColumnLayout { NText { text: "Latest Version:" - color: Colors.mOnSurface + color: Color.mOnSurface Layout.alignment: Qt.AlignRight } NText { text: root.latestVersion - color: Colors.mOnSurface + color: Color.mOnSurface font.weight: Style.fontWeightBold } NText { text: "Installed Version:" - color: Colors.mOnSurface + color: Color.mOnSurface Layout.alignment: Qt.AlignRight } NText { text: root.currentVersion - color: Colors.mOnSurface + color: Color.mOnSurface font.weight: Style.fontWeightBold } } @@ -110,8 +110,8 @@ ColumnLayout { Layout.preferredWidth: updateText.implicitWidth + 46 * scaling Layout.preferredHeight: Style.barHeight * scaling radius: Style.radiusLarge * scaling - color: updateArea.containsMouse ? Colors.mPrimary : "transparent" - border.color: Colors.mPrimary + color: updateArea.containsMouse ? Color.mPrimary : "transparent" + border.color: Color.mPrimary border.width: Math.max(1, Style.borderThin * scaling) visible: { if (root.currentVersion === "Unknown" || root.latestVersion === "Unknown") @@ -139,14 +139,14 @@ ColumnLayout { text: "system_update" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXL * scaling - color: updateArea.containsMouse ? Colors.mSurface : Colors.mPrimary + color: updateArea.containsMouse ? Color.mSurface : Color.mPrimary } NText { id: updateText text: "Download latest release" font.pointSize: Style.fontSizeLarge * scaling - color: updateArea.containsMouse ? Colors.mSurface : Colors.mPrimary + color: updateArea.containsMouse ? Color.mSurface : Color.mPrimary } } @@ -172,7 +172,7 @@ ColumnLayout { text: `Shout-out to our ${root.contributors.length} awesome contributors!` font.pointSize: Style.fontSizeLarge * scaling font.weight: Style.fontWeightBold - color: Colors.mOnSurface + color: Color.mOnSurface Layout.alignment: Qt.AlignCenter Layout.topMargin: Style.marginLarge * 2 } @@ -200,7 +200,7 @@ ColumnLayout { width: contributorsGrid.cellWidth - Style.marginLarge * scaling height: contributorsGrid.cellHeight - Style.marginTiny * scaling radius: Style.radiusLarge * scaling - color: contributorArea.containsMouse ? Colors.mTertiary : "transparent" + color: contributorArea.containsMouse ? Color.mTertiary : "transparent" RowLayout { anchors.fill: parent @@ -217,7 +217,7 @@ ColumnLayout { anchors.fill: parent anchors.margins: Style.marginTiny * scaling fallbackIcon: "person" - borderColor: Colors.mPrimary + borderColor: Color.mPrimary borderWidth: Math.max(1, Style.borderMedium * scaling) imageRadius: width * 0.5 } @@ -231,7 +231,7 @@ ColumnLayout { NText { text: modelData.login || "Unknown" font.weight: Style.fontWeightBold - color: contributorArea.containsMouse ? Colors.mSurface : Colors.mOnSurface + color: contributorArea.containsMouse ? Color.mSurface : Color.mOnSurface elide: Text.ElideRight Layout.fillWidth: true } @@ -240,7 +240,7 @@ ColumnLayout { text: (modelData.contributions || 0) + " " + ((modelData.contributions || 0) === 1 ? "commit" : "commits") font.pointSize: Style.fontSizeSmall * scaling - color: contributorArea.containsMouse ? Colors.mSurface : Colors.mOnSurface + color: contributorArea.containsMouse ? Color.mSurface : Color.mOnSurface } } } diff --git a/Modules/SettingsPanel/Tabs/AudioTab.qml b/Modules/SettingsPanel/Tabs/AudioTab.qml index 0efca92..d454f2b 100644 --- a/Modules/SettingsPanel/Tabs/AudioTab.qml +++ b/Modules/SettingsPanel/Tabs/AudioTab.qml @@ -48,7 +48,7 @@ ColumnLayout { text: "Audio" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.mOnSurface + color: Color.mOnSurface Layout.bottomMargin: Style.marginSmall * scaling } @@ -69,13 +69,13 @@ ColumnLayout { NText { text: "Master Volume" font.weight: Style.fontWeightBold - color: Colors.mOnSurface + color: Color.mOnSurface } NText { text: "System-wide volume level" font.pointSize: Style.fontSizeSmall * scaling - color: Colors.mOnSurface + color: Color.mOnSurface wrapMode: Text.WordWrap Layout.fillWidth: true } @@ -109,7 +109,7 @@ ColumnLayout { NText { text: Math.floor(AudioService.volume * 100) + "%" Layout.alignment: Qt.AlignVCenter - color: Colors.mOnSurface + color: Color.mOnSurface } } } @@ -148,7 +148,7 @@ ColumnLayout { text: "Audio Devices" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.mOnSurface + color: Color.mOnSurface Layout.bottomMargin: Style.marginSmall * scaling } @@ -167,13 +167,13 @@ ColumnLayout { text: "Output Device" font.pointSize: Style.fontSizeMedium * scaling font.weight: Style.fontWeightBold - color: Colors.mOnSurface + color: Color.mOnSurface } NText { text: "Select the desired audio output device" font.pointSize: Style.fontSizeSmall * scaling - color: Colors.mOnSurface + color: Color.mOnSurface wrapMode: Text.WordWrap Layout.fillWidth: true } @@ -206,13 +206,13 @@ ColumnLayout { text: "Input Device" font.pointSize: Style.fontSizeMedium * scaling font.weight: Style.fontWeightBold - color: Colors.mOnSurface + color: Color.mOnSurface } NText { text: "Select desired audio input device" font.pointSize: Style.fontSizeSmall * scaling - color: Colors.mOnSurface + color: Color.mOnSurface wrapMode: Text.WordWrap Layout.fillWidth: true } @@ -246,7 +246,7 @@ ColumnLayout { text: "Audio Visualizer" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.mOnSurface + color: Color.mOnSurface Layout.bottomMargin: Style.marginSmall * scaling } diff --git a/Modules/SettingsPanel/Tabs/BarTab.qml b/Modules/SettingsPanel/Tabs/BarTab.qml index 66ce55e..71f9f64 100644 --- a/Modules/SettingsPanel/Tabs/BarTab.qml +++ b/Modules/SettingsPanel/Tabs/BarTab.qml @@ -37,7 +37,7 @@ ColumnLayout { text: "Components" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.mOnSurface + color: Color.mOnSurface } NToggle { @@ -51,7 +51,7 @@ ColumnLayout { NToggle { label: "Show System Info" - description: "Display system information (CPU, RAM, Temperature)" + description: "Display system statistics (CPU, RAM, Temperature)" checked: Settings.data.bar.showSystemInfo onToggled: checked => { Settings.data.bar.showSystemInfo = checked @@ -66,6 +66,24 @@ ColumnLayout { Settings.data.bar.showMedia = checked } } + + NToggle { + label: "Show Notifications History" + description: "Display a shortcut to the notifications history" + checked: Settings.data.bar.showNotificationsHistory + onToggled: checked => { + Settings.data.bar.showNotificationsHistory = checked + } + } + + NToggle { + label: "Show Applications Tray" + description: "Display the applications tray" + checked: Settings.data.bar.showTray + onToggled: checked => { + Settings.data.bar.showTray = checked + } + } } } } diff --git a/Modules/SettingsPanel/Tabs/BrightnessTab.qml b/Modules/SettingsPanel/Tabs/BrightnessTab.qml index d195f29..4f9f7df 100644 --- a/Modules/SettingsPanel/Tabs/BrightnessTab.qml +++ b/Modules/SettingsPanel/Tabs/BrightnessTab.qml @@ -31,13 +31,13 @@ Item { text: "Brightness Settings" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.mOnSurface + color: Color.mOnSurface } NText { text: "Configure brightness controls and monitor settings." font.pointSize: Style.fontSize * scaling - color: Colors.mOnSurfaceVariant + color: Color.mOnSurfaceVariant } // Bar Visibility Section @@ -50,15 +50,15 @@ Item { text: "Bar Integration" font.pointSize: Style.fontSizeLarge * scaling font.weight: Style.fontWeightBold - color: Colors.mOnSurface + color: Color.mOnSurface } NToggle { label: "Show Brightness Icon" description: "Display the brightness control icon in the top bar" - checked: !Settings.data.bar.hideBrightness + checked: Settings.data.bar.showBrightness onToggled: checked => { - Settings.data.bar.hideBrightness = !checked + Settings.data.bar.showBrightness = checked } } } @@ -78,13 +78,13 @@ Item { text: "Brightness Step Size" font.pointSize: Style.fontSizeLarge * scaling font.weight: Style.fontWeightBold - color: Colors.mOnSurface + color: Color.mOnSurface } NText { text: "Adjust the step size for brightness changes (scroll wheel, keyboard shortcuts)" font.pointSize: Style.fontSizeSmall * scaling - color: Colors.mOnSurfaceVariant + color: Color.mOnSurfaceVariant wrapMode: Text.WordWrap Layout.fillWidth: true } @@ -109,7 +109,7 @@ Item { NText { text: Settings.data.brightness.brightnessStep + "%" Layout.alignment: Qt.AlignVCenter - color: Colors.mOnSurface + color: Color.mOnSurface font.pointSize: Style.fontSizeMedium * scaling font.weight: Style.fontWeightBold } @@ -131,13 +131,13 @@ Item { text: "Monitor Brightness Overview" font.pointSize: Style.fontSizeLarge * scaling font.weight: Style.fontWeightBold - color: Colors.mOnSurface + color: Color.mOnSurface } NText { text: "Current brightness levels for all detected monitors" font.pointSize: Style.fontSizeSmall * scaling - color: Colors.mOnSurfaceVariant + color: Color.mOnSurfaceVariant wrapMode: Text.WordWrap Layout.fillWidth: true } @@ -146,8 +146,8 @@ Item { Rectangle { Layout.fillWidth: true radius: Style.radiusMedium * scaling - color: Colors.mSurface - border.color: Colors.mOutline + color: Color.mSurface + border.color: Color.mOutline border.width: Math.max(1, Style.borderThin * scaling) implicitHeight: contentCol.implicitHeight + Style.marginXL * 2 * scaling @@ -165,7 +165,7 @@ Item { text: "Primary Monitor" font.pointSize: Style.fontSizeLarge * scaling font.weight: Style.fontWeightBold - color: Colors.mSecondary + color: Color.mSecondary } Item { @@ -175,7 +175,7 @@ Item { NText { text: BrightnessService.currentMethod === "ddcutil" ? "External (DDC)" : "Internal" font.pointSize: Style.fontSizeSmall * scaling - color: Colors.mOnSurfaceVariant + color: Color.mOnSurfaceVariant Layout.alignment: Qt.AlignRight } } @@ -187,7 +187,7 @@ Item { NText { text: "Brightness:" font.pointSize: Style.fontSizeMedium * scaling - color: Colors.mOnSurface + color: Color.mOnSurface } NSlider { @@ -208,7 +208,7 @@ Item { text: BrightnessService.available ? Math.round(BrightnessService.brightness) + "%" : "N/A" font.pointSize: Style.fontSizeMedium * scaling font.weight: Style.fontWeightBold - color: BrightnessService.available ? Colors.mPrimary : Colors.mOnSurfaceVariant + color: BrightnessService.available ? Color.mPrimary : Color.mOnSurfaceVariant Layout.alignment: Qt.AlignRight } } @@ -220,13 +220,13 @@ Item { NText { text: "Method:" font.pointSize: Style.fontSizeSmall * scaling - color: Colors.mOnSurfaceVariant + color: Color.mOnSurfaceVariant } NText { text: BrightnessService.currentMethod || "Unknown" font.pointSize: Style.fontSizeSmall * scaling - color: Colors.mOnSurface + color: Color.mOnSurface Layout.alignment: Qt.AlignLeft } @@ -237,7 +237,7 @@ Item { NText { text: BrightnessService.available ? "Available" : "Unavailable" font.pointSize: Style.fontSizeSmall * scaling - color: BrightnessService.available ? Colors.mPrimary : Colors.mError + color: BrightnessService.available ? Color.mPrimary : Color.mError Layout.alignment: Qt.AlignRight } } diff --git a/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml b/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml index ed7acbe..e470e08 100644 --- a/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml +++ b/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml @@ -152,14 +152,14 @@ ColumnLayout { text: "Predefined Color Schemes" font.pointSize: Style.fontSizeLarge * scaling font.weight: Style.fontWeightBold - color: Colors.mOnSurface + color: Color.mOnSurface Layout.fillWidth: true } NText { text: "These color schemes only apply when 'Use Matugen' is disabled. When enabled, Matugen will generate colors based on your wallpaper instead." font.pointSize: Style.fontSizeSmall * scaling - color: Colors.mOnSurface + color: Color.mOnSurface Layout.fillWidth: true wrapMode: Text.WordWrap } @@ -190,7 +190,7 @@ ColumnLayout { radius: Style.radiusMedium * scaling color: getSchemeColor(modelData, "mSurface") border.width: Math.max(1, Style.borderThick * scaling) - border.color: Settings.data.colorSchemes.predefinedScheme === modelData ? Colors.mPrimary : Colors.mOutline + border.color: Settings.data.colorSchemes.predefinedScheme === modelData ? Color.mPrimary : Color.mOutline scale: root.cardScaleLow // Mouse area for selection @@ -285,14 +285,14 @@ ColumnLayout { width: 24 * scaling height: 24 * scaling radius: width * 0.5 - color: Colors.mPrimary + color: Color.mPrimary NText { anchors.centerIn: parent text: "✓" font.pointSize: Style.fontSizeSmall * scaling font.weight: Style.fontWeightBold - color: Colors.mOnPrimary + color: Color.mOnPrimary } } diff --git a/Modules/SettingsPanel/Tabs/DisplayTab.qml b/Modules/SettingsPanel/Tabs/DisplayTab.qml index a8007e9..980284f 100644 --- a/Modules/SettingsPanel/Tabs/DisplayTab.qml +++ b/Modules/SettingsPanel/Tabs/DisplayTab.qml @@ -45,13 +45,13 @@ Item { text: "Per‑monitor configuration" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.mOnSurface + color: Color.mOnSurface } NText { text: "By default, bars and notifications are shown on all displays. Select one or more below to narrow your view." font.pointSize: Style.fontSize * scaling - color: Colors.mOnSurfaceVariant + color: Color.mOnSurfaceVariant } Repeater { @@ -59,8 +59,8 @@ Item { delegate: Rectangle { Layout.fillWidth: true radius: Style.radiusMedium * scaling - color: Colors.mSurface - border.color: Colors.mOutline + color: Color.mSurface + border.color: Color.mOutline border.width: Math.max(1, Style.borderThin * scaling) implicitHeight: contentCol.implicitHeight + Style.marginXL * 2 * scaling @@ -74,13 +74,13 @@ Item { text: (modelData.name || "Unknown") font.pointSize: Style.fontSizeLarge * scaling font.weight: Style.fontWeightBold - color: Colors.mSecondary + color: Color.mSecondary } NText { text: `Resolution: ${modelData.width}x${modelData.height} - Position: (${modelData.x}, ${modelData.y})` font.pointSize: Style.fontSizeSmall * scaling - color: Colors.mOnSurface + color: Color.mOnSurface } ColumnLayout { diff --git a/Modules/SettingsPanel/Tabs/GeneralTab.qml b/Modules/SettingsPanel/Tabs/GeneralTab.qml index ba683cb..9e32849 100644 --- a/Modules/SettingsPanel/Tabs/GeneralTab.qml +++ b/Modules/SettingsPanel/Tabs/GeneralTab.qml @@ -37,7 +37,7 @@ ColumnLayout { text: "General Settings" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.mOnSurface + color: Color.mOnSurface } // Profile section @@ -56,7 +56,7 @@ ColumnLayout { height: 64 * scaling imagePath: Settings.data.general.avatarImage fallbackIcon: "person" - borderColor: Colors.mPrimary + borderColor: Color.mPrimary borderWidth: Math.max(1, Style.borderMedium) } @@ -88,7 +88,7 @@ ColumnLayout { text: "User Interface" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.mOnSurface + color: Color.mOnSurface Layout.bottomMargin: Style.marginSmall * scaling } diff --git a/Modules/SettingsPanel/Tabs/NetworkTab.qml b/Modules/SettingsPanel/Tabs/NetworkTab.qml index f9e66f2..32ffb04 100644 --- a/Modules/SettingsPanel/Tabs/NetworkTab.qml +++ b/Modules/SettingsPanel/Tabs/NetworkTab.qml @@ -39,7 +39,7 @@ ColumnLayout { text: "Interfaces" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.mOnSurface + color: Color.mOnSurface } NToggle { diff --git a/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml b/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml index 534d451..21bc75d 100644 --- a/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml +++ b/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml @@ -37,7 +37,7 @@ ColumnLayout { text: "Recording" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.mOnSurface + color: Color.mOnSurface Layout.bottomMargin: Style.marginSmall * scaling } @@ -88,7 +88,7 @@ ColumnLayout { text: "Video Settings" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.mOnSurface + color: Color.mOnSurface Layout.bottomMargin: Style.marginSmall * scaling } @@ -216,7 +216,7 @@ ColumnLayout { text: "Audio Settings" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.mOnSurface + color: Color.mOnSurface Layout.bottomMargin: Style.marginSmall * scaling } diff --git a/Modules/SettingsPanel/Tabs/TimeWeatherTab.qml b/Modules/SettingsPanel/Tabs/TimeWeatherTab.qml index 157b916..5d93ee1 100644 --- a/Modules/SettingsPanel/Tabs/TimeWeatherTab.qml +++ b/Modules/SettingsPanel/Tabs/TimeWeatherTab.qml @@ -37,7 +37,7 @@ ColumnLayout { text: "Location" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.mOnSurface + color: Color.mOnSurface Layout.bottomMargin: Style.marginSmall * scaling } @@ -74,7 +74,7 @@ ColumnLayout { text: "Time Format" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.mOnSurface + color: Color.mOnSurface Layout.bottomMargin: Style.marginSmall * scaling } @@ -121,7 +121,7 @@ ColumnLayout { text: "Weather" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.mOnSurface + color: Color.mOnSurface Layout.bottomMargin: Style.marginSmall * scaling } diff --git a/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml b/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml index 7481da1..b2a49ac 100644 --- a/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml +++ b/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml @@ -33,15 +33,15 @@ Item { text: "Current Wallpaper" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.mOnSurface + color: Color.mOnSurface } Rectangle { Layout.fillWidth: true Layout.preferredHeight: 120 * scaling radius: Style.radiusMedium * scaling - color: Colors.mSurface - border.color: Colors.mOutline + color: Color.mSurface + border.color: Color.mOutline border.width: Math.max(1, Style.borderThin * scaling) clip: true @@ -51,7 +51,7 @@ Item { anchors.margins: Style.marginSmall * scaling imagePath: WallpapersService.currentWallpaper fallbackIcon: "image" - borderColor: Colors.mOutline + borderColor: Color.mOutline borderWidth: Math.max(1, Style.borderThin * scaling) imageRadius: Style.radiusMedium * scaling } @@ -74,12 +74,12 @@ Item { text: "Wallpaper Selector" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.mOnSurface + color: Color.mOnSurface } NText { text: "Click on a wallpaper to set it as your current wallpaper" - color: Colors.mOnSurface + color: Color.mOnSurface wrapMode: Text.WordWrap Layout.fillWidth: true } @@ -87,7 +87,7 @@ Item { NText { text: Settings.data.wallpaper.swww.enabled ? "Wallpapers will change with " + Settings.data.wallpaper.swww.transitionType + " transition" : "Wallpapers will change instantly" - color: Colors.mOnSurface + color: Color.mOnSurface font.pointSize: Style.fontSizeSmall * scaling visible: Settings.data.wallpaper.swww.enabled } @@ -141,8 +141,8 @@ Item { width: wallpaperGridView.itemSize height: Math.floor(wallpaperGridView.itemSize * 0.67) radius: Style.radiusMedium * scaling - color: isSelected ? Colors.mPrimary : Colors.mSurface - border.color: isSelected ? Colors.mSecondary : Colors.mOutline + color: isSelected ? Color.mPrimary : Color.mSurface + border.color: isSelected ? Color.mSecondary : Color.mOutline border.width: Math.max(1, Style.borderThin * scaling) clip: true @@ -163,8 +163,8 @@ Item { width: 20 * scaling height: 20 * scaling radius: width / 2 - color: Colors.mPrimary - border.color: Colors.mOutline + color: Color.mPrimary + border.color: Color.mOutline border.width: Math.max(1, Style.borderThin * scaling) visible: isSelected @@ -173,14 +173,14 @@ Item { text: "check" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeSmall * scaling - color: Colors.mOnPrimary + color: Color.mOnPrimary } } // Hover effect Rectangle { anchors.fill: parent - color: Colors.mOnSurface + color: Color.mOnSurface opacity: mouseArea.containsMouse ? 0.1 : 0 radius: parent.radius @@ -206,9 +206,9 @@ Item { // Empty state Rectangle { anchors.fill: parent - color: Colors.mSurface + color: Color.mSurface radius: Style.radiusMedium * scaling - border.color: Colors.mOutline + border.color: Color.mOutline border.width: Math.max(1, Style.borderThin * scaling) visible: WallpapersService.wallpaperList.length === 0 && !WallpapersService.scanning @@ -220,20 +220,20 @@ Item { text: "folder_open" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeLarge * scaling - color: Colors.mOnSurface + color: Color.mOnSurface Layout.alignment: Qt.AlignHCenter } NText { text: "No wallpapers found" - color: Colors.mOnSurface + color: Color.mOnSurface font.weight: Style.fontWeightBold Layout.alignment: Qt.AlignHCenter } NText { text: "Make sure your wallpaper directory is configured and contains image files" - color: Colors.mOnSurface + color: Color.mOnSurface wrapMode: Text.WordWrap horizontalAlignment: Text.AlignHCenter Layout.preferredWidth: Style.sliderWidth * 1.5 * scaling diff --git a/Modules/SettingsPanel/Tabs/WallpaperTab.qml b/Modules/SettingsPanel/Tabs/WallpaperTab.qml index 5672c3f..0670232 100644 --- a/Modules/SettingsPanel/Tabs/WallpaperTab.qml +++ b/Modules/SettingsPanel/Tabs/WallpaperTab.qml @@ -37,7 +37,7 @@ ColumnLayout { text: "Directory" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.mOnSurface + color: Color.mOnSurface Layout.bottomMargin: Style.marginSmall * scaling } @@ -79,7 +79,7 @@ ColumnLayout { text: "Automation" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.mOnSurface + color: Color.mOnSurface Layout.bottomMargin: Style.marginSmall * scaling } @@ -102,13 +102,13 @@ ColumnLayout { NText { text: "Wallpaper Interval" font.weight: Style.fontWeightBold - color: Colors.mOnSurface + color: Color.mOnSurface } NText { text: "How often to change wallpapers automatically (in seconds)" font.pointSize: Style.fontSizeSmall * scaling - color: Colors.mOnSurface + color: Color.mOnSurface wrapMode: Text.WordWrap Layout.fillWidth: true } @@ -128,7 +128,7 @@ ColumnLayout { stepSize: 10 value: Settings.data.wallpaper.randomInterval onPressedChanged: Settings.data.wallpaper.randomInterval = Math.round(value) - cutoutColor: Colors.mSurface + cutoutColor: Color.mSurface } } } @@ -149,7 +149,7 @@ ColumnLayout { text: "SWWW" font.pointSize: Style.fontSizeXL * scaling font.weight: Style.fontWeightBold - color: Colors.mOnSurface + color: Color.mOnSurface Layout.bottomMargin: Style.marginSmall * scaling } @@ -275,13 +275,13 @@ ColumnLayout { NText { text: "Transition FPS" font.weight: Style.fontWeightBold - color: Colors.mOnSurface + color: Color.mOnSurface } NText { text: "Frames per second for transition animations" font.pointSize: Style.fontSizeSmall * scaling - color: Colors.mOnSurface + color: Color.mOnSurface wrapMode: Text.WordWrap Layout.fillWidth: true } @@ -301,7 +301,7 @@ ColumnLayout { stepSize: 5 value: Settings.data.wallpaper.swww.transitionFps onPressedChanged: Settings.data.wallpaper.swww.transitionFps = Math.round(value) - cutoutColor: Colors.mSurface + cutoutColor: Color.mSurface } } @@ -314,13 +314,13 @@ ColumnLayout { NText { text: "Transition Duration" font.weight: Style.fontWeightBold - color: Colors.mOnSurface + color: Color.mOnSurface } NText { text: "Duration of transition animations in seconds" font.pointSize: Style.fontSizeSmall * scaling - color: Colors.mOnSurface + color: Color.mOnSurface wrapMode: Text.WordWrap Layout.fillWidth: true } @@ -340,7 +340,7 @@ ColumnLayout { stepSize: 0.05 value: Settings.data.wallpaper.swww.transitionDuration onPressedChanged: Settings.data.wallpaper.swww.transitionDuration = value - cutoutColor: Colors.mSurface + cutoutColor: Color.mSurface } } } diff --git a/Modules/SidePanel/Cards/MediaCard.qml b/Modules/SidePanel/Cards/MediaCard.qml index 4c1e603..aee2166 100644 --- a/Modules/SidePanel/Cards/MediaCard.qml +++ b/Modules/SidePanel/Cards/MediaCard.qml @@ -41,12 +41,12 @@ NBox { text: "album" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXXL * 2.5 * scaling - color: Colors.mPrimary + color: Color.mPrimary Layout.alignment: Qt.AlignHCenter } NText { text: "No media player detected" - color: Colors.mOnSurfaceVariant + color: Color.mOnSurfaceVariant Layout.alignment: Qt.AlignHCenter } @@ -77,7 +77,7 @@ NBox { // implicitWidth: 120 * scaling // implicitHeight: 30 * scaling color: "transparent" - border.color: playerSelector.activeFocus ? Colors.mTertiary : Colors.mOutline + border.color: playerSelector.activeFocus ? Color.mTertiary : Color.mOutline border.width: Math.max(1, Style.borderThin * scaling) radius: Style.radiusMedium * scaling } @@ -88,7 +88,7 @@ NBox { rightPadding: playerSelector.indicator.width + playerSelector.spacing text: playerSelector.displayText font.pointSize: Style.fontSizeSmall * scaling - color: Colors.mOnSurface + color: Color.mOnSurface verticalAlignment: Text.AlignVCenter elide: Text.ElideRight } @@ -99,7 +99,7 @@ NBox { text: "arrow_drop_down" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXL * scaling - color: Colors.mOnSurface + color: Color.mOnSurface horizontalAlignment: Text.AlignRight } @@ -120,8 +120,8 @@ NBox { } background: Rectangle { - color: Colors.mSurface - border.color: Colors.mOutline + color: Color.mSurface + border.color: Color.mOutline border.width: Math.max(1, Style.borderThin * scaling) radius: Style.radiusTiny * scaling } @@ -132,7 +132,7 @@ NBox { contentItem: NText { text: modelData.identity font.pointSize: Style.fontSizeSmall * scaling - color: highlighted ? Colors.mSurface : Colors.mOnSurface + color: highlighted ? Color.mSurface : Color.mOnSurface verticalAlignment: Text.AlignVCenter elide: Text.ElideRight } @@ -140,7 +140,7 @@ NBox { background: Rectangle { width: popup.width - Style.marginSmall * scaling * 2 - color: highlighted ? Colors.mTertiary : "transparent" + color: highlighted ? Color.mTertiary : "transparent" radius: Style.radiusTiny * scaling } } @@ -161,8 +161,8 @@ NBox { width: 90 * scaling height: 90 * scaling radius: width * 0.5 - color: trackArt.visible ? Colors.mPrimary : "transparent" - border.color: trackArt.visible ? Colors.mOutline : "transparent" + color: trackArt.visible ? Color.mPrimary : "transparent" + border.color: trackArt.visible ? Color.mOutline : "transparent" border.width: Math.max(1, Style.borderThin * scaling) clip: true @@ -174,7 +174,7 @@ NBox { anchors.margins: Style.marginTiny * scaling imagePath: MediaService.trackArtUrl fallbackIcon: "music_note" - borderColor: Colors.mOutline + borderColor: Color.mOutline borderWidth: Math.max(1, Style.borderThin * scaling) imageRadius: width * 0.5 } @@ -183,7 +183,7 @@ NBox { NText { anchors.centerIn: parent text: "album" - color: Colors.mPrimary + color: Color.mPrimary font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeLarge * 12 * scaling visible: !trackArt.visible @@ -210,7 +210,7 @@ NBox { NText { visible: MediaService.trackArtist !== "" text: MediaService.trackArtist - color: Colors.mOnSurface + color: Color.mOnSurface font.pointSize: Style.fontSizeSmall * scaling elide: Text.ElideRight Layout.fillWidth: true @@ -219,7 +219,7 @@ NBox { NText { visible: MediaService.trackAlbum !== "" text: MediaService.trackAlbum - color: Colors.mOnSurface + color: Color.mOnSurface font.pointSize: Style.fontSizeSmall * scaling elide: Text.ElideRight Layout.fillWidth: true @@ -235,7 +235,7 @@ NBox { width: parent.width height: 4 * scaling radius: Style.radiusSmall * scaling - color: Colors.mSurface + color: Color.mSurface Layout.fillWidth: true property real progressRatio: { @@ -250,7 +250,7 @@ NBox { width: progressBarBackground.progressRatio * parent.width height: parent.height radius: parent.radius - color: Colors.mPrimary + color: Color.mPrimary Behavior on width { NumberAnimation { @@ -266,8 +266,8 @@ NBox { width: 16 * scaling height: 16 * scaling radius: width * 0.5 - color: Colors.mPrimary - border.color: Colors.mOutline + color: Color.mPrimary + border.color: Color.mOutline border.width: Math.max(1 * Style.borderMedium * scaling) x: Math.max(0, Math.min(parent.width - width, progressFill.width - width / 2)) anchors.verticalCenter: parent.verticalCenter @@ -343,7 +343,7 @@ NBox { width: 300 * scaling height: 80 * scaling values: CavaService.values - fillColor: Colors.mOnSurface + fillColor: Color.mOnSurface Layout.alignment: Qt.AlignHCenter } } diff --git a/Modules/SidePanel/Cards/ProfileCard.qml b/Modules/SidePanel/Cards/ProfileCard.qml index 544f129..cf1e329 100644 --- a/Modules/SidePanel/Cards/ProfileCard.qml +++ b/Modules/SidePanel/Cards/ProfileCard.qml @@ -33,7 +33,7 @@ NBox { height: Style.baseWidgetSize * 1.25 * scaling imagePath: Settings.data.general.avatarImage fallbackIcon: "person" - borderColor: Colors.mPrimary + borderColor: Color.mPrimary borderWidth: Math.max(1, Style.borderMedium * scaling) } @@ -46,7 +46,7 @@ NBox { } NText { text: `System Uptime: ${uptimeText}` - color: Colors.mOnSurface + color: Color.mOnSurface } } diff --git a/Modules/SidePanel/Cards/WeatherCard.qml b/Modules/SidePanel/Cards/WeatherCard.qml index 274689f..df9bdfd 100644 --- a/Modules/SidePanel/Cards/WeatherCard.qml +++ b/Modules/SidePanel/Cards/WeatherCard.qml @@ -31,7 +31,7 @@ NBox { LocationService.data.weather.current_weather.weathercode) : "" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXXL * 1.5 * scaling - color: Colors.mPrimary + color: Color.mPrimary } ColumnLayout { @@ -92,13 +92,13 @@ NBox { spacing: Style.marginSmall * scaling NText { text: Qt.formatDateTime(new Date(LocationService.data.weather.daily.time[index]), "ddd") - color: Colors.mOnSurface + color: Color.mOnSurface } NText { text: LocationService.weatherSymbolFromCode(LocationService.data.weather.daily.weathercode[index]) font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeXL * scaling - color: Colors.mPrimary + color: Color.mPrimary } NText { text: { @@ -113,7 +113,7 @@ NBox { return `${max}°/${min}°` } font.pointSize: Style.fontSizeSmall * scaling - color: Colors.mOnSurfaceVariant + color: Color.mOnSurfaceVariant } } } diff --git a/Modules/SidePanel/PowerMenu.qml b/Modules/SidePanel/PowerMenu.qml index 5fa5bf5..8a52447 100644 --- a/Modules/SidePanel/PowerMenu.qml +++ b/Modules/SidePanel/PowerMenu.qml @@ -26,9 +26,9 @@ NPanel { width: 160 * scaling height: 220 * scaling radius: Style.radiusMedium * scaling - border.color: Colors.mOutline + border.color: Color.mOutline border.width: Math.max(1, Style.borderThin * scaling) - color: Colors.mSurface + color: Color.mSurface visible: true z: 9999 @@ -57,7 +57,7 @@ NPanel { Layout.fillWidth: true Layout.preferredHeight: Style.barHeight * scaling radius: Style.radiusSmall * scaling - color: lockButtonArea.containsMouse ? Colors.mTertiary : "transparent" + color: lockButtonArea.containsMouse ? Color.mTertiary : "transparent" Item { anchors.left: parent.left @@ -80,7 +80,7 @@ NPanel { font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 } - color: lockButtonArea.containsMouse ? Colors.mOnTertiary : Colors.mOnSurface + color: lockButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -88,7 +88,7 @@ NPanel { Text { text: "Lock Screen" - color: lockButtonArea.containsMouse ? Colors.mOnTertiary : Colors.mOnSurface + color: lockButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -117,7 +117,7 @@ NPanel { Layout.fillWidth: true Layout.preferredHeight: Style.barHeight * scaling radius: Style.radiusSmall * scaling - color: suspendButtonArea.containsMouse ? Colors.mTertiary : "transparent" + color: suspendButtonArea.containsMouse ? Color.mTertiary : "transparent" Item { anchors.left: parent.left @@ -140,7 +140,7 @@ NPanel { font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 } - color: suspendButtonArea.containsMouse ? Colors.mOnTertiary : Colors.mOnSurface + color: suspendButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -148,7 +148,7 @@ NPanel { Text { text: "Suspend" - color: suspendButtonArea.containsMouse ? Colors.mOnTertiary : Colors.mOnSurface + color: suspendButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -175,7 +175,7 @@ NPanel { Layout.fillWidth: true Layout.preferredHeight: Style.barHeight * scaling radius: Style.radiusSmall * scaling - color: rebootButtonArea.containsMouse ? Colors.mTertiary : "transparent" + color: rebootButtonArea.containsMouse ? Color.mTertiary : "transparent" Item { anchors.left: parent.left @@ -198,7 +198,7 @@ NPanel { font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 } - color: rebootButtonArea.containsMouse ? Colors.mOnTertiary : Colors.mOnSurface + color: rebootButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -206,7 +206,7 @@ NPanel { Text { text: "Reboot" - color: rebootButtonArea.containsMouse ? Colors.mOnTertiary : Colors.mOnSurface + color: rebootButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -233,7 +233,7 @@ NPanel { Layout.fillWidth: true Layout.preferredHeight: Style.barHeight * scaling radius: Style.radiusSmall * scaling - color: logoutButtonArea.containsMouse ? Colors.mTertiary : "transparent" + color: logoutButtonArea.containsMouse ? Color.mTertiary : "transparent" Item { anchors.left: parent.left @@ -256,7 +256,7 @@ NPanel { font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 } - color: logoutButtonArea.containsMouse ? Colors.mOnTertiary : Colors.mOnSurface + color: logoutButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -264,7 +264,7 @@ NPanel { Text { text: "Logout" - color: logoutButtonArea.containsMouse ? Colors.mOnTertiary : Colors.mOnSurface + color: logoutButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -291,7 +291,7 @@ NPanel { Layout.fillWidth: true Layout.preferredHeight: Style.barHeight * scaling radius: Style.radiusSmall * scaling - color: shutdownButtonArea.containsMouse ? Colors.mTertiary : "transparent" + color: shutdownButtonArea.containsMouse ? Color.mTertiary : "transparent" Item { anchors.left: parent.left @@ -314,7 +314,7 @@ NPanel { font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 } - color: shutdownButtonArea.containsMouse ? Colors.mOnTertiary : Colors.mOnSurface + color: shutdownButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling @@ -322,7 +322,7 @@ NPanel { Text { text: "Shutdown" - color: shutdownButtonArea.containsMouse ? Colors.mOnTertiary : Colors.mOnSurface + color: shutdownButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 1 * scaling diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml index 76f03e0..be6a576 100644 --- a/Modules/SidePanel/SidePanel.qml +++ b/Modules/SidePanel/SidePanel.qml @@ -81,9 +81,9 @@ NLoader { // Inline helpers moved to dedicated widgets: NCard and NCircleStat Rectangle { id: panelBackground - color: Colors.mSurface + color: Color.mSurface radius: Style.radiusLarge * scaling - border.color: Colors.mOutlineVariant + border.color: Color.mOutlineVariant border.width: Math.max(1, Style.borderThin * scaling) layer.enabled: true width: 460 * scaling diff --git a/Widgets/NBox.qml b/Widgets/NBox.qml index ffa69f4..5a09a68 100644 --- a/Widgets/NBox.qml +++ b/Widgets/NBox.qml @@ -11,9 +11,9 @@ Rectangle { implicitWidth: childrenRect.width implicitHeight: childrenRect.height - color: Colors.mSurfaceVariant + color: Color.mSurfaceVariant radius: Style.radiusMedium * scaling - border.color: Colors.mOutlineVariant + border.color: Color.mOutlineVariant border.width: Math.max(1, Style.borderThin * scaling) clip: true } diff --git a/Widgets/NBusyIndicator.qml b/Widgets/NBusyIndicator.qml index bef3a28..6c77a12 100644 --- a/Widgets/NBusyIndicator.qml +++ b/Widgets/NBusyIndicator.qml @@ -6,7 +6,7 @@ Item { id: root property bool running: true - property color color: Colors.mPrimary + property color color: Color.mPrimary property int size: Style.baseWidgetSize * scaling property int strokeWidth: Style.borderThick * scaling property int duration: 1000 diff --git a/Widgets/NCard.qml b/Widgets/NCard.qml index db01764..a72b5fb 100644 --- a/Widgets/NCard.qml +++ b/Widgets/NCard.qml @@ -9,8 +9,8 @@ Rectangle { implicitWidth: childrenRect.width implicitHeight: childrenRect.height - color: Colors.mSurface + color: Color.mSurface radius: Style.radiusMedium * scaling - border.color: Colors.mOutline + border.color: Color.mOutline border.width: Math.max(1, Style.borderThin * scaling) } diff --git a/Widgets/NCircleStat.qml b/Widgets/NCircleStat.qml index 81d66bf..cc33052 100644 --- a/Widgets/NCircleStat.qml +++ b/Widgets/NCircleStat.qml @@ -18,9 +18,9 @@ Rectangle { width: 68 * scaling height: 92 * scaling - color: flat ? "transparent" : Colors.mSurface + color: flat ? "transparent" : Color.mSurface radius: Style.radiusSmall * scaling - border.color: flat ? "transparent" : Colors.mSurfaceVariant + border.color: flat ? "transparent" : Color.mSurfaceVariant border.width: flat ? 0 : Math.max(1, Style.borderThin * scaling) clip: true @@ -58,14 +58,14 @@ Rectangle { ctx.reset() ctx.lineWidth = 6 * scaling * contentScale // Track uses surfaceVariant for stronger contrast - ctx.strokeStyle = Colors.mSurface + ctx.strokeStyle = Color.mSurface ctx.beginPath() ctx.arc(cx, cy, r, start, endBg) ctx.stroke() // Value arc const ratio = Math.max(0, Math.min(1, root.value / 100)) const end = start + (endBg - start) * ratio - ctx.strokeStyle = Colors.mPrimary + ctx.strokeStyle = Color.mPrimary ctx.beginPath() ctx.arc(cx, cy, r, start, end) ctx.stroke() @@ -79,7 +79,7 @@ Rectangle { text: `${root.value}${root.suffix}` font.pointSize: Style.fontSizeMedium * scaling * contentScale font.weight: Style.fontWeightBold - color: Colors.mOnSurface + color: Color.mOnSurface horizontalAlignment: Text.AlignHCenter } @@ -89,8 +89,8 @@ Rectangle { width: 28 * scaling * contentScale height: width radius: width / 2 - color: Colors.mSurface - // border.color: Colors.mPrimary + color: Color.mSurface + // border.color: Color.mPrimary // border.width: Math.max(1, Style.borderThin * scaling) anchors.right: parent.right anchors.top: parent.top @@ -102,7 +102,7 @@ Rectangle { text: root.icon font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeLargeXL * scaling * contentScale - color: Colors.mOnSurface + color: Color.mOnSurface horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } diff --git a/Widgets/NComboBox.qml b/Widgets/NComboBox.qml index fa4ffa1..671289e 100644 --- a/Widgets/NComboBox.qml +++ b/Widgets/NComboBox.qml @@ -29,12 +29,12 @@ ColumnLayout { text: label font.pointSize: Style.fontSizeMedium * scaling font.weight: Style.fontWeightBold - color: Colors.mOnSurface + color: Color.mOnSurface } NText { text: description font.pointSize: Style.fontSizeSmall * scaling - color: Colors.mOnSurface + color: Color.mOnSurface wrapMode: Text.WordWrap } } @@ -61,8 +61,8 @@ ColumnLayout { background: Rectangle { implicitWidth: Style.baseWidgetSize * 3.75 * scaling implicitHeight: preferredHeight - color: Colors.mSurface - border.color: combo.activeFocus ? Colors.mTertiary : Colors.mOutline + color: Color.mSurface + border.color: combo.activeFocus ? Color.mTertiary : Color.mOutline border.width: Math.max(1, Style.borderThin * scaling) radius: Style.radiusMedium * scaling } @@ -118,22 +118,22 @@ ColumnLayout { contentItem: NText { text: name font.pointSize: Style.fontSizeMedium * scaling - color: highlighted ? Colors.mSurface : Colors.mOnSurface + color: highlighted ? Color.mSurface : Color.mOnSurface verticalAlignment: Text.AlignVCenter elide: Text.ElideRight } background: Rectangle { width: combo.width - Style.marginMedium * scaling * 3 - color: highlighted ? Colors.mTertiary : "transparent" + color: highlighted ? Color.mTertiary : "transparent" radius: Style.radiusSmall * scaling } } } background: Rectangle { - color: Colors.mSurfaceVariant - border.color: Colors.mOutline + color: Color.mSurfaceVariant + border.color: Color.mOutline border.width: Math.max(1, Style.borderThin * scaling) radius: Style.radiusMedium * scaling } diff --git a/Widgets/NDivider.qml b/Widgets/NDivider.qml index ca7cbcb..0448a31 100644 --- a/Widgets/NDivider.qml +++ b/Widgets/NDivider.qml @@ -7,5 +7,5 @@ import qs.Services Rectangle { width: parent.width height: Math.max(1, Style.borderThin * scaling) - color: Colors.mOutline + color: Color.mOutline } diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml index 6b74153..2d4b3e8 100644 --- a/Widgets/NIconButton.qml +++ b/Widgets/NIconButton.qml @@ -26,9 +26,9 @@ Rectangle { implicitWidth: size implicitHeight: size - color: (root.hovering || showFilled) ? Colors.mPrimary : "transparent" + color: (root.hovering || showFilled) ? Color.mPrimary : "transparent" radius: width * 0.5 - border.color: showBorder ? Colors.mPrimary : "transparent" + border.color: showBorder ? Color.mPrimary : "transparent" border.width: Math.max(1, Style.borderThin * scaling) NText { @@ -42,7 +42,7 @@ Rectangle { font.variableAxes: { "wght": (Font.Normal + Font.Bold) / 2.0 } - color: (root.hovering || showFilled) ? Colors.mOnPrimary : showBorder ? Colors.mPrimary : Colors.mOnSurface + color: (root.hovering || showFilled) ? Color.mOnPrimary : showBorder ? Color.mPrimary : Color.mOnSurface horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter opacity: root.enabled ? Style.opacityFull : Style.opacityMedium diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 3407a70..8e0ce6d 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -11,7 +11,7 @@ PanelWindow { property bool showOverlay: Settings.data.general.dimDesktop property int topMargin: Style.barHeight * scaling - property color overlayColor: showOverlay ? Colors.applyOpacity(Colors.mShadow, "AA") : "transparent" + property color overlayColor: showOverlay ? Color.applyOpacity(Color.mShadow, "AA") : "transparent" signal dismissed function hide() { diff --git a/Widgets/NPill.qml b/Widgets/NPill.qml index 4ed4f46..b5ce985 100644 --- a/Widgets/NPill.qml +++ b/Widgets/NPill.qml @@ -9,11 +9,11 @@ Item { property string icon: "" property string text: "" property string tooltipText: "" - property color pillColor: Colors.mSurfaceVariant - property color textColor: Colors.mOnSurface - property color iconCircleColor: Colors.mPrimary - property color iconTextColor: Colors.mSurface - property color collapsedIconColor: Colors.mOnSurface + property color pillColor: Color.mSurfaceVariant + property color textColor: Color.mOnSurface + property color iconCircleColor: Color.mPrimary + property color iconTextColor: Color.mSurface + property color collapsedIconColor: Color.mOnSurface property real sizeMultiplier: 0.8 property bool autoHide: false diff --git a/Widgets/NRadioButton.qml b/Widgets/NRadioButton.qml index 34b7f20..b3c92cf 100644 --- a/Widgets/NRadioButton.qml +++ b/Widgets/NRadioButton.qml @@ -14,7 +14,7 @@ RadioButton { implicitHeight: Style.baseWidgetSize * 0.625 * scaling radius: width * 0.5 color: "transparent" - border.color: root.checked ? Colors.mPrimary : Colors.mOnSurface + border.color: root.checked ? Color.mPrimary : Color.mOnSurface border.width: Math.max(1, Style.borderMedium * scaling) anchors.verticalCenter: parent.verticalCenter @@ -24,7 +24,7 @@ RadioButton { implicitHeight: Style.marginSmall * scaling radius: width * 0.5 - color: Qt.alpha(Colors.mPrimary, root.checked ? 1 : 0) + color: Qt.alpha(Color.mPrimary, root.checked ? 1 : 0) } Behavior on border.color { diff --git a/Widgets/NSlider.qml b/Widgets/NSlider.qml index 642dbd7..a013959 100644 --- a/Widgets/NSlider.qml +++ b/Widgets/NSlider.qml @@ -26,13 +26,13 @@ Slider { width: root.availableWidth height: implicitHeight radius: height / 2 - color: Colors.mSurface + color: Color.mSurface Rectangle { id: activeTrack width: root.visualPosition * parent.width height: parent.height - color: Colors.mPrimary + color: Color.mPrimary radius: parent.radius } @@ -42,7 +42,7 @@ Slider { width: knobDiameter + cutoutExtra height: knobDiameter + cutoutExtra radius: width / 2 - color: root.cutoutColor !== undefined ? root.cutoutColor : Colors.mSurface + color: root.cutoutColor !== undefined ? root.cutoutColor : Color.mSurface x: Math.max(0, Math.min(parent.width - width, root.visualPosition * (parent.width - root.knobDiameter) - cutoutExtra / 2)) y: (parent.height - height) / 2 @@ -60,7 +60,7 @@ Slider { anchors.fill: knob source: knob shadowEnabled: true - shadowColor: Colors.mShadow + shadowColor: Color.mShadow shadowOpacity: 0.25 shadowHorizontalOffset: 0 shadowVerticalOffset: 1 @@ -72,8 +72,8 @@ Slider { implicitWidth: knobDiameter implicitHeight: knobDiameter radius: width * 0.5 - color: root.pressed ? Colors.mSurfaceVariant : Colors.mSurface - border.color: Colors.mPrimary + color: root.pressed ? Color.mSurfaceVariant : Color.mSurface + border.color: Color.mPrimary border.width: Math.max(1, Style.borderThick * scaling) // Press feedback halo (using accent color, low opacity) @@ -82,7 +82,7 @@ Slider { width: parent.width + 8 * scaling height: parent.height + 8 * scaling radius: width / 2 - color: Colors.mPrimary + color: Color.mPrimary opacity: root.pressed ? 0.16 : 0.0 } } diff --git a/Widgets/NText.qml b/Widgets/NText.qml index 656f632..8836300 100644 --- a/Widgets/NText.qml +++ b/Widgets/NText.qml @@ -9,5 +9,5 @@ Text { font.family: Settings.data.ui.fontFamily font.pointSize: Style.fontSizeMedium * scaling font.weight: Style.fontWeightRegular - color: Colors.mOnSurface + color: Color.mOnSurface } diff --git a/Widgets/NTextInput.qml b/Widgets/NTextInput.qml index 0ced0f0..7441001 100644 --- a/Widgets/NTextInput.qml +++ b/Widgets/NTextInput.qml @@ -29,13 +29,13 @@ Item { text: label font.pointSize: Style.fontSizeMedium * scaling font.weight: Style.fontWeightBold - color: Colors.mOnSurface + color: Color.mOnSurface } NText { text: description font.pointSize: Style.fontSizeSmall * scaling - color: Colors.mOnSurface + color: Color.mOnSurface wrapMode: Text.WordWrap Layout.fillWidth: true } @@ -47,8 +47,8 @@ Item { implicitWidth: root.width implicitHeight: Style.baseWidgetSize * 1.35 * scaling radius: Style.radiusMedium * scaling - color: Colors.mSurface - border.color: Colors.mOutline + color: Color.mSurface + border.color: Color.mOutline border.width: Math.max(1, Style.borderThin * scaling) // Focus ring @@ -56,7 +56,7 @@ Item { anchors.fill: parent radius: frame.radius color: "transparent" - border.color: input.activeFocus ? Colors.mTertiary : "transparent" + border.color: input.activeFocus ? Color.mTertiary : "transparent" border.width: input.activeFocus ? Math.max(1, Style.borderThin * scaling) : 0 } @@ -74,8 +74,8 @@ Item { echoMode: TextInput.Normal readOnly: root.readOnly enabled: root.enabled - color: Colors.mOnSurface - placeholderTextColor: Colors.mOnSurface + color: Color.mOnSurface + placeholderTextColor: Color.mOnSurface background: null font.pointSize: Style.fontSizeSmall * scaling onEditingFinished: root.editingFinished() diff --git a/Widgets/NToggle.qml b/Widgets/NToggle.qml index eddb228..e1cae48 100644 --- a/Widgets/NToggle.qml +++ b/Widgets/NToggle.qml @@ -27,13 +27,13 @@ RowLayout { text: label font.pointSize: Style.fontSizeMedium * scaling font.weight: Style.fontWeightBold - color: Colors.mOnSurface + color: Color.mOnSurface } NText { text: description font.pointSize: Style.fontSizeSmall * scaling - color: Colors.mOnSurface + color: Color.mOnSurface wrapMode: Text.WordWrap Layout.fillWidth: true } @@ -45,16 +45,16 @@ RowLayout { implicitWidth: root.baseSize * 1.625 * scaling implicitHeight: root.baseSize * scaling radius: height * 0.5 - color: root.checked ? Colors.mPrimary : Colors.mSurface - border.color: root.checked ? Colors.mPrimary : Colors.mOutline + color: root.checked ? Color.mPrimary : Color.mSurface + border.color: root.checked ? Color.mPrimary : Color.mOutline border.width: Math.max(1, Style.borderMedium * scaling) Rectangle { implicitWidth: (root.baseSize - 5) * scaling implicitHeight: (root.baseSize - 5) * scaling radius: height * 0.5 - color: root.checked ? Colors.mOnPrimary : Colors.mPrimary - border.color: root.checked ? Colors.mSurface : Colors.mSurface + color: root.checked ? Color.mOnPrimary : Color.mPrimary + border.color: root.checked ? Color.mSurface : Color.mSurface border.width: Math.max(1, Style.borderMedium * scaling) y: 2 * scaling x: root.checked ? switcher.width - width - 2 * scaling : 2 * scaling diff --git a/Widgets/NTooltip.qml b/Widgets/NTooltip.qml index aba0660..2d02be9 100644 --- a/Widgets/NTooltip.qml +++ b/Widgets/NTooltip.qml @@ -135,8 +135,8 @@ Window { id: tooltipRect anchors.fill: parent radius: Style.radiusMedium * scaling - color: Colors.mSurface - border.color: Colors.mOutline + color: Color.mSurface + border.color: Color.mOutline border.width: Math.max(1, Style.borderThin * scaling) z: 1 From fbfda31733e5f80146051a63a05e5992961393eb Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sat, 16 Aug 2025 23:00:25 -0400 Subject: [PATCH 358/394] Improved ActiveWindow --- Modules/Bar/ActiveWindow.qml | 103 +++++++++++------------------------ 1 file changed, 31 insertions(+), 72 deletions(-) diff --git a/Modules/Bar/ActiveWindow.qml b/Modules/Bar/ActiveWindow.qml index 62550db..d4df13d 100644 --- a/Modules/Bar/ActiveWindow.qml +++ b/Modules/Bar/ActiveWindow.qml @@ -22,7 +22,7 @@ Row { repeat: false onTriggered: { showingFullTitle = false - titleText.text = getDisplayText() + // No need to update text here, the width property change will handle it } } @@ -36,13 +36,34 @@ Row { showingFullTitle = true fullTitleTimer.restart() } - titleText.text = getDisplayText() } } + function getFocusedWindow() { + if (typeof NiriService === "undefined" || NiriService.focusedWindowIndex < 0 + || NiriService.focusedWindowIndex >= NiriService.windows.length) { + return null + } + return NiriService.windows[NiriService.focusedWindowIndex] + } + + function getTitle() { + const focusedWindow = getFocusedWindow() + return focusedWindow ? (focusedWindow.title || focusedWindow.appId || "") : "" + } + + // A hidden text element to safely measure the full title width + NText { + id: fullTitleMetrics + visible: false + text: getTitle() + font: titleText.font + } + Rectangle { - width: row.width + Style.marginSmall * scaling * 2 - height: row.height + // Let the Rectangle size itself based on its content (the Row) + width: row.width + Style.marginMedium* scaling * 2 + height: row.height + Style.marginSmall * scaling color: Color.mSurfaceVariant radius: Style.radiusSmall * scaling anchors.verticalCenter: parent.verticalCenter @@ -66,19 +87,20 @@ Row { font.pointSize: Style.fontSizeLarge * scaling verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter - visible: getDisplayText() !== "" + visible: getTitle() !== "" } NText { id: titleText - width: (showingFullTitle || mouseArea.containsMouse) ? 300 * scaling : 100 * scaling - text: getDisplayText() + width: (showingFullTitle || mouseArea.containsMouse) ? fullTitleMetrics.contentWidth : 150 * scaling + text: getTitle() font.pointSize: Style.fontSizeReduced * scaling font.weight: Style.fontWeightBold elide: Text.ElideRight anchors.verticalCenter: parent.verticalCenter verticalAlignment: Text.AlignVCenter color: Color.mTertiary + Behavior on width { NumberAnimation { duration: Style.animationSlow @@ -87,77 +109,14 @@ Row { } } } + // Mouse area for hover detection MouseArea { id: mouseArea anchors.fill: parent hoverEnabled: true - cursorShape: Qt.IBeamCursor - onEntered: { - titleText.text = getDisplayText() - } - onExited: { - titleText.text = getDisplayText() - } + cursorShape: Qt.PointingHandCursor } } } - function getDisplayText() { - // Check if Niri service is available - if (typeof NiriService === "undefined") { - return "" - } - - // Get the focused window data - const focusedWindow = NiriService.focusedWindowIndex >= 0 && NiriService.focusedWindowIndex - < NiriService.windows.length ? NiriService.windows[NiriService.focusedWindowIndex] : null - - if (!focusedWindow) { - return "" - } - - const appId = focusedWindow.appId || "" - const title = focusedWindow.title || "" - - // If no appId, fall back to title processing - if (!appId) { - if (!title || title === "(No active window)" || title === "(Unnamed window)") { - return "" - } - - // Extract program name from title (before first space or special characters) - const programName = title.split(/[\s\-_]/)[0] - - if (programName.length <= 2 || programName === title) { - return title - } - - if (isGenericName(programName)) { - return title - } - - return programName - } - - return title - - // // Use appId for program name, show full title on hover or window switch - // if (showingFullTitle || mouseArea.containsMouse) { - // return truncateTitle(title || appId, 50) - // } else { - // return truncateTitle(title || appId, 20) - // } - } - - function truncateTitle(title, length) { - if (title.length > length) { - return title.substring(0, length - 3) + "..." - } - return title - } - - function isGenericName(name) { - const genericNames = ["window", "application", "app", "program", "process", "unknown"] - return genericNames.includes(name.toLowerCase()) - } } From 2b1893f8bc1d168aba5114fb3f162bb767d745a9 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sat, 16 Aug 2025 23:12:14 -0400 Subject: [PATCH 359/394] ActiveWindow fully debugged --- Modules/Bar/ActiveWindow.qml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Modules/Bar/ActiveWindow.qml b/Modules/Bar/ActiveWindow.qml index d4df13d..80dd870 100644 --- a/Modules/Bar/ActiveWindow.qml +++ b/Modules/Bar/ActiveWindow.qml @@ -7,7 +7,7 @@ import qs.Services import qs.Widgets Row { - id: layout + id: root anchors.verticalCenter: parent.verticalCenter spacing: Style.marginSmall * scaling visible: Settings.data.bar.showActiveWindow @@ -22,7 +22,6 @@ Row { repeat: false onTriggered: { showingFullTitle = false - // No need to update text here, the width property change will handle it } } @@ -56,13 +55,13 @@ Row { NText { id: fullTitleMetrics visible: false - text: getTitle() + text: titleText.text font: titleText.font } Rectangle { // Let the Rectangle size itself based on its content (the Row) - width: row.width + Style.marginMedium* scaling * 2 + width: row.width + Style.marginMedium * scaling * 2 height: row.height + Style.marginSmall * scaling color: Color.mSurfaceVariant radius: Style.radiusSmall * scaling @@ -92,7 +91,11 @@ Row { NText { id: titleText - width: (showingFullTitle || mouseArea.containsMouse) ? fullTitleMetrics.contentWidth : 150 * scaling + + // If hovered or just switched window, show up to 300 pixels + // If not hovered show up to 150 pixels + width: (showingFullTitle || mouseArea.containsMouse) ? Math.min(fullTitleMetrics.contentWidth, 300 * scaling) : Math.min( + fullTitleMetrics.contentWidth, 150 * scaling) text: getTitle() font.pointSize: Style.fontSizeReduced * scaling font.weight: Style.fontWeightBold @@ -104,7 +107,7 @@ Row { Behavior on width { NumberAnimation { duration: Style.animationSlow - easing.type: Easing.OutBack + easing.type: Easing.InOutCubic } } } From e3ea79073d53f2a9d4b70e15e886175621411bc8 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sat, 16 Aug 2025 23:29:25 -0400 Subject: [PATCH 360/394] MediaMini rebuilt to behave the same as activewindow --- Modules/Bar/ActiveWindow.qml | 7 ++- Modules/Bar/MediaMini.qml | 110 +++++++++++++++++++++++------------ 2 files changed, 78 insertions(+), 39 deletions(-) diff --git a/Modules/Bar/ActiveWindow.qml b/Modules/Bar/ActiveWindow.qml index 80dd870..c2ffe6e 100644 --- a/Modules/Bar/ActiveWindow.qml +++ b/Modules/Bar/ActiveWindow.qml @@ -10,7 +10,7 @@ Row { id: root anchors.verticalCenter: parent.verticalCenter spacing: Style.marginSmall * scaling - visible: Settings.data.bar.showActiveWindow + visible: (Settings.data.bar.showActiveWindow && getTitle() !== "") property bool showingFullTitle: false property int lastWindowIndex: -1 @@ -94,7 +94,8 @@ Row { // If hovered or just switched window, show up to 300 pixels // If not hovered show up to 150 pixels - width: (showingFullTitle || mouseArea.containsMouse) ? Math.min(fullTitleMetrics.contentWidth, 300 * scaling) : Math.min( + width: (showingFullTitle || mouseArea.containsMouse) ? Math.min(fullTitleMetrics.contentWidth, + 300 * scaling) : Math.min( fullTitleMetrics.contentWidth, 150 * scaling) text: getTitle() font.pointSize: Style.fontSizeReduced * scaling @@ -102,7 +103,7 @@ Row { elide: Text.ElideRight anchors.verticalCenter: parent.verticalCenter verticalAlignment: Text.AlignVCenter - color: Color.mTertiary + color: Color.mSecondary Behavior on width { NumberAnimation { diff --git a/Modules/Bar/MediaMini.qml b/Modules/Bar/MediaMini.qml index 86e4bc8..4c0fe9c 100644 --- a/Modules/Bar/MediaMini.qml +++ b/Modules/Bar/MediaMini.qml @@ -1,54 +1,92 @@ import QtQuick +import QtQuick.Controls import QtQuick.Layouts +import Quickshell import qs.Commons import qs.Services import qs.Widgets -Item { +Row { id: root - - width: visible ? mediaRow.width : 0 - height: Style.barHeight * scaling + anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginSmall * scaling visible: Settings.data.bar.showMedia && (MediaService.canPlay || MediaService.canPause) - RowLayout { - id: mediaRow - height: parent.height - spacing: Style.spacingTiniest * scaling + function getTitle() { + return MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - ${MediaService.trackArtist}` : "") + } - // NIconButton { - // icon: MediaService.isPlaying ? "pause" : "play_arrow" - // tooltipText: "Play/pause media" - // sizeMultiplier: 0.8 - // showBorder: false - // onClicked: MediaService.playPause() - // } - NText { - text: MediaService.isPlaying ? "pause" : "play_arrow" - font.family: "Material Symbols Outlined" - font.pointSize: Style.fontSizeLarge * scaling - verticalAlignment: Text.AlignVCenter + // A hidden text element to safely measure the full title width + NText { + id: fullTitleMetrics + visible: false + text: titleText.text + font: titleText.font + } - MouseArea { - id: titleContainerMouseArea - anchors.fill: parent + Rectangle { + // Let the Rectangle size itself based on its content (the Row) + width: row.width + Style.marginMedium * scaling * 2 + height: row.height + Style.marginSmall * scaling + color: Color.mSurfaceVariant + radius: Style.radiusSmall * scaling + anchors.verticalCenter: parent.verticalCenter - onClicked: { - onClicked: MediaService.playPause() + Item { + id: mainContainer + anchors.fill: parent + anchors.leftMargin: Style.marginSmall * scaling + anchors.rightMargin: Style.marginSmall * scaling + + Row { + id: row + anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginTiny * scaling + + // Window icon + NText { + id: windowIcon + text: MediaService.isPlaying ? "pause" : "play_arrow" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeLarge * scaling + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + visible: getTitle() !== "" + } + + NText { + id: titleText + + // If hovered or just switched window, show up to 300 pixels + // If not hovered show up to 150 pixels + width: (mouseArea.containsMouse) ? Math.min(fullTitleMetrics.contentWidth, + 400 * scaling) : Math.min( + fullTitleMetrics.contentWidth, 150 * scaling) + text: getTitle() + font.pointSize: Style.fontSizeReduced * scaling + font.weight: Style.fontWeightBold + elide: Text.ElideRight + anchors.verticalCenter: parent.verticalCenter + verticalAlignment: Text.AlignVCenter + color: Color.mSecondary + + Behavior on width { + NumberAnimation { + duration: Style.animationSlow + easing.type: Easing.InOutCubic + } + } } } - } - // Track info - NText { - text: MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - {MediaService.trackArtist}` : "") - font.pointSize: Style.fontSizeReduced * scaling - font.weight: Style.fontWeightBold - elide: Text.ElideRight - color: Color.mSecondary - verticalAlignment: Text.AlignVCenter - Layout.maximumWidth: 200 * scaling - Layout.alignment: Qt.AlignVCenter + // Mouse area for hover detection + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: MediaService.playPause() + } } } } From 0223e3d9d163e1f83975733500bc0f8f1b617ba4 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sat, 16 Aug 2025 23:46:02 -0400 Subject: [PATCH 361/394] formatting --- Modules/Bar/MediaMini.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/Bar/MediaMini.qml b/Modules/Bar/MediaMini.qml index 4c0fe9c..44be81e 100644 --- a/Modules/Bar/MediaMini.qml +++ b/Modules/Bar/MediaMini.qml @@ -60,8 +60,8 @@ Row { // If hovered or just switched window, show up to 300 pixels // If not hovered show up to 150 pixels width: (mouseArea.containsMouse) ? Math.min(fullTitleMetrics.contentWidth, - 400 * scaling) : Math.min( - fullTitleMetrics.contentWidth, 150 * scaling) + 400 * scaling) : Math.min(fullTitleMetrics.contentWidth, + 150 * scaling) text: getTitle() font.pointSize: Style.fontSizeReduced * scaling font.weight: Style.fontWeightBold From 4225cbe704c00e4c34720f5bc10b2ba924909369 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 17 Aug 2025 00:13:35 -0400 Subject: [PATCH 362/394] Brightness: Multi-monitor working (no external modification detected) --- Modules/Bar/Brightness.qml | 40 +++++++++---------- Services/BrightnessService.qml | 72 +++++----------------------------- 2 files changed, 29 insertions(+), 83 deletions(-) diff --git a/Modules/Bar/Brightness.qml b/Modules/Bar/Brightness.qml index a065937..7d47b53 100644 --- a/Modules/Bar/Brightness.qml +++ b/Modules/Bar/Brightness.qml @@ -14,35 +14,34 @@ Item { // Used to avoid opening the pill on Quickshell startup property bool firstBrightnessReceived: false - property real lastBrightness: -1 function getIcon() { - if (!BrightnessService.available) { - return "brightness_auto" - } - var brightness = BrightnessService.brightness - return brightness <= 0 ? "brightness_1" : brightness < 33 ? "brightness_low" : brightness < 66 ? "brightness_medium" : "brightness_high" + var brightness = BrightnessService.getMonitorForScreen(screen).brightness + return brightness <= 0 ? "brightness_1" : brightness < 0.33 ? "brightness_low" : brightness < 0.66 ? "brightness_medium" : "brightness_high" } // Connection used to open the pill when brightness changes Connections { - target: BrightnessService.focusedMonitor + target: BrightnessService.getMonitorForScreen(screen) function onBrightnessUpdated() { - var currentBrightness = BrightnessService.brightness + Logger.log("Bar-Brightness", "OnBrightnessUpdated") + + var monitor = BrightnessService.getMonitorForScreen(screen); + var currentBrightness = monitor.brightness // Ignore if this is the first time or if brightness hasn't actually changed if (!firstBrightnessReceived) { firstBrightnessReceived = true - lastBrightness = currentBrightness + monitor.lastBrightness = currentBrightness return } // Only show pill if brightness actually changed (not just loaded from settings) - if (Math.abs(currentBrightness - lastBrightness) > 0.1) { + if (Math.abs(currentBrightness - monitor.lastBrightness) > 0.1) { pill.show() } - lastBrightness = currentBrightness + monitor.lastBrightness = currentBrightness } } @@ -52,21 +51,22 @@ Item { iconCircleColor: Color.mPrimary collapsedIconColor: Color.mOnSurface autoHide: false // Important to be false so we can hover as long as we want - text: Math.round(BrightnessService.brightness) + "%" - tooltipText: "Brightness: " + Math.round( - BrightnessService.brightness) + "%\nMethod: " + BrightnessService.currentMethod - + "\nLeft click for advanced settings.\nScroll up/down to change brightness." + text: Math.round(BrightnessService.getMonitorForScreen(screen).brightness * 100) + "%" + tooltipText: { + var monitor = BrightnessService.getMonitorForScreen(screen) + return "Brightness: " + Math.round(monitor.brightness * 100) + "%\nMethod: " + monitor.method + + "\nLeft click for advanced settings.\nScroll up/down to change brightness." + } onWheel: function (angle) { - if (!BrightnessService.available) - return - + var monitor = BrightnessService.getMonitorForScreen(screen) if (angle > 0) { - BrightnessService.increaseBrightness() + monitor.increaseBrightness() } else if (angle < 0) { - BrightnessService.decreaseBrightness() + monitor.decreaseBrightness() } } + onClicked: { settingsPanel.requestedTab = SettingsPanel.Tab.Brightness settingsPanel.isLoaded = true diff --git a/Services/BrightnessService.qml b/Services/BrightnessService.qml index 306bca4..edfe198 100644 --- a/Services/BrightnessService.qml +++ b/Services/BrightnessService.qml @@ -14,73 +14,10 @@ Singleton { readonly property list monitors: variants.instances property bool appleDisplayPresent: false - // Public properties for backward compatibility - readonly property real brightness: focusedMonitor ? focusedMonitor.brightness * 100 : Settings.data.brightness.lastBrightness - readonly property bool available: focusedMonitor !== null - readonly property string currentMethod: focusedMonitor ? focusedMonitor.method : Settings.data.brightness.lastMethod - readonly property var detectedDisplays: monitors.map(m => ({ - "name": m.modelData.name, - "type": m.isDdc ? "external" : "internal", - "method": m.method, - "index": m.busNum - })) - - // Get the currently focused monitor - readonly property Monitor focusedMonitor: { - if (monitors.length === 0) - return null - // For now, return the first monitor. Could be enhanced to detect focused monitor - return monitors[0] - } - function getMonitorForScreen(screen: ShellScreen): var { return monitors.find(m => m.modelData === screen) } - function increaseBrightness(step = null): void { - if (focusedMonitor) { - var stepSize = step !== null ? step : Settings.data.brightness.brightnessStep - focusedMonitor.setBrightness(focusedMonitor.brightness + (stepSize / 100)) - } - } - - function decreaseBrightness(step = null): void { - if (focusedMonitor) { - var stepSize = step !== null ? step : Settings.data.brightness.brightnessStep - focusedMonitor.setBrightness(focusedMonitor.brightness - (stepSize / 100)) - } - } - - function setBrightness(newBrightness: real): void { - if (focusedMonitor) { - focusedMonitor.setBrightness(newBrightness / 100) - } - } - - function setBrightnessDebounced(newBrightness: real): void { - if (focusedMonitor) { - focusedMonitor.setBrightnessDebounced(newBrightness / 100) - } - } - - // Backward compatibility functions - function updateBrightness(): void {// No longer needed with the new architecture - } - - function setDisplay(displayIndex: int): bool { - // No longer needed with the new architecture - return true - } - - function getDisplayInfo(): var { - return focusedMonitor ? { - "name": focusedMonitor.modelData.name, - "type": focusedMonitor.isDdc ? "external" : "internal", - "method": focusedMonitor.method, - "index": focusedMonitor.busNum - } : null - } - function getAvailableMethods(): list { var methods = [] if (monitors.some(m => m.isDdc)) @@ -147,6 +84,7 @@ Singleton { readonly property string method: isAppleDisplay ? "apple" : (isDdc ? "ddcutil" : "internal") property real brightness: getStoredBrightness() + property real lastBrightness: 0 property real queuedBrightness: NaN // Signal for brightness changes @@ -207,6 +145,14 @@ Singleton { } } + function increaseBrightness(): void { + setBrightness(brightness + 0.1) + } + + function decreaseBrightness(): void { + setBrightness(monitor.brightness - 0.1) + } + function getStoredBrightness(): real { // Try to get stored brightness for this specific monitor var stored = Settings.data.brightness.monitorBrightness.find(m => { From 629b4c5fd798c92a4bb1322e3791cd9b34336d01 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 17 Aug 2025 00:21:57 -0400 Subject: [PATCH 363/394] Removed Brightness slider from the DemoPanel, to avoid maintaining to much fragile code. --- Modules/{Demo => DemoPanel}/DemoPanel.qml | 56 ----------------------- shell.qml | 2 +- 2 files changed, 1 insertion(+), 57 deletions(-) rename Modules/{Demo => DemoPanel}/DemoPanel.qml (81%) diff --git a/Modules/Demo/DemoPanel.qml b/Modules/DemoPanel/DemoPanel.qml similarity index 81% rename from Modules/Demo/DemoPanel.qml rename to Modules/DemoPanel/DemoPanel.qml index be28857..f05e380 100644 --- a/Modules/Demo/DemoPanel.qml +++ b/Modules/DemoPanel/DemoPanel.qml @@ -302,62 +302,6 @@ NLoader { Layout.fillWidth: true } } - - // Brightness Control - ColumnLayout { - spacing: Style.marginMedium * scaling - NText { - text: "Brightness Control" - color: Color.mSecondary - font.weight: Style.fontWeightBold - } - - NText { - text: `Brightness: ${Math.round(BrightnessService.brightness)}%` - Layout.alignment: Qt.AlignVCenter - } - - RowLayout { - spacing: Style.marginSmall * scaling - NIconButton { - icon: "brightness_low" - tooltipText: "Decrease Brightness" - fontPointSize: Style.fontSizeLarge * scaling - onClicked: { - BrightnessService.decreaseBrightness() - } - } - NSlider { - from: 0 - to: 100 - stepSize: 1 - value: BrightnessService.brightness - implicitWidth: bgRect.width * 0.5 - onMoved: { - BrightnessService.setBrightnessDebounced(value) - } - } - NIconButton { - icon: "brightness_high" - tooltipText: "Increase Brightness" - fontPointSize: Style.fontSizeLarge * scaling - onClicked: { - BrightnessService.increaseBrightness() - } - } - } - - NText { - text: `Method: ${BrightnessService.currentMethod} | Available: ${BrightnessService.available}` - color: Color.mOnSurfaceVariant - font.pointSize: Style.fontSizeSmall * scaling - Layout.alignment: Qt.AlignHCenter - } - - NDivider { - Layout.fillWidth: true - } - } } } } diff --git a/shell.qml b/shell.qml index 5134397..6415d86 100644 --- a/shell.qml +++ b/shell.qml @@ -9,7 +9,7 @@ import qs.Modules.AppLauncher import qs.Modules.Background import qs.Modules.Bar import qs.Modules.Calendar -import qs.Modules.Demo +import qs.Modules.DemoPanel import qs.Modules.Dock import qs.Modules.IPC import qs.Modules.LockScreen From c991ac85b43baa503ce411fc77b8b54c37d73294 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 17 Aug 2025 00:23:47 -0400 Subject: [PATCH 364/394] Brightness: always debounce, DDCUtil is painfully slow --- Services/BrightnessService.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Services/BrightnessService.qml b/Services/BrightnessService.qml index edfe198..1fcfee9 100644 --- a/Services/BrightnessService.qml +++ b/Services/BrightnessService.qml @@ -146,11 +146,11 @@ Singleton { } function increaseBrightness(): void { - setBrightness(brightness + 0.1) + setBrightnessDebounced(brightness + 0.1) } function decreaseBrightness(): void { - setBrightness(monitor.brightness - 0.1) + setBrightnessDebounced(monitor.brightness - 0.1) } function getStoredBrightness(): real { From 05f9acdc5dc13382aeca1e7b915eedd57eed3599 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 17 Aug 2025 10:19:51 +0200 Subject: [PATCH 365/394] Add CompositorService, make Logger look a bit nicer --- Assets/ColorSchemes/Noctalia.json | 20 ++ Commons/Logger.qml | 2 +- Modules/Background/Overview.qml | 4 +- Modules/Bar/ActiveWindow.qml | 14 +- Modules/SidePanel/PowerMenu.qml | 20 +- Services/CompositorService.qml | 387 ++++++++++++++++++++++++++++++ Services/NiriService.qml | 138 ----------- Services/WorkspacesService.qml | 142 ++--------- Widgets/NPanel.qml | 43 +++- 9 files changed, 473 insertions(+), 297 deletions(-) create mode 100644 Assets/ColorSchemes/Noctalia.json create mode 100644 Services/CompositorService.qml delete mode 100644 Services/NiriService.qml diff --git a/Assets/ColorSchemes/Noctalia.json b/Assets/ColorSchemes/Noctalia.json new file mode 100644 index 0000000..1a16c59 --- /dev/null +++ b/Assets/ColorSchemes/Noctalia.json @@ -0,0 +1,20 @@ +{ + "mPrimary": "#c7a1d8", + "mOnPrimary": "#1a151f", + "mSecondary": "#a984c4", + "mOnSecondary": "#f3edf7", + "mTertiary": "#e0b7c9", + "mOnTertiary": "#20161f", + + "mError": "#e9899d", + "mOnError": "#1e1418", + + "mSurface": "#1c1822", + "mOnSurface": "#e9e4f0", + "mSurfaceVariant": "#262130", + "mOnSurfaceVariant": "#a79ab0", + "mOutline": "#4d445a", + "mOutlineVariant": "#342c42", + "mShadow": "#120f18" + } + \ No newline at end of file diff --git a/Commons/Logger.qml b/Commons/Logger.qml index dbdb9c4..22a4726 100644 --- a/Commons/Logger.qml +++ b/Commons/Logger.qml @@ -10,7 +10,7 @@ Singleton { var t = Time.getFormattedTimestamp() if (args.length > 1) { const maxLength = 14 - var module = args.shift().substring(0, maxLength).padStart(maxLength, ".") + var module = args.shift().substring(0, maxLength).padStart(maxLength, " ") return `\x1b[36m[${t}]\x1b[0m \x1b[35m${module}\x1b[0m ` + args.join(" ") } else { return `[\x1b[36m[${t}]\x1b[0m ` + args.join(" ") diff --git a/Modules/Background/Overview.qml b/Modules/Background/Overview.qml index c3bea3a..ecd6638 100644 --- a/Modules/Background/Overview.qml +++ b/Modules/Background/Overview.qml @@ -7,10 +7,10 @@ import qs.Services import qs.Widgets NLoader { - active: WorkspacesService.isNiri + active: CompositorService.isNiri Component.onCompleted: { - if (WorkspacesService.isNiri) { + if (CompositorService.isNiri) { Logger.log("Overview", "Loading Overview component (Niri detected)") } else { Logger.log("Overview", "Skipping Overview component (Niri not detected)") diff --git a/Modules/Bar/ActiveWindow.qml b/Modules/Bar/ActiveWindow.qml index c2ffe6e..47cb42b 100644 --- a/Modules/Bar/ActiveWindow.qml +++ b/Modules/Bar/ActiveWindow.qml @@ -27,11 +27,11 @@ Row { // Update text when window changes Connections { - target: typeof NiriService !== "undefined" ? NiriService : null - function onFocusedWindowIndexChanged() { + target: CompositorService + function onActiveWindowChanged() { // Check if window actually changed - if (NiriService.focusedWindowIndex !== lastWindowIndex) { - lastWindowIndex = NiriService.focusedWindowIndex + if (CompositorService.focusedWindowIndex !== lastWindowIndex) { + lastWindowIndex = CompositorService.focusedWindowIndex showingFullTitle = true fullTitleTimer.restart() } @@ -39,11 +39,7 @@ Row { } function getFocusedWindow() { - if (typeof NiriService === "undefined" || NiriService.focusedWindowIndex < 0 - || NiriService.focusedWindowIndex >= NiriService.windows.length) { - return null - } - return NiriService.windows[NiriService.focusedWindowIndex] + return CompositorService.getFocusedWindow() } function getTitle() { diff --git a/Modules/SidePanel/PowerMenu.qml b/Modules/SidePanel/PowerMenu.qml index 8a52447..02d6ed9 100644 --- a/Modules/SidePanel/PowerMenu.qml +++ b/Modules/SidePanel/PowerMenu.qml @@ -348,13 +348,7 @@ NPanel { // ---------------------------------- // System functions function logout() { - if (WorkspacesService.isNiri) { - logoutProcessNiri.running = true - } else if (WorkspacesService.isHyprland) { - logoutProcessHyprland.running = true - } else { - Logger.warn("PowerMenu", "No supported compositor detected for logout") - } + CompositorService.logout() } function suspend() { @@ -390,19 +384,7 @@ NPanel { running: false } - Process { - id: logoutProcessNiri - command: ["niri", "msg", "action", "quit", "--skip-confirmation"] - running: false - } - - Process { - id: logoutProcessHyprland - - command: ["hyprctl", "dispatch", "exit"] - running: false - } Process { id: logoutProcess diff --git a/Services/CompositorService.qml b/Services/CompositorService.qml new file mode 100644 index 0000000..d58d53b --- /dev/null +++ b/Services/CompositorService.qml @@ -0,0 +1,387 @@ +pragma Singleton + +pragma ComponentBehavior + +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Hyprland +import qs.Commons +import qs.Services + +Singleton { + id: root + + // Generic compositor properties + property string compositorType: "unknown" // "hyprland", "niri", or "unknown" + property bool isHyprland: false + property bool isNiri: false + + // Generic workspace and window data + property ListModel workspaces: ListModel {} + property var windows: [] + property int focusedWindowIndex: -1 + property string focusedWindowTitle: "(No active window)" + property bool inOverview: false + + // Generic events + signal workspaceChanged() + signal activeWindowChanged() + signal overviewStateChanged() + signal windowListChanged() + + // Compositor detection + Component.onCompleted: { + detectCompositor() + } + + // Hyprland connections + Connections { + target: Hyprland.workspaces + enabled: isHyprland + function onValuesChanged() { + updateHyprlandWorkspaces() + workspaceChanged() + } + } + + Connections { + target: Hyprland + enabled: isHyprland + function onRawEvent(event) { + updateHyprlandWorkspaces() + workspaceChanged() + } + } + + + + function detectCompositor() { + try { + // Try Hyprland first + if (Hyprland.eventSocketPath) { + compositorType = "hyprland" + isHyprland = true + isNiri = false + initHyprland() + return + } + } catch (e) { + // Hyprland not available + } + + // Try Niri (always available since we handle it directly) + compositorType = "niri" + isHyprland = false + isNiri = true + initNiri() + return + + // No supported compositor found + compositorType = "unknown" + isHyprland = false + isNiri = false + Logger.warn("CompositorService", "No supported compositor detected") + } + + // Hyprland integration + function initHyprland() { + try { + Hyprland.refreshWorkspaces() + updateHyprlandWorkspaces() + setupHyprlandConnections() + Logger.log("CompositorService", "Hyprland initialized successfully") + } catch (e) { + Logger.error("CompositorService", "Error initializing Hyprland:", e) + compositorType = "unknown" + isHyprland = false + } + } + + function setupHyprlandConnections() { + // Connections are set up at the top level, this function just marks that Hyprland is ready + } + + function updateHyprlandWorkspaces() { + if (!isHyprland) return + + workspaces.clear() + try { + const hlWorkspaces = Hyprland.workspaces.values + for (var i = 0; i < hlWorkspaces.length; i++) { + const ws = hlWorkspaces[i] + // Only append workspaces with id >= 1 + if (ws.id >= 1) { + workspaces.append({ + "id": i, + "idx": ws.id, + "name": ws.name || "", + "output": ws.monitor?.name || "", + "isActive": ws.active === true, + "isFocused": ws.focused === true, + "isUrgent": ws.urgent === true + }) + } + } + } catch (e) { + Logger.error("CompositorService", "Error updating Hyprland workspaces:", e) + } + } + + // Niri integration + function initNiri() { + try { + // Start the event stream to receive Niri events + niriEventStream.running = true + // Initial load of workspaces and windows + updateNiriWorkspaces() + updateNiriWindows() + Logger.log("CompositorService", "Niri initialized successfully") + } catch (e) { + Logger.error("CompositorService", "Error initializing Niri:", e) + compositorType = "unknown" + isNiri = false + } + } + + function updateNiriWorkspaces() { + if (!isNiri) return + + // Get workspaces from the Niri process + niriWorkspaceProcess.running = true + } + + function updateNiriWindows() { + if (!isNiri) return + + // Get windows from the Niri process + niriWindowsProcess.running = true + } + + // Niri workspace process + Process { + id: niriWorkspaceProcess + running: false + command: ["niri", "msg", "--json", "workspaces"] + + stdout: SplitParser { + onRead: function (line) { + try { + const workspacesData = JSON.parse(line) + const workspacesList = [] + + for (const ws of workspacesData) { + workspacesList.push({ + "id": ws.id, + "idx": ws.idx, + "name": ws.name || "", + "output": ws.output || "", + "isFocused": ws.is_focused === true, + "isActive": ws.is_active === true, + "isUrgent": ws.is_urgent === true, + "isOccupied": ws.active_window_id ? true : false + }) + } + + workspacesList.sort((a, b) => { + if (a.output !== b.output) { + return a.output.localeCompare(b.output) + } + return a.id - b.id + }) + + // Update the workspaces ListModel + workspaces.clear() + for (var i = 0; i < workspacesList.length; i++) { + workspaces.append(workspacesList[i]) + } + workspaceChanged() + } catch (e) { + Logger.error("CompositorService", "Failed to parse workspaces:", e, line) + } + } + } + } + + // Niri event stream process + Process { + id: niriEventStream + running: false + command: ["niri", "msg", "--json", "event-stream"] + + stdout: SplitParser { + onRead: data => { + try { + const event = JSON.parse(data.trim()) + + if (event.WorkspacesChanged) { + niriWorkspaceProcess.running = true + } else if (event.WindowsChanged) { + try { + const windowsData = event.WindowsChanged.windows + const windowsList = [] + for (const win of windowsData) { + windowsList.push({ + "id": win.id, + "title": win.title || "", + "appId": win.app_id || "", + "workspaceId": win.workspace_id || null, + "isFocused": win.is_focused === true + }) + } + + windowsList.sort((a, b) => a.id - b.id) + windows = windowsList + windowListChanged() + + // Update focused window index + for (var i = 0; i < windowsList.length; i++) { + if (windowsList[i].isFocused) { + focusedWindowIndex = i + break + } + } + updateFocusedWindowTitle() + activeWindowChanged() + } catch (e) { + Logger.error("CompositorService", "Error parsing windows event:", e) + } + } else if (event.WorkspaceActivated) { + niriWorkspaceProcess.running = true + } else if (event.WindowFocusChanged) { + try { + const focusedId = event.WindowFocusChanged.id + if (focusedId) { + focusedWindowIndex = windows.findIndex(w => w.id === focusedId) + if (focusedWindowIndex < 0) { + focusedWindowIndex = 0 + } + } else { + focusedWindowIndex = -1 + } + updateFocusedWindowTitle() + activeWindowChanged() + } catch (e) { + Logger.error("CompositorService", "Error parsing window focus event:", e) + } + } else if (event.OverviewOpenedOrClosed) { + try { + inOverview = event.OverviewOpenedOrClosed.is_open === true + overviewStateChanged() + } catch (e) { + Logger.error("CompositorService", "Error parsing overview state:", e) + } + } + } catch (e) { + Logger.error("CompositorService", "Error parsing event stream:", e, data) + } + } + } + } + + // Niri windows process (for initial load) + Process { + id: niriWindowsProcess + running: false + command: ["niri", "msg", "--json", "windows"] + + stdout: SplitParser { + onRead: function (line) { + try { + const windowsData = JSON.parse(line) + const windowsList = [] + for (const win of windowsData) { + windowsList.push({ + "id": win.id, + "title": win.title || "", + "appId": win.app_id || "", + "workspaceId": win.workspace_id || null, + "isFocused": win.is_focused === true + }) + } + + windowsList.sort((a, b) => a.id - b.id) + windows = windowsList + windowListChanged() + + // Update focused window index + for (var i = 0; i < windowsList.length; i++) { + if (windowsList[i].isFocused) { + focusedWindowIndex = i + break + } + } + updateFocusedWindowTitle() + activeWindowChanged() + } catch (e) { + Logger.error("CompositorService", "Failed to parse windows:", e, line) + } + } + } + } + + function updateFocusedWindowTitle() { + if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) { + focusedWindowTitle = windows[focusedWindowIndex].title || "(Unnamed window)" + } else { + focusedWindowTitle = "(No active window)" + } + } + + // Generic workspace switching + function switchToWorkspace(workspaceId) { + if (isHyprland) { + try { + Hyprland.dispatch(`workspace ${workspaceId}`) + } catch (e) { + Logger.error("CompositorService", "Error switching Hyprland workspace:", e) + } + } else if (isNiri) { + try { + Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", workspaceId.toString()]) + } catch (e) { + Logger.error("CompositorService", "Error switching Niri workspace:", e) + } + } else { + Logger.warn("CompositorService", "No supported compositor detected for workspace switching") + } + } + + // Generic logout/shutdown commands + function logout() { + if (isHyprland) { + try { + Quickshell.execDetached(["hyprctl", "dispatch", "exit"]) + } catch (e) { + Logger.error("CompositorService", "Error logging out from Hyprland:", e) + } + } else if (isNiri) { + try { + Quickshell.execDetached(["niri", "msg", "action", "quit", "--skip-confirmation"]) + } catch (e) { + Logger.error("CompositorService", "Error logging out from Niri:", e) + } + } else { + Logger.warn("CompositorService", "No supported compositor detected for logout") + } + } + + // Get current workspace + function getCurrentWorkspace() { + for (var i = 0; i < workspaces.count; i++) { + const ws = workspaces.get(i) + if (ws.isFocused) { + return ws + } + } + return null + } + + // Get focused window + function getFocusedWindow() { + if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) { + return windows[focusedWindowIndex] + } + return null + } +} \ No newline at end of file diff --git a/Services/NiriService.qml b/Services/NiriService.qml deleted file mode 100644 index 4a8f403..0000000 --- a/Services/NiriService.qml +++ /dev/null @@ -1,138 +0,0 @@ -pragma Singleton - -pragma ComponentBehavior - -import QtQuick -import Quickshell -import Quickshell.Io - -Singleton { - id: root - - property var workspaces: [] - property var windows: [] - property int focusedWindowIndex: -1 - property bool inOverview: false - property string focusedWindowTitle: "(No active window)" - - function updateFocusedWindowTitle() { - if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) { - focusedWindowTitle = windows[focusedWindowIndex].title || "(Unnamed window)" - } else { - focusedWindowTitle = "(No active window)" - } - } - - onWindowsChanged: updateFocusedWindowTitle() - onFocusedWindowIndexChanged: updateFocusedWindowTitle() - - Component.onCompleted: { - eventStream.running = true - } - - Process { - id: workspaceProcess - running: false - command: ["niri", "msg", "--json", "workspaces"] - - stdout: SplitParser { - onRead: function (line) { - try { - const workspacesData = JSON.parse(line) - const workspacesList = [] - - for (const ws of workspacesData) { - workspacesList.push({ - "id": ws.id, - "idx": ws.idx, - "name": ws.name || "", - "output": ws.output || "", - "isFocused": ws.is_focused === true, - "isActive": ws.is_active === true, - "isUrgent": ws.is_urgent === true, - "isOccupied": ws.active_window_id ? true : false - }) - } - - workspacesList.sort((a, b) => { - if (a.output !== b.output) { - return a.output.localeCompare(b.output) - } - return a.id - b.id - }) - - root.workspaces = workspacesList - } catch (e) { - Logger.error("NiriService", "Failed to parse workspaces:", e, line) - } - } - } - } - - Process { - id: eventStream - running: false - command: ["niri", "msg", "--json", "event-stream"] - - stdout: SplitParser { - onRead: data => { - try { - const event = JSON.parse(data.trim()) - - if (event.WorkspacesChanged) { - workspaceProcess.running = true - } else if (event.WindowsChanged) { - try { - const windowsData = event.WindowsChanged.windows - const windowsList = [] - for (const win of windowsData) { - windowsList.push({ - "id": win.id, - "title": win.title || "", - "appId": win.app_id || "", - "workspaceId": win.workspace_id || null, - "isFocused": win.is_focused === true - }) - } - - windowsList.sort((a, b) => a.id - b.id) - root.windows = windowsList - for (var i = 0; i < windowsList.length; i++) { - if (windowsList[i].isFocused) { - root.focusedWindowIndex = i - break - } - } - } catch (e) { - Logger.error("NiriService", "Error parsing windows event:", e) - } - } else if (event.WorkspaceActivated) { - workspaceProcess.running = true - } else if (event.WindowFocusChanged) { - try { - const focusedId = event.WindowFocusChanged.id - if (focusedId) { - root.focusedWindowIndex = root.windows.findIndex(w => w.id === focusedId) - if (root.focusedWindowIndex < 0) { - root.focusedWindowIndex = 0 - } - } else { - root.focusedWindowIndex = -1 - } - } catch (e) { - Logger.error("NiriService", "Error parsing window focus event:", e) - } - } else if (event.OverviewOpenedOrClosed) { - try { - root.inOverview = event.OverviewOpenedOrClosed.is_open === true - } catch (e) { - Logger.error("NiriService", "Error parsing overview state:", e) - } - } - } catch (e) { - Logger.error("NiriService", "Error parsing event stream:", e, data) - } - } - } - } -} diff --git a/Services/WorkspacesService.qml b/Services/WorkspacesService.qml index 3ac0598..f928eea 100644 --- a/Services/WorkspacesService.qml +++ b/Services/WorkspacesService.qml @@ -4,150 +4,46 @@ pragma ComponentBehavior import QtQuick import Quickshell -import Quickshell.Io -import Quickshell.Hyprland import qs.Commons import qs.Services Singleton { id: root + // Delegate to CompositorService for all workspace operations property ListModel workspaces: ListModel {} property bool isHyprland: false property bool isNiri: false - property var hlWorkspaces: Hyprland.workspaces.values - // Detect which compositor we're using + Component.onCompleted: { - detectCompositor() - } - - function detectCompositor() { - try { - try { - if (Hyprland.eventSocketPath) { - isHyprland = true - isNiri = false - initHyprland() - return - } - } catch (e) { - - } - - if (typeof NiriService !== "undefined") { - isHyprland = false - isNiri = true - initNiri() - return - } - } catch (e) { - Logger.error("WorkspacesService", "Error detecting compositor:", e) - } - } - - // Initialize Hyprland integration - function initHyprland() { - try { - // Fixes the odd workspace issue. - Hyprland.refreshWorkspaces() - // hlWorkspaces = Hyprland.workspaces.values; - // updateHyprlandWorkspaces(); - return true - } catch (e) { - Logger.error("WorkspacesService", "Error initializing Hyprland:", e) - isHyprland = false - return false - } - } - - onHlWorkspacesChanged: { - updateHyprlandWorkspaces() + // Connect to CompositorService workspace changes + CompositorService.workspaceChanged.connect(updateWorkspaces) + // Initial sync + updateWorkspaces() } + // Listen to compositor detection changes Connections { - target: Hyprland.workspaces - function onValuesChanged() { - updateHyprlandWorkspaces() + target: CompositorService + function onIsHyprlandChanged() { + isHyprland = CompositorService.isHyprland + } + function onIsNiriChanged() { + isNiri = CompositorService.isNiri } } - Connections { - target: Hyprland - function onRawEvent(event) { - updateHyprlandWorkspaces() - } - } - - function updateHyprlandWorkspaces() { + function updateWorkspaces() { workspaces.clear() - try { - for (var i = 0; i < hlWorkspaces.length; i++) { - const ws = hlWorkspaces[i] - // Only append workspaces with id >= 1 - if (ws.id >= 1) { - workspaces.append({ - "id": i, - "idx": ws.id, - "name": ws.name || "", - "output": ws.monitor?.name || "", - "isActive": ws.active === true, - "isFocused": ws.focused === true, - "isUrgent": ws.urgent === true - }) - } - } - workspacesChanged() - } catch (e) { - Logger.error("WorkspacesService", "Error updating Hyprland workspaces:", e) + for (var i = 0; i < CompositorService.workspaces.count; i++) { + const ws = CompositorService.workspaces.get(i) + workspaces.append(ws) } - } - - function initNiri() { - updateNiriWorkspaces() - } - - Connections { - target: NiriService - function onWorkspacesChanged() { - updateNiriWorkspaces() - } - } - - function updateNiriWorkspaces() { - const niriWorkspaces = NiriService.workspaces || [] - workspaces.clear() - for (var i = 0; i < niriWorkspaces.length; i++) { - const ws = niriWorkspaces[i] - workspaces.append({ - "id": ws.id, - "idx": ws.idx || 1, - "name": ws.name || "", - "output": ws.output || "", - "isFocused": ws.isFocused === true, - "isActive": ws.isActive === true, - "isUrgent": ws.isUrgent === true, - "isOccupied": ws.isOccupied === true - }) - } - + // Explicitly trigger the signal to ensure the Workspace module gets notified workspacesChanged() } function switchToWorkspace(workspaceId) { - if (isHyprland) { - try { - Hyprland.dispatch(`workspace ${workspaceId}`) - } catch (e) { - Logger.error("WorkspacesService", "Error switching Hyprland workspace:", e) - } - } else if (isNiri) { - try { - Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", workspaceId.toString()]) - } catch (e) { - Logger.error("WorkspacesService", "Error switching Niri workspace:", e) - } - } else { - Logger.warn("WorkspacesService", "No supported compositor detected for workspace switching") - } + CompositorService.switchToWorkspace(workspaceId) } } diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 8e0ce6d..7fb636b 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -11,11 +11,18 @@ PanelWindow { property bool showOverlay: Settings.data.general.dimDesktop property int topMargin: Style.barHeight * scaling - property color overlayColor: showOverlay ? Color.applyOpacity(Color.mShadow, "AA") : "transparent" + // Show dimming if this panel is opened OR if we're in a transition (to prevent flickering) + property color overlayColor: (showOverlay && (PanelService.openedPanel === root || isTransitioning)) ? Color.applyOpacity(Color.mShadow, "AA") : "transparent" + property bool isTransitioning: false signal dismissed function hide() { - //visible = false + // Clear the panel service when hiding + if (PanelService.openedPanel === root) { + PanelService.openedPanel = null + } + isTransitioning = false + visible = false root.dismissed() } @@ -23,14 +30,21 @@ PanelWindow { // Ensure only one panel is visible at a time using PanelService as ephemeral storage try { if (PanelService.openedPanel && PanelService.openedPanel !== root && PanelService.openedPanel.hide) { + // Mark both panels as transitioning to prevent dimming flicker + isTransitioning = true + PanelService.openedPanel.isTransitioning = true PanelService.openedPanel.hide() + // Small delay to ensure smooth transition + showTimer.start() + return } + // No previous panel, show immediately PanelService.openedPanel = root + visible = true } catch (e) { // ignore } - visible = true } implicitWidth: screen.width @@ -57,6 +71,17 @@ PanelWindow { } } + Timer { + id: showTimer + interval: 50 // Small delay to ensure smooth transition + repeat: false + onTriggered: { + PanelService.openedPanel = root + isTransitioning = false + visible = true + } + } + Component.onDestruction: { try { if (visible && Settings.openPanel === root) @@ -68,8 +93,16 @@ PanelWindow { onVisibleChanged: { try { - if (!visible && Settings.openPanel === root) - Settings.openPanel = null + if (!visible) { + // Clear panel service when panel becomes invisible + if (PanelService.openedPanel === root) { + PanelService.openedPanel = null + } + if (Settings.openPanel === root) { + Settings.openPanel = null + } + isTransitioning = false + } } catch (e) { } From b05abca3c14b3fad4321b76f39cfbf101def9770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Atoch?= Date: Sun, 17 Aug 2025 05:08:46 -0400 Subject: [PATCH 366/394] House keeping: removed unused brightness bash --- Bin/brigthness.sh | 223 ---------------------------------------------- Commons/Color.qml | 4 +- 2 files changed, 2 insertions(+), 225 deletions(-) delete mode 100755 Bin/brigthness.sh diff --git a/Bin/brigthness.sh b/Bin/brigthness.sh deleted file mode 100755 index d133302..0000000 --- a/Bin/brigthness.sh +++ /dev/null @@ -1,223 +0,0 @@ -#!/bin/bash -# -# Bash script to get/set monitor brightness. -# This script acts as a smart wrapper for 'ddcutil' and the Linux sysfs backlight interface. -# -# It automatically determines whether to use ddcutil (for external monitors) -# or the /sys/class/backlight interface (for internal displays like eDP/LVDS). -# - -# --- Configuration --- -readonly CACHE_PATH="/tmp/ddcutil_detect_cache.txt" -readonly CACHE_TTL=3600 # Cache duration in seconds (1 hour) - -# --- Helper Functions --- - -# Prints an error message to stderr and exits with code 1. -# Usage: fail "Error message" -fail() { - echo "Error: $1" >&2 - exit 1 -} - -# Checks if a monitor name corresponds to an internal display. -# Usage: is_internal "monitor_name" -# Returns 0 (true) if internal, 1 (false) otherwise. -is_internal() { - local monitor_name="$1" - # eDP (embedded DisplayPort) and LVDS are common for laptop panels. - if [[ "$monitor_name" == "eDP"* || "$monitor_name" == "LVDS"* ]]; then - return 0 # Success (is internal) - else - return 1 # Failure (is not internal) - fi -} - -# Finds the corresponding backlight device in /sys/class/backlight for a given -# connector name (e.g., "DP-1"). -# It echoes the device name (e.g., "intel_backlight") on success. -# Usage: get_sysfs_backlight_device "connector_name" -get_sysfs_backlight_device() { - local target_connector="$1" - - for entry in /sys/class/backlight/*; do - # Ensure it's a valid directory entry - [ -d "$entry" ] || continue - - local device_name - device_name=$(basename "$entry") - - # Prioritize nvidia devices if found - if [[ "$device_name" == "nvidia_"* ]]; then - echo "$device_name" - return 0 - fi - - # Follow the symlink to find the graphics card connector - local real_path - if [ -L "$entry/device" ]; then - real_path=$(readlink -f "$entry/device") - # Extract the connector name from a path like .../card0-DP-1/... - # This regex finds "card" + digits + "-", and captures what follows. - if [[ "$real_path" =~ card[0-9]+-([^/]+) ]]; then - local path_connector="${BASH_REMATCH[1]}" - if [[ "$path_connector" == "$target_connector" ]]; then - echo "$device_name" - return 0 - fi - fi - fi - done - - return 1 # Not found -} - -# Retrieves the output of `ddcutil detect`, using a cache to avoid -# repeated slow calls. Echoes the command output. -# Usage: get_ddcutil_detect_output -get_ddcutil_detect_output() { - local use_cache=true - if [ ! -f "$CACHE_PATH" ]; then - use_cache=false - else - local now - now=$(date +%s) - local mtime - mtime=$(stat -c %Y "$CACHE_PATH") - local age=$((now - mtime)) - if (( age > CACHE_TTL )); then - use_cache=false - fi - fi - - if [[ "$use_cache" == true ]]; then - cat "$CACHE_PATH" - else - # Run the command, tee the output to the cache file, and also return it - ddcutil detect 2>/dev/null | tee "$CACHE_PATH" - fi -} - -# Parses the output of `ddcutil detect` to find the display index (e.g., 1) -# for a given monitor connector name (e.g., "DP-1"). -# Echoes the display index on success. -# Usage: get_ddc_index_for_monitor "monitor_name" -get_ddc_index_for_monitor() { - local target_monitor="$1" - local detect_output - detect_output=$(get_ddcutil_detect_output) - - # Check if ddcutil command failed or produced no output - if [ -z "$detect_output" ]; then - return 1 - fi - - local current_display="" - # Use process substitution and a while loop to read line-by-line - # This avoids issues with subshells and variable scope. - while IFS= read -r line; do - # Find lines like "Display 1" and store the number - if [[ "$line" =~ ^Display[[:space:]]+([0-9]+) ]]; then - current_display="${BASH_REMATCH[1]}" - continue - fi - - # Once we have a display number, look for its connector info - if [ -n "$current_display" ]; then - # Look for lines like "DRM connector: card0-DP-1" - if [[ "$line" =~ DRM_?connector:.*card[0-9]+-(${target_monitor}) ]]; then - echo "$current_display" - return 0 - fi - fi - done <<< "$detect_output" - - return 1 # Not found -} - -# --- Main Logic --- - -main() { - # Check for correct number of arguments - if [[ "$#" -lt 2 ]]; then - echo "-1" - exit 1 - fi - - local cmd="$1" - local mon="$2" - local val="${3:-}" # Default to empty if not provided - - if is_internal "$mon"; then - # --- Handle Internal Display (via /sys/class/backlight) --- - local backlight_device - backlight_device=$(get_sysfs_backlight_device "$mon") - if [ -z "$backlight_device" ]; then - echo "-1" # Error: Could not find backlight device - exit 1 - fi - - local brightness_file="/sys/class/backlight/$backlight_device/brightness" - local max_brightness_file="/sys/class/backlight/$backlight_device/max_brightness" - - if [ "$cmd" == "get" ]; then - local current_b - current_b=$(cat "$brightness_file") - local max_b - max_b=$(cat "$max_brightness_file") - # Perform integer arithmetic to scale to 0-100 - echo $((current_b * 100 / max_b)) - - elif [ "$cmd" == "set" ]; then - [[ "$#" -lt 3 ]] && echo "-1" && exit 1 - local max_b - max_b=$(cat "$max_brightness_file") - # Scale the 0-100 value to the device's raw value - local raw_brightness=$((val * max_b / 100)) - # NOTE: Writing here may require root privileges or specific udev rules. - if ! echo "$raw_brightness" > "$brightness_file" 2>/dev/null; then - echo "-1" # Error: Permission denied or other write error - exit 1 - fi - echo "$val" # Echo back the set value on success - else - echo "-1" # Invalid command - exit 1 - fi - - else - # --- Handle External Display (via ddcutil) --- - local display_index - display_index=$(get_ddc_index_for_monitor "$mon") - if [ -z "$display_index" ]; then - echo "-1" # Error: Could not find DDC display index - exit 1 - fi - - if [ "$cmd" == "get" ]; then - # Call ddcutil, parse for "current value = X," - local output - output=$(ddcutil --display "$display_index" getvcp 10 2>/dev/null) - if [[ "$output" =~ current[[:space:]]+value[[:space:]]*=[[:space:]]*([0-9]+) ]]; then - echo "${BASH_REMATCH[1]}" - else - echo "-1" # Error: Could not parse brightness from ddcutil - exit 1 - fi - elif [ "$cmd" == "set" ]; then - [[ "$#" -lt 3 ]] && echo "-1" && exit 1 - if ddcutil --display "$display_index" setvcp 10 "$val" >/dev/null 2>&1; then - echo "$val" # Echo back the set value on success - else - echo "-1" # Error: ddcutil command failed - exit 1 - fi - else - echo "-1" # Invalid command - exit 1 - fi - fi -} - -# Run the main function with all script arguments -main "$@" \ No newline at end of file diff --git a/Commons/Color.qml b/Commons/Color.qml index 5830d4a..c168290 100644 --- a/Commons/Color.qml +++ b/Commons/Color.qml @@ -97,11 +97,11 @@ Singleton { path: Settings.configDir + "colors.json" watchChanges: true onFileChanged: { - Logger.log("Colors", "Reloading colors from disk") + Logger.log("Color", "Reloading colors from disk") reload() } onAdapterUpdated: { - Logger.log("Colors", "Writing colors to disk") + Logger.log("Color", "Writing colors to disk") writeAdapter() } onLoadFailed: function (error) { From 1c5fdcb191bf571843afa2a363457400497cd1f0 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 17 Aug 2025 11:38:38 +0200 Subject: [PATCH 367/394] Edit README --- ...{Noctalia.json => Noctalia (default).json} | 0 ...{Rosepine (default).json => Rosepine.json} | 0 README.md | 253 +++++++++++++++++- 3 files changed, 252 insertions(+), 1 deletion(-) rename Assets/ColorSchemes/{Noctalia.json => Noctalia (default).json} (100%) rename Assets/ColorSchemes/{Rosepine (default).json => Rosepine.json} (100%) diff --git a/Assets/ColorSchemes/Noctalia.json b/Assets/ColorSchemes/Noctalia (default).json similarity index 100% rename from Assets/ColorSchemes/Noctalia.json rename to Assets/ColorSchemes/Noctalia (default).json diff --git a/Assets/ColorSchemes/Rosepine (default).json b/Assets/ColorSchemes/Rosepine.json similarity index 100% rename from Assets/ColorSchemes/Rosepine (default).json rename to Assets/ColorSchemes/Rosepine.json diff --git a/README.md b/README.md index 10b5086..4a53b0a 100644 --- a/README.md +++ b/README.md @@ -1 +1,252 @@ -Work in progress... \ No newline at end of file +

+ Noctalia Logo +

+ +# Noctalia + +**_quiet by design_** + +

+ + Last commit + + + GitHub stars + + + GitHub contributors + + + Discord + +

+ +A sleek, minimal, and thoughtfully crafted desktop shell for Wayland using **Quickshell**. Features a modern modular architecture with a status bar, notification system, control panel, comprehensive system integration, and more — all styled with a warm lavender palette and Material Design 3 principles. + +## Preview + +
+Click to expand preview images + +![Main](https://i.imgur.com/5mOIGD2.jpeg) +
+ +![Control Panel](https://i.imgur.com/fJmCV6m.jpeg) +
+ +![Applauncher](https://i.imgur.com/9OPV30q.jpeg) + +
+
+ +--- + +> ⚠️ **Note:** +> This shell currently supports **Niri** and **Hyprland** compositors. For other compositors, you will need to implement custom workspace logic in the CompositorService. + +--- + +## Features + +- **Status Bar:** Modular bar with workspace indicators, system monitors, clock, and quick access controls +- **Workspace Management:** Dynamic workspace switching with visual indicators and active window tracking +- **Notifications:** Rich notification system with history panel +- **Application Launcher:** Stylized launcher with favorites, recent apps, and special commands (calc, clipboard) +- **Side Panel:** Quick access panel with media controls, weather, power profiles, and system utilities +- **Settings Panel:** Comprehensive configuration interface for all shell components and preferences +- **Lock Screen:** Secure lock experience with PAM authentication, time display, and animated background +- **Audio Integration:** Volume controls, media playback, and audio visualizer (cava-based) +- **Connectivity:** WiFi and Bluetooth management with device pairing and network status +- **Power Management:** Battery monitoring, brightness control, and power profile switching +- **System Monitoring:** CPU, memory, and network usage monitoring with visual indicators +- **Tray System:** Application tray with menu support and system integration +- **Background Management:** Wallpaper management with effects and dynamic theming support + +--- + +## Quick Start + +### Installation + +```bash +# Install Quickshell +yay -S quickshell-git + +# Download and install Noctalia +mkdir -p ~/.config/quickshell && curl -sL https://github.com/Ly-sec/Noctalia/releases/latest/download/noctalia-latest.tar.gz | tar -xz --strip-components=1 -C ~/.config/quickshell/ +``` + +### Usage + +```bash +# Start the shell +qs + +# Toggle launcher +qs ipc call appLauncher toggle + +# Toggle lock screen +qs ipc call lockScreen toggle +``` + +### Keybinds + +| Action | Command | +|--------|---------| +| Toggle Application Launcher | `qs ipc call appLauncher toggle` | +| Toggle Lock Screen | `qs ipc call lockScreen toggle` | +| Toggle Notification History | `qs ipc call notifications toggleHistory` | +| Toggle Settings Panel | `qs ipc call settings toggle` | +| Increase Brightness | `qs ipc call brightness increase` | +| Decrease Brightness | `qs ipc call brightness decrease` | + +### Configuration + +Access settings through the side panel (top right button) to configure weather, wallpapers, screen recording, audio, network, and theme options. + +### Application Launcher + +The launcher supports special commands for enhanced functionality: +- `>calc` - Simple mathematical calculations +- `>clip` - Clipboard history management + +--- + +
+Theme Colors + +| Color Role | Color | Description | +| -------------------- | ----------- | -------------------------- | +| Primary | `#c7a1d8` | Soft lavender purple | +| On Primary | `#1a151f` | Dark text on primary | +| Secondary | `#a984c4` | Muted lavender | +| On Secondary | `#f3edf7` | Light text on secondary | +| Tertiary | `#e0b7c9` | Warm pink-lavender | +| On Tertiary | `#20161f` | Dark text on tertiary | +| Surface | `#1c1822` | Dark purple-tinted surface | +| On Surface | `#e9e4f0` | Light text on surface | +| Surface Variant | `#262130` | Elevated surface variant | +| On Surface Variant | `#a79ab0` | Muted text on surface variant | +| Error | `#e9899d` | Soft rose red | +| On Error | `#1e1418` | Dark text on error | +| Outline | `#4d445a` | Purple-tinted outline | +| Outline Variant | `#342c42` | Variant outline color | +| Shadow | `#120f18` | Deep purple-tinted shadow | + +
+ +--- + +## Advanced Configuration + +### Niri Configuration + +Add this to your `layout` section for proper swww integration: + +``` +background-color "transparent" +``` + +### Recommended Compositor Settings + +For Niri: + +``` +window-rule { + geometry-corner-radius 20 + clip-to-geometry true +} + +layer-rule { + match namespace="^swww-daemon$" + place-within-backdrop true +} + +layer-rule { + match namespace="^quickshell-wallpaper$" +} + +layer-rule { + match namespace="^quickshell-overview$" + place-within-backdrop true +} +``` + +--- + +## Dependencies + +### Required + +- `quickshell-git` - Core shell framework +- `material-symbols-git` - Icon font for UI elements +- `xdg-desktop-portal-gnome` - Desktop integration (or alternative portal) + +### Optional + +- `swww` - Wallpaper animations and effects +- `matugen` - Material You color scheme generation +- `cava` - Audio visualizer component +- `gpu-screen-recorder` - Screen recording functionality + +--- + +## Development + +### Project Structure + +``` +Noctalia/ +├── shell.qml # Main shell entry point +├── Modules/ # UI components +│ ├── Bar/ # Status bar components +│ ├── Dock/ # Application launcher +│ ├── SidePanel/ # Quick access panel +│ ├── SettingsPanel/ # Configuration interface +│ └── ... +├── Services/ # Backend services +│ ├── CompositorService.qml +│ ├── WorkspacesService.qml +│ ├── AudioService.qml +│ └── ... +├── Widgets/ # Reusable UI components +├── Commons/ # Shared utilities +├── Assets/ # Static assets +└── Bin/ # Utility scripts +``` + +### Contributing + +1. Follow the existing code style and patterns +2. Use the modular architecture for new features +3. Implement proper error handling and logging +4. Test with both Hyprland and Niri compositors (if applicable) + +Contributions are welcome! Don't worry about being perfect - every contribution helps! Whether it's fixing a small bug, adding a new feature, or improving documentation, we welcome all contributions. Feel free to open an issue to discuss ideas or ask questions before diving in. For feature requests and ideas, you can also use our discussions page. + +--- + +## 💜 Credits + +Huge thanks to [**@ferrreo**](https://github.com/ferrreo) and [**@quadbyte**](https://github.com/quadbyte) for their contributions and the cool features they added! + +--- + +#### Donation + +While I actually didn't want to accept donations, more and more people are asking to donate so... I don't know, if you really feel like donating then I obviously highly appreciate it but **PLEASE** never feel forced to donate or anything. It won't change how we work on Noctalia, it's a project that we work on for fun in the end. + +[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R01IX85B) + +--- + +#### Special Thanks + +Thank you to everyone who supports me and this project 💜! +* Gohma + +--- + +## License + +This project is licensed under the terms of the [MIT License](./LICENSE). From aabe05a7ea61e6c48cf2901d812b56656d58ee5a Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 17 Aug 2025 11:44:13 +0200 Subject: [PATCH 368/394] Add Battery indicator to LockScreen --- Modules/LockScreen/LockScreen.qml | 62 +++++++++++++++++++++++++++++++ README.md | 33 ++++++++-------- 2 files changed, 79 insertions(+), 16 deletions(-) diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index d932fb9..2d75c25 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -5,6 +5,7 @@ import QtQuick.Effects import Quickshell import Quickshell.Wayland import Quickshell.Services.Pam +import Quickshell.Services.UPower import Quickshell.Io import Quickshell.Widgets import qs.Commons @@ -91,6 +92,46 @@ WlSessionLock { } WlSessionLockSurface { + // Battery indicator component + Item { + id: batteryIndicator + + // Import UPower for battery data + property var battery: UPower.displayDevice + property bool isReady: battery && battery.ready && battery.isLaptopBattery && battery.isPresent + property real percent: isReady ? (battery.percentage * 100) : 0 + property bool charging: isReady ? battery.state === UPowerDeviceState.Charging : false + property bool batteryVisible: isReady && percent > 0 + + // Choose icon based on charge and charging state + function getIcon() { + if (!batteryVisible) + return "" + + if (charging) + return "battery_android_bolt" + + if (percent >= 95) + return "battery_android_full" + + // Hardcoded battery symbols + if (percent >= 85) + return "battery_android_6" + if (percent >= 70) + return "battery_android_5" + if (percent >= 55) + return "battery_android_4" + if (percent >= 40) + return "battery_android_3" + if (percent >= 25) + return "battery_android_2" + if (percent >= 10) + return "battery_android_1" + if (percent >= 0) + return "battery_android_0" + } + } + // Wallpaper image Image { id: lockBgImage @@ -354,6 +395,27 @@ WlSessionLock { font.weight: Font.Bold Layout.fillWidth: true } + + // Battery indicator + Row { + spacing: Style.marginSmall * scaling + visible: batteryIndicator.batteryVisible + + Text { + text: batteryIndicator.getIcon() + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeMedium + color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface + } + + Text { + text: Math.round(batteryIndicator.percent) + "%" + color: Color.mOnSurface + font.family: "DejaVu Sans Mono" + font.pointSize: Style.fontSizeMedium + font.weight: Font.Bold + } + } } } diff --git a/README.md b/README.md index 4a53b0a..06fbd0e 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,23 @@ A sleek, minimal, and thoughtfully crafted desktop shell for Wayland using **Qui --- +## Dependencies + +### Required + +- `quickshell-git` - Core shell framework +- `material-symbols-git` - Icon font for UI elements +- `xdg-desktop-portal-gnome` - Desktop integration (or alternative portal) + +### Optional + +- `swww` - Wallpaper animations and effects +- `matugen` - Material You color scheme generation +- `cava` - Audio visualizer component +- `gpu-screen-recorder` - Screen recording functionality + +--- + ## Quick Start ### Installation @@ -174,22 +191,6 @@ layer-rule { --- -## Dependencies - -### Required - -- `quickshell-git` - Core shell framework -- `material-symbols-git` - Icon font for UI elements -- `xdg-desktop-portal-gnome` - Desktop integration (or alternative portal) - -### Optional - -- `swww` - Wallpaper animations and effects -- `matugen` - Material You color scheme generation -- `cava` - Audio visualizer component -- `gpu-screen-recorder` - Screen recording functionality - ---- ## Development From f5a192baba5a3403e5b475830a40e6f071dbae00 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 17 Aug 2025 05:44:59 -0400 Subject: [PATCH 369/394] Removed extra 's' from all services. Made the noctalia color scheme the default --- .../Catppuccin.json | 0 .../Dracula.json | 0 .../Gruvbox.json | 0 .../Noctalia (default).json} | 0 .../{ColorSchemes => ColorScheme}/Nord.json | 0 .../Rosepine.json} | 0 .../Solarized.json | 0 .../Tokyo Night.json | 0 Commons/Color.qml | 30 ++++++++-------- Commons/Settings.qml | 8 ++--- Modules/Background/Background.qml | 4 +-- Modules/Background/Overview.qml | 4 +-- Modules/Background/WallpaperPicker.qml | 10 ------ Modules/Bar/SystemMonitor.qml | 6 ++-- Modules/Bar/Workspace.qml | 12 +++---- Modules/LockScreen/LockScreen.qml | 2 +- Modules/SettingsPanel/Tabs/ColorSchemeTab.qml | 10 +++--- .../Tabs/WallpaperSelectorTab.qml | 14 ++++---- Modules/SidePanel/Cards/SystemMonitorCard.qml | 8 ++--- ...emesService.qml => ColorSchemeService.qml} | 14 ++++---- Services/CompositorService.qml | 36 +++++++++---------- ...StatsService.qml => SystemStatService.qml} | 0 ...papersService.qml => WallpaperService.qml} | 10 +++--- ...spacesService.qml => WorkspaceService.qml} | 0 24 files changed, 79 insertions(+), 89 deletions(-) rename Assets/{ColorSchemes => ColorScheme}/Catppuccin.json (100%) rename Assets/{ColorSchemes => ColorScheme}/Dracula.json (100%) rename Assets/{ColorSchemes => ColorScheme}/Gruvbox.json (100%) rename Assets/{ColorSchemes/Noctalia.json => ColorScheme/Noctalia (default).json} (100%) rename Assets/{ColorSchemes => ColorScheme}/Nord.json (100%) rename Assets/{ColorSchemes/Rosepine (default).json => ColorScheme/Rosepine.json} (100%) rename Assets/{ColorSchemes => ColorScheme}/Solarized.json (100%) rename Assets/{ColorSchemes => ColorScheme}/Tokyo Night.json (100%) delete mode 100644 Modules/Background/WallpaperPicker.qml rename Services/{ColorSchemesService.qml => ColorSchemeService.qml} (78%) rename Services/{SystemStatsService.qml => SystemStatService.qml} (100%) rename Services/{WallpapersService.qml => WallpaperService.qml} (95%) rename Services/{WorkspacesService.qml => WorkspaceService.qml} (100%) diff --git a/Assets/ColorSchemes/Catppuccin.json b/Assets/ColorScheme/Catppuccin.json similarity index 100% rename from Assets/ColorSchemes/Catppuccin.json rename to Assets/ColorScheme/Catppuccin.json diff --git a/Assets/ColorSchemes/Dracula.json b/Assets/ColorScheme/Dracula.json similarity index 100% rename from Assets/ColorSchemes/Dracula.json rename to Assets/ColorScheme/Dracula.json diff --git a/Assets/ColorSchemes/Gruvbox.json b/Assets/ColorScheme/Gruvbox.json similarity index 100% rename from Assets/ColorSchemes/Gruvbox.json rename to Assets/ColorScheme/Gruvbox.json diff --git a/Assets/ColorSchemes/Noctalia.json b/Assets/ColorScheme/Noctalia (default).json similarity index 100% rename from Assets/ColorSchemes/Noctalia.json rename to Assets/ColorScheme/Noctalia (default).json diff --git a/Assets/ColorSchemes/Nord.json b/Assets/ColorScheme/Nord.json similarity index 100% rename from Assets/ColorSchemes/Nord.json rename to Assets/ColorScheme/Nord.json diff --git a/Assets/ColorSchemes/Rosepine (default).json b/Assets/ColorScheme/Rosepine.json similarity index 100% rename from Assets/ColorSchemes/Rosepine (default).json rename to Assets/ColorScheme/Rosepine.json diff --git a/Assets/ColorSchemes/Solarized.json b/Assets/ColorScheme/Solarized.json similarity index 100% rename from Assets/ColorSchemes/Solarized.json rename to Assets/ColorScheme/Solarized.json diff --git a/Assets/ColorSchemes/Tokyo Night.json b/Assets/ColorScheme/Tokyo Night.json similarity index 100% rename from Assets/ColorSchemes/Tokyo Night.json rename to Assets/ColorScheme/Tokyo Night.json diff --git a/Commons/Color.qml b/Commons/Color.qml index c168290..333abc5 100644 --- a/Commons/Color.qml +++ b/Commons/Color.qml @@ -47,23 +47,23 @@ Singleton { QtObject { id: defaultColors - property color mPrimary: "#ebbcba" - property color mOnPrimary: "#1f1d2e" - property color mSecondary: "#31748f" - property color mOnSecondary: "#e0def4" - property color mTertiary: "#9ccfd8" - property color mOnTertiary: "#191724" + property color mPrimary: "#c7a1d8" + property color mOnPrimary: "#1a151f" + property color mSecondary: "#a984c4" + property color mOnSecondary: "#f3edf7" + property color mTertiary: "#e0b7c9" + property color mOnTertiary: "#20161f" - property color mError: "#eb6f92" - property color mOnError: "#1f1d2e" + property color mError: "#e9899d" + property color mOnError: "#1e1418" - property color mSurface: "#1f1d2e" - property color mOnSurface: "#e0def4" - property color mSurfaceVariant: "#26233a" - property color mOnSurfaceVariant: "#908caa" - property color mOutline: "#44415a" - property color mOutlineVariant: "#514e6c" - property color mShadow: "#191724" + property color mSurface: "#1c1822" + property color mOnSurface: "#e9e4f0" + property color mSurfaceVariant: "#262130" + property color mOnSurfaceVariant: "#a79ab0" + property color mOutline: "#4d445a" + property color mOutlineVariant: "#342c42" + property color mShadow: "#120f18" } // ---------------------------------------------------------------- diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 51a5a55..0b1f496 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -54,7 +54,7 @@ Singleton { // Only set wallpaper on initial load, not on reloads if (isInitialLoad && adapter.wallpaper.current !== "") { Logger.log("Settings", "Set current wallpaper", adapter.wallpaper.current) - WallpapersService.setCurrentWallpaper(adapter.wallpaper.current, true) + WallpaperService.setCurrentWallpaper(adapter.wallpaper.current, true) } isInitialLoad = false }) @@ -125,9 +125,9 @@ Singleton { property int randomInterval: 300 property JsonObject swww - onDirectoryChanged: WallpapersService.loadWallpapers() - onIsRandomChanged: WallpapersService.toggleRandomWallpaper() - onRandomIntervalChanged: WallpapersService.restartRandomWallpaperTimer() + onDirectoryChanged: WallpaperService.listWallpapers() + onIsRandomChanged: WallpaperService.toggleRandomWallpaper() + onRandomIntervalChanged: WallpaperService.restartRandomWallpaperTimer() swww: JsonObject { property bool enabled: false diff --git a/Modules/Background/Background.qml b/Modules/Background/Background.qml index 437cd3e..c94511b 100644 --- a/Modules/Background/Background.qml +++ b/Modules/Background/Background.qml @@ -9,8 +9,8 @@ Variants { delegate: PanelWindow { required property ShellScreen modelData - property string wallpaperSource: WallpapersService.currentWallpaper !== "" - && !Settings.data.wallpaper.swww.enabled ? WallpapersService.currentWallpaper : "" + property string wallpaperSource: WallpaperService.currentWallpaper !== "" + && !Settings.data.wallpaper.swww.enabled ? WallpaperService.currentWallpaper : "" visible: wallpaperSource !== "" && !Settings.data.wallpaper.swww.enabled diff --git a/Modules/Background/Overview.qml b/Modules/Background/Overview.qml index ecd6638..01abc29 100644 --- a/Modules/Background/Overview.qml +++ b/Modules/Background/Overview.qml @@ -22,8 +22,8 @@ NLoader { delegate: PanelWindow { required property ShellScreen modelData - property string wallpaperSource: WallpapersService.currentWallpaper !== "" - && !Settings.data.wallpaper.swww.enabled ? WallpapersService.currentWallpaper : "" + property string wallpaperSource: WallpaperService.currentWallpaper !== "" + && !Settings.data.wallpaper.swww.enabled ? WallpaperService.currentWallpaper : "" visible: wallpaperSource !== "" && !Settings.data.wallpaper.swww.enabled color: "transparent" diff --git a/Modules/Background/WallpaperPicker.qml b/Modules/Background/WallpaperPicker.qml deleted file mode 100644 index 2387e9e..0000000 --- a/Modules/Background/WallpaperPicker.qml +++ /dev/null @@ -1,10 +0,0 @@ -pragma Singleton - -import QtQuick -import Quickshell -import qs.Commons -import qs.Services - -Item { - id: root -} diff --git a/Modules/Bar/SystemMonitor.qml b/Modules/Bar/SystemMonitor.qml index 0946aa4..b95d89d 100644 --- a/Modules/Bar/SystemMonitor.qml +++ b/Modules/Bar/SystemMonitor.qml @@ -28,7 +28,7 @@ Row { NText { id: cpuUsageText - text: `${SystemStatsService.cpuUsage}%` + text: `${SystemStatService.cpuUsage}%` font.pointSize: Style.fontSizeReduced * scaling font.weight: Style.fontWeightBold anchors.verticalCenter: parent.verticalCenter @@ -52,7 +52,7 @@ Row { } NText { - text: `${SystemStatsService.cpuTemp}°C` + text: `${SystemStatService.cpuTemp}°C` font.pointSize: Style.fontSizeReduced * scaling font.weight: Style.fontWeightBold anchors.verticalCenter: parent.verticalCenter @@ -75,7 +75,7 @@ Row { } NText { - text: `${SystemStatsService.memoryUsageGb}G` + text: `${SystemStatService.memoryUsageGb}G` font.pointSize: Style.fontSizeReduced * scaling font.weight: Style.fontWeightBold anchors.verticalCenter: parent.verticalCenter diff --git a/Modules/Bar/Workspace.qml b/Modules/Bar/Workspace.qml index 60499ce..c967f36 100644 --- a/Modules/Bar/Workspace.qml +++ b/Modules/Bar/Workspace.qml @@ -43,8 +43,8 @@ Item { Component.onCompleted: { localWorkspaces.clear() - for (var i = 0; i < WorkspacesService.workspaces.count; i++) { - const ws = WorkspacesService.workspaces.get(i) + for (var i = 0; i < WorkspaceService.workspaces.count; i++) { + const ws = WorkspaceService.workspaces.get(i) if (ws.output.toLowerCase() === screen.name.toLowerCase()) { localWorkspaces.append(ws) } @@ -54,11 +54,11 @@ Item { } Connections { - target: WorkspacesService + target: WorkspaceService function onWorkspacesChanged() { localWorkspaces.clear() - for (var i = 0; i < WorkspacesService.workspaces.count; i++) { - const ws = WorkspacesService.workspaces.get(i) + for (var i = 0; i < WorkspaceService.workspaces.count; i++) { + const ws = WorkspaceService.workspaces.get(i) if (ws.output.toLowerCase() === screen.name.toLowerCase()) { localWorkspaces.append(ws) } @@ -182,7 +182,7 @@ Item { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { - WorkspacesService.switchToWorkspace(model.idx) + WorkspaceService.switchToWorkspace(model.idx) } hoverEnabled: true } diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index d932fb9..960af59 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -96,7 +96,7 @@ WlSessionLock { id: lockBgImage anchors.fill: parent fillMode: Image.PreserveAspectCrop - source: WallpapersService.currentWallpaper !== "" ? WallpapersService.currentWallpaper : "" + source: WallpaperService.currentWallpaper !== "" ? WallpaperService.currentWallpaper : "" cache: true smooth: true mipmap: false diff --git a/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml b/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml index e470e08..e159e32 100644 --- a/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml +++ b/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml @@ -67,7 +67,7 @@ ColumnLayout { // When the list of available schemes changes, clear the cache. // The Repeater below will automatically re-create the FileViews. Connections { - target: ColorSchemesService + target: ColorSchemeService function onSchemesChanged() { schemeColorsCache = {} } @@ -79,7 +79,7 @@ ColumnLayout { id: fileLoaders Repeater { - model: ColorSchemesService.schemes + model: ColorSchemeService.schemes // The delegate is a Component, which correctly wraps the non-visual FileView delegate: Item { @@ -133,7 +133,7 @@ ColumnLayout { onToggled: checked => { Settings.data.colorSchemes.useWallpaperColors = checked if (Settings.data.colorSchemes.useWallpaperColors) { - ColorSchemesService.changedWallpaper() + ColorSchemeService.changedWallpaper() } } } @@ -178,7 +178,7 @@ ColumnLayout { Layout.fillWidth: true Repeater { - model: ColorSchemesService.schemes + model: ColorSchemeService.schemes Rectangle { id: schemeCard @@ -201,7 +201,7 @@ ColumnLayout { // TBC: broken uncheck useWallpaperColors Settings.data.colorSchemes.useWallpaperColors = false Settings.data.colorSchemes.predefinedScheme = schemePath - ColorSchemesService.applyScheme(schemePath) + ColorSchemeService.applyScheme(schemePath) } hoverEnabled: true cursorShape: Qt.PointingHandCursor diff --git a/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml b/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml index b2a49ac..07d4dd9 100644 --- a/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml +++ b/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml @@ -49,7 +49,7 @@ Item { id: currentWallpaperImage anchors.fill: parent anchors.margins: Style.marginSmall * scaling - imagePath: WallpapersService.currentWallpaper + imagePath: WallpaperService.currentWallpaper fallbackIcon: "image" borderColor: Color.mOutline borderWidth: Math.max(1, Style.borderThin * scaling) @@ -97,7 +97,7 @@ Item { icon: "refresh" tooltipText: "Refresh wallpaper list" onClicked: { - WallpapersService.loadWallpapers() + WallpaperService.listWallpapers() } Layout.alignment: Qt.AlignTop | Qt.AlignRight } @@ -108,14 +108,14 @@ Item { Layout.fillWidth: true Layout.preferredHeight: { return Math.ceil( - WallpapersService.wallpaperList.length / wallpaperGridView.columns) * wallpaperGridView.cellHeight + WallpaperService.wallpaperList.length / wallpaperGridView.columns) * wallpaperGridView.cellHeight } GridView { id: wallpaperGridView anchors.fill: parent clip: true - model: WallpapersService.wallpaperList + model: WallpaperService.wallpaperList boundsBehavior: Flickable.StopAtBounds flickableDirection: Flickable.AutoFlickDirection @@ -136,7 +136,7 @@ Item { delegate: Rectangle { id: wallpaperItem property string wallpaperPath: modelData - property bool isSelected: wallpaperPath === WallpapersService.currentWallpaper + property bool isSelected: wallpaperPath === WallpaperService.currentWallpaper width: wallpaperGridView.itemSize height: Math.floor(wallpaperGridView.itemSize * 0.67) @@ -197,7 +197,7 @@ Item { acceptedButtons: Qt.LeftButton hoverEnabled: true onClicked: { - WallpapersService.changeWallpaper(wallpaperPath) + WallpaperService.changeWallpaper(wallpaperPath) } } } @@ -210,7 +210,7 @@ Item { radius: Style.radiusMedium * scaling border.color: Color.mOutline border.width: Math.max(1, Style.borderThin * scaling) - visible: WallpapersService.wallpaperList.length === 0 && !WallpapersService.scanning + visible: WallpaperService.wallpaperList.length === 0 && !WallpaperService.scanning ColumnLayout { anchors.centerIn: parent diff --git a/Modules/SidePanel/Cards/SystemMonitorCard.qml b/Modules/SidePanel/Cards/SystemMonitorCard.qml index c3fb93e..325a29c 100644 --- a/Modules/SidePanel/Cards/SystemMonitorCard.qml +++ b/Modules/SidePanel/Cards/SystemMonitorCard.qml @@ -28,7 +28,7 @@ NBox { } NCircleStat { - value: SystemStatsService.cpuUsage + value: SystemStatService.cpuUsage icon: "speed" flat: true contentScale: 0.8 @@ -36,7 +36,7 @@ NBox { height: 68 * scaling } NCircleStat { - value: SystemStatsService.cpuTemp + value: SystemStatService.cpuTemp suffix: "°C" icon: "device_thermostat" flat: true @@ -45,7 +45,7 @@ NBox { height: 68 * scaling } NCircleStat { - value: SystemStatsService.memoryUsagePer + value: SystemStatService.memoryUsagePer icon: "memory" flat: true contentScale: 0.8 @@ -53,7 +53,7 @@ NBox { height: 68 * scaling } NCircleStat { - value: SystemStatsService.diskUsage + value: SystemStatService.diskUsage icon: "hard_drive" flat: true contentScale: 0.8 diff --git a/Services/ColorSchemesService.qml b/Services/ColorSchemeService.qml similarity index 78% rename from Services/ColorSchemesService.qml rename to Services/ColorSchemeService.qml index 29af4c4..148a05c 100644 --- a/Services/ColorSchemesService.qml +++ b/Services/ColorSchemeService.qml @@ -10,17 +10,17 @@ Singleton { id: root Component.onCompleted: { - Logger.log("ColorSchemes", "Service started") + Logger.log("ColorScheme", "Service started") loadColorSchemes() } property var schemes: [] property bool scanning: false - property string schemesDirectory: Quickshell.shellDir + "/Assets/ColorSchemes" + property string schemesDirectory: Quickshell.shellDir + "/Assets/ColorScheme" property string colorsJsonFilePath: Settings.configDir + "colors.json" function loadColorSchemes() { - Logger.log("ColorSchemes", "Load ColorSchemes") + Logger.log("ColorScheme", "Load ColorScheme") scanning = true schemes = [] // Unsetting, then setting the folder will re-trigger the parsing! @@ -34,7 +34,7 @@ Singleton { function changedWallpaper() { if (Settings.data.colorSchemes.useWallpaperColors) { - Logger.log("ColorSchemes", "Starting color generation from wallpaper") + Logger.log("ColorScheme", "Starting color generation from wallpaper") generateColorsProcess.running = true // Invalidate potential predefined scheme Settings.data.colorSchemes.predefinedScheme = "" @@ -55,19 +55,19 @@ Singleton { } schemes = files scanning = false - Logger.log("ColorSchemes", "Loaded", schemes.length, "schemes") + Logger.log("ColorScheme", "Listed", schemes.length, "schemes") } } } Process { id: generateColorsProcess - command: ["matugen", "image", WallpapersService.currentWallpaper, "--config", Quickshell.shellDir + "/Assets/Matugen/matugen.toml"] + command: ["matugen", "image", WallpaperService.currentWallpaper, "--config", Quickshell.shellDir + "/Assets/Matugen/matugen.toml"] workingDirectory: Quickshell.shellDir running: false stdout: StdioCollector { onStreamFinished: { - Logger.log("ColorSchemes", "Completed colors generation") + Logger.log("ColorScheme", "Completed colors generation") } } stderr: StdioCollector { diff --git a/Services/CompositorService.qml b/Services/CompositorService.qml index d58d53b..c74457b 100644 --- a/Services/CompositorService.qml +++ b/Services/CompositorService.qml @@ -81,7 +81,7 @@ Singleton { compositorType = "unknown" isHyprland = false isNiri = false - Logger.warn("CompositorService", "No supported compositor detected") + Logger.warn("Compositor", "No supported compositor detected") } // Hyprland integration @@ -90,9 +90,9 @@ Singleton { Hyprland.refreshWorkspaces() updateHyprlandWorkspaces() setupHyprlandConnections() - Logger.log("CompositorService", "Hyprland initialized successfully") + Logger.log("Compositor", "Hyprland initialized successfully") } catch (e) { - Logger.error("CompositorService", "Error initializing Hyprland:", e) + Logger.error("Compositor", "Error initializing Hyprland:", e) compositorType = "unknown" isHyprland = false } @@ -124,7 +124,7 @@ Singleton { } } } catch (e) { - Logger.error("CompositorService", "Error updating Hyprland workspaces:", e) + Logger.error("Compositor", "Error updating Hyprland workspaces:", e) } } @@ -136,9 +136,9 @@ Singleton { // Initial load of workspaces and windows updateNiriWorkspaces() updateNiriWindows() - Logger.log("CompositorService", "Niri initialized successfully") + Logger.log("Compositor", "Niri initialized successfully") } catch (e) { - Logger.error("CompositorService", "Error initializing Niri:", e) + Logger.error("Compositor", "Error initializing Niri:", e) compositorType = "unknown" isNiri = false } @@ -197,7 +197,7 @@ Singleton { } workspaceChanged() } catch (e) { - Logger.error("CompositorService", "Failed to parse workspaces:", e, line) + Logger.error("Compositor", "Failed to parse workspaces:", e, line) } } } @@ -244,7 +244,7 @@ Singleton { updateFocusedWindowTitle() activeWindowChanged() } catch (e) { - Logger.error("CompositorService", "Error parsing windows event:", e) + Logger.error("Compositor", "Error parsing windows event:", e) } } else if (event.WorkspaceActivated) { niriWorkspaceProcess.running = true @@ -262,18 +262,18 @@ Singleton { updateFocusedWindowTitle() activeWindowChanged() } catch (e) { - Logger.error("CompositorService", "Error parsing window focus event:", e) + Logger.error("Compositor", "Error parsing window focus event:", e) } } else if (event.OverviewOpenedOrClosed) { try { inOverview = event.OverviewOpenedOrClosed.is_open === true overviewStateChanged() } catch (e) { - Logger.error("CompositorService", "Error parsing overview state:", e) + Logger.error("Compositor", "Error parsing overview state:", e) } } } catch (e) { - Logger.error("CompositorService", "Error parsing event stream:", e, data) + Logger.error("Compositor", "Error parsing event stream:", e, data) } } } @@ -314,7 +314,7 @@ Singleton { updateFocusedWindowTitle() activeWindowChanged() } catch (e) { - Logger.error("CompositorService", "Failed to parse windows:", e, line) + Logger.error("Compositor", "Failed to parse windows:", e, line) } } } @@ -334,16 +334,16 @@ Singleton { try { Hyprland.dispatch(`workspace ${workspaceId}`) } catch (e) { - Logger.error("CompositorService", "Error switching Hyprland workspace:", e) + Logger.error("Compositor", "Error switching Hyprland workspace:", e) } } else if (isNiri) { try { Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", workspaceId.toString()]) } catch (e) { - Logger.error("CompositorService", "Error switching Niri workspace:", e) + Logger.error("Compositor", "Error switching Niri workspace:", e) } } else { - Logger.warn("CompositorService", "No supported compositor detected for workspace switching") + Logger.warn("Compositor", "No supported compositor detected for workspace switching") } } @@ -353,16 +353,16 @@ Singleton { try { Quickshell.execDetached(["hyprctl", "dispatch", "exit"]) } catch (e) { - Logger.error("CompositorService", "Error logging out from Hyprland:", e) + Logger.error("Compositor", "Error logging out from Hyprland:", e) } } else if (isNiri) { try { Quickshell.execDetached(["niri", "msg", "action", "quit", "--skip-confirmation"]) } catch (e) { - Logger.error("CompositorService", "Error logging out from Niri:", e) + Logger.error("Compositor", "Error logging out from Niri:", e) } } else { - Logger.warn("CompositorService", "No supported compositor detected for logout") + Logger.warn("Compositor", "No supported compositor detected for logout") } } diff --git a/Services/SystemStatsService.qml b/Services/SystemStatService.qml similarity index 100% rename from Services/SystemStatsService.qml rename to Services/SystemStatService.qml diff --git a/Services/WallpapersService.qml b/Services/WallpaperService.qml similarity index 95% rename from Services/WallpapersService.qml rename to Services/WallpaperService.qml index 49503c2..501abdd 100644 --- a/Services/WallpapersService.qml +++ b/Services/WallpaperService.qml @@ -11,7 +11,7 @@ Singleton { Component.onCompleted: { Logger.log("Wallpapers", "Service started") - loadWallpapers() + listWallpapers() // Wallpaper is set when the settings are loaded. // Don't start random wallpaper during initialization @@ -25,8 +25,8 @@ Singleton { property string transitionType: Settings.data.wallpaper.swww.transitionType property var randomChoices: ["simple", "fade", "left", "right", "top", "bottom", "wipe", "wave", "grow", "center", "any", "outer"] - function loadWallpapers() { - Logger.log("Wallpapers", "Load Wallpapers") + function listWallpapers() { + Logger.log("Wallpapers", "Listing wallpapers") scanning = true wallpaperList = [] // Unsetting, then setting the folder will re-trigger the parsing! @@ -65,9 +65,9 @@ Singleton { randomWallpaperTimer.restart() } - // Only notify ColorSchemes service if the wallpaper actually changed + // Only notify ColorScheme service if the wallpaper actually changed if (wallpaperChanged) { - ColorSchemesService.changedWallpaper() + ColorSchemeService.changedWallpaper() } } diff --git a/Services/WorkspacesService.qml b/Services/WorkspaceService.qml similarity index 100% rename from Services/WorkspacesService.qml rename to Services/WorkspaceService.qml From dbdfbdc746d370a4a9f4b5f323ff9ed5cc9227a1 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 17 Aug 2025 05:54:55 -0400 Subject: [PATCH 370/394] "transparent" => Color.transparent --- Modules/AppLauncher/AppLauncher.qml | 2 +- Modules/Background/Background.qml | 2 +- Modules/Background/Overview.qml | 2 +- Modules/Background/ScreenCorners.qml | 2 +- Modules/Bar/Bar.qml | 2 +- Modules/Bar/BluetoothMenu.qml | 2 +- Modules/Bar/Tray.qml | 2 +- Modules/Bar/TrayMenu.qml | 12 ++++++------ Modules/Bar/WiFiMenu.qml | 4 ++-- Modules/Bar/Workspace.qml | 2 +- Modules/Calendar/Calendar.qml | 2 +- Modules/Dock/Dock.qml | 6 +++--- Modules/LockScreen/LockScreen.qml | 16 ++++++++-------- Modules/Notification/Notification.qml | 2 +- Modules/SettingsPanel/SettingsPanel.qml | 2 +- Modules/SettingsPanel/Tabs/AboutTab.qml | 4 ++-- Modules/SidePanel/Cards/MediaCard.qml | 8 ++++---- Modules/SidePanel/PowerMenu.qml | 10 +++++----- Widgets/NCircleStat.qml | 4 ++-- Widgets/NClock.qml | 2 +- Widgets/NComboBox.qml | 2 +- Widgets/NIconButton.qml | 4 ++-- Widgets/NImageRounded.qml | 6 +++--- Widgets/NPanel.qml | 4 ++-- Widgets/NPill.qml | 2 +- Widgets/NRadioButton.qml | 2 +- Widgets/NTextInput.qml | 4 ++-- Widgets/NTooltip.qml | 2 +- 28 files changed, 57 insertions(+), 57 deletions(-) diff --git a/Modules/AppLauncher/AppLauncher.qml b/Modules/AppLauncher/AppLauncher.qml index 31c9b10..4d25076 100644 --- a/Modules/AppLauncher/AppLauncher.qml +++ b/Modules/AppLauncher/AppLauncher.qml @@ -374,7 +374,7 @@ NLoader { property bool isSelected: index === selectedIndex color: (appCardArea.containsMouse || isSelected) ? Qt.darker(Color.mPrimary, 1.1) : Color.mSurface - border.color: (appCardArea.containsMouse || isSelected) ? Color.mPrimary : "transparent" + border.color: (appCardArea.containsMouse || isSelected) ? Color.mPrimary : Color.transparent border.width: Math.max(1, (appCardArea.containsMouse || isSelected) ? Style.borderMedium * scaling : 0) diff --git a/Modules/Background/Background.qml b/Modules/Background/Background.qml index c94511b..ea8c721 100644 --- a/Modules/Background/Background.qml +++ b/Modules/Background/Background.qml @@ -22,7 +22,7 @@ Variants { } } - color: "transparent" + color: Color.transparent screen: modelData WlrLayershell.layer: WlrLayer.Background WlrLayershell.exclusionMode: ExclusionMode.Ignore diff --git a/Modules/Background/Overview.qml b/Modules/Background/Overview.qml index 01abc29..75a3a95 100644 --- a/Modules/Background/Overview.qml +++ b/Modules/Background/Overview.qml @@ -26,7 +26,7 @@ NLoader { && !Settings.data.wallpaper.swww.enabled ? WallpaperService.currentWallpaper : "" visible: wallpaperSource !== "" && !Settings.data.wallpaper.swww.enabled - color: "transparent" + color: Color.transparent screen: modelData WlrLayershell.layer: WlrLayer.Background WlrLayershell.exclusionMode: ExclusionMode.Ignore diff --git a/Modules/Background/ScreenCorners.qml b/Modules/Background/ScreenCorners.qml index 185eb77..730d800 100644 --- a/Modules/Background/ScreenCorners.qml +++ b/Modules/Background/ScreenCorners.qml @@ -27,7 +27,7 @@ NLoader { // Rounded radius for the inner cutout property int innerRadius: 20 - color: "transparent" + color: Color.transparent WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.namespace: "quickshell-corner" diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index a3388a0..8f4b023 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -18,7 +18,7 @@ Variants { screen: modelData implicitHeight: Style.barHeight * scaling - color: "transparent" + color: Color.transparent // If no bar activated in settings, then show them all visible: modelData ? (Settings.data.bar.monitors.includes(modelData.name) diff --git a/Modules/Bar/BluetoothMenu.qml b/Modules/Bar/BluetoothMenu.qml index bff9ecf..9153de8 100644 --- a/Modules/Bar/BluetoothMenu.qml +++ b/Modules/Bar/BluetoothMenu.qml @@ -271,7 +271,7 @@ NLoader { Rectangle { anchors.fill: parent radius: Style.radiusMedium * scaling - color: modelData.device.connected ? Color.mPrimary : (deviceMouseArea.containsMouse ? Color.mTertiary : "transparent") + color: modelData.device.connected ? Color.mPrimary : (deviceMouseArea.containsMouse ? Color.mTertiary : Color.transparent) RowLayout { anchors.fill: parent diff --git a/Modules/Bar/Tray.qml b/Modules/Bar/Tray.qml index 83caea2..4439431 100644 --- a/Modules/Bar/Tray.qml +++ b/Modules/Bar/Tray.qml @@ -172,7 +172,7 @@ Item { Rectangle { id: trayMenuRect - color: "transparent" + color: Color.transparent anchors.fill: parent // Animation properties diff --git a/Modules/Bar/TrayMenu.qml b/Modules/Bar/TrayMenu.qml index 5e342b2..50c61c0 100644 --- a/Modules/Bar/TrayMenu.qml +++ b/Modules/Bar/TrayMenu.qml @@ -17,7 +17,7 @@ PopupWindow { implicitWidth: Style.baseWidgetSize * 5.625 * scaling implicitHeight: Math.max(60 * scaling, listView.contentHeight + (Style.marginMedium * 2 * scaling)) visible: false - color: "transparent" + color: Color.transparent anchor.item: anchorItem ? anchorItem : null anchor.rect.x: anchorX @@ -99,7 +99,7 @@ PopupWindow { width: listView.width height: (modelData?.isSeparator) ? 8 * scaling : Math.max(32 * scaling, text.height + 8) - color: "transparent" + color: Color.transparent property var subMenu: null @@ -112,7 +112,7 @@ PopupWindow { Rectangle { id: bg anchors.fill: parent - color: mouseArea.containsMouse ? Color.mTertiary : "transparent" + color: mouseArea.containsMouse ? Color.mTertiary : Color.transparent radius: Style.radiusSmall * scaling visible: !(modelData?.isSeparator ?? false) @@ -254,7 +254,7 @@ PopupWindow { implicitWidth: Style.baseWidgetSize * 5.625 * scaling implicitHeight: Math.max(40, listView.contentHeight + 12) visible: false - color: "transparent" + color: Color.transparent property QsMenuHandle menu property var anchorItem: null @@ -334,7 +334,7 @@ PopupWindow { width: listView.width height: (modelData?.isSeparator) ? 8 * scaling : Math.max(32 * scaling, subText.height + 8) - color: "transparent" + color: Color.transparent property var subMenu: null @@ -347,7 +347,7 @@ PopupWindow { Rectangle { id: bg anchors.fill: parent - color: mouseArea.containsMouse ? Color.mTertiary : "transparent" + color: mouseArea.containsMouse ? Color.mTertiary : Color.transparent radius: Style.radiusSmall * scaling visible: !(modelData?.isSeparator ?? false) property color hoverTextColor: mouseArea.containsMouse ? Color.mOnSurface : Color.mOnSurface diff --git a/Modules/Bar/WiFiMenu.qml b/Modules/Bar/WiFiMenu.qml index d16c020..71c02fe 100644 --- a/Modules/Bar/WiFiMenu.qml +++ b/Modules/Bar/WiFiMenu.qml @@ -243,7 +243,7 @@ NLoader { Layout.fillWidth: true Layout.preferredHeight: Style.baseWidgetSize * 1.5 * scaling radius: Style.radiusMedium * scaling - color: modelData.connected ? Color.mPrimary : (networkMouseArea.containsMouse ? Color.mTertiary : "transparent") + color: modelData.connected ? Color.mPrimary : (networkMouseArea.containsMouse ? Color.mTertiary : Color.transparent) RowLayout { anchors.fill: parent @@ -377,7 +377,7 @@ NLoader { Rectangle { anchors.fill: parent radius: Style.radiusTiny * scaling - color: "transparent" + color: Color.transparent border.color: passwordInputField.activeFocus ? Color.mPrimary : Color.mOutline border.width: Math.max(1, Style.borderThin * scaling) diff --git a/Modules/Bar/Workspace.qml b/Modules/Bar/Workspace.qml index c967f36..d077bc1 100644 --- a/Modules/Bar/Workspace.qml +++ b/Modules/Bar/Workspace.qml @@ -244,7 +244,7 @@ Item { width: workspacePillContainer.width + 18 * root.masterProgress * scale height: workspacePillContainer.height + 18 * root.masterProgress * scale radius: width / 2 - color: "transparent" + color: Color.transparent border.color: root.effectColor border.width: Math.max(1, Math.round((2 + 6 * (1.0 - root.masterProgress)) * scaling)) opacity: root.effectsActive && model.isFocused ? (1.0 - root.masterProgress) * 0.7 : 0 diff --git a/Modules/Calendar/Calendar.qml b/Modules/Calendar/Calendar.qml index b620964..ac99390 100644 --- a/Modules/Calendar/Calendar.qml +++ b/Modules/Calendar/Calendar.qml @@ -211,7 +211,7 @@ NLoader { width: (Style.baseWidgetSize * scaling) height: (Style.baseWidgetSize * scaling) radius: Style.radiusSmall * scaling - color: model.today ? Color.mPrimary : "transparent" + color: model.today ? Color.mPrimary : Color.transparent NText { anchors.centerIn: parent diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml index d873002..a47fff9 100644 --- a/Modules/Dock/Dock.qml +++ b/Modules/Dock/Dock.qml @@ -46,7 +46,7 @@ NLoader { anchors.left: true anchors.right: true focusable: false - color: "transparent" + color: Color.transparent implicitHeight: iconSize * 1.4 * scaling // Watch for autoHide setting changes @@ -182,7 +182,7 @@ NLoader { id: appButton width: iconSize * scaling height: iconSize * scaling - color: "transparent" + color: Color.transparent radius: Style.radiusMedium * scaling property bool isActive: ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData @@ -194,7 +194,7 @@ NLoader { Rectangle { id: hoverBackground anchors.fill: parent - color: appButton.hovered ? Color.mSurfaceVariant : "transparent" + color: appButton.hovered ? Color.mSurfaceVariant : Color.transparent radius: parent.radius opacity: appButton.hovered ? 0.8 : 0 diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index 47d965c..4e88187 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -146,7 +146,7 @@ WlSessionLock { // Blurred background Rectangle { anchors.fill: parent - color: "transparent" + color: Color.transparent // Simple blur effect layer.enabled: true @@ -266,7 +266,7 @@ WlSessionLock { width: 120 * scaling height: 120 * scaling radius: width * 0.5 - color: "transparent" + color: Color.transparent border.color: Color.mPrimary border.width: Math.max(1, Style.borderThick * scaling) anchors.horizontalCenter: parent.horizontalCenter @@ -277,7 +277,7 @@ WlSessionLock { width: parent.width + 24 * scaling height: parent.height + 24 * scaling radius: width * 0.5 - color: "transparent" + color: Color.transparent border.color: Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.3) border.width: Math.max(1, Style.borderMedium * scaling) z: -1 @@ -572,7 +572,7 @@ WlSessionLock { // Status messages Text { text: lock.authenticating ? "Authenticating..." : (lock.errorMessage !== "" ? "Authentication failed." : "") - color: lock.authenticating ? Color.mPrimary : (lock.errorMessage !== "" ? Color.mError : "transparent") + color: lock.authenticating ? Color.mPrimary : (lock.errorMessage !== "" ? Color.mError : Color.transparent) font.family: "DejaVu Sans Mono" font.pointSize: Style.fontSizeLarge Layout.fillWidth: true @@ -659,7 +659,7 @@ WlSessionLock { Rectangle { anchors.fill: parent radius: parent.radius - color: "transparent" + color: Color.transparent border.color: Color.applyOpacity(Color.mPrimary, "4D") border.width: Math.max(1, Style.borderThin * scaling) z: -1 @@ -706,7 +706,7 @@ WlSessionLock { width: parent.width + 10 * scaling height: parent.height + 10 * scaling radius: width * 0.5 - color: "transparent" + color: Color.transparent border.color: Qt.rgba(Color.mError.r, Color.mError.g, Color.mError.b, 0.3) border.width: Math.max(1, Style.borderMedium * scaling) opacity: shutdownArea.containsMouse ? 1 : 0 @@ -763,7 +763,7 @@ WlSessionLock { width: parent.width + 10 * scaling height: parent.height + 10 * scaling radius: width * 0.5 - color: "transparent" + color: Color.transparent border.color: Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.3) border.width: Math.max(1, Style.borderMedium * scaling) opacity: rebootArea.containsMouse ? 1 : 0 @@ -819,7 +819,7 @@ WlSessionLock { width: parent.width + 10 * scaling height: parent.height + 10 * scaling radius: width * 0.5 - color: "transparent" + color: Color.transparent border.color: Qt.rgba(Color.mSecondary.r, Color.mSecondary.g, Color.mSecondary.b, 0.3) border.width: Math.max(1, Style.borderMedium * scaling) opacity: logoutArea.containsMouse ? 1 : 0 diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index 5b2dd24..f2e085c 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -25,7 +25,7 @@ Variants { // Track notifications being removed for animation property var removingNotifications: ({}) - color: "transparent" + color: Color.transparent // If no notification display activated in settings, then show them all visible: modelData ? (Settings.data.notifications.monitors.includes(modelData.name) diff --git a/Modules/SettingsPanel/SettingsPanel.qml b/Modules/SettingsPanel/SettingsPanel.qml index 4e980ce..62a0d24 100644 --- a/Modules/SettingsPanel/SettingsPanel.qml +++ b/Modules/SettingsPanel/SettingsPanel.qml @@ -265,7 +265,7 @@ NLoader { width: parent.width height: 32 * scaling radius: Style.radiusSmall * scaling - color: selected ? Color.mPrimary : (tabItem.hovering ? Color.mTertiary : "transparent") + color: selected ? Color.mPrimary : (tabItem.hovering ? Color.mTertiary : Color.transparent) readonly property bool selected: index === currentTabIndex property bool hovering: false property color tabTextColor: selected ? Color.mOnPrimary : (tabItem.hovering ? Color.mOnTertiary : Color.mOnSurface) diff --git a/Modules/SettingsPanel/Tabs/AboutTab.qml b/Modules/SettingsPanel/Tabs/AboutTab.qml index 811a406..c902c87 100644 --- a/Modules/SettingsPanel/Tabs/AboutTab.qml +++ b/Modules/SettingsPanel/Tabs/AboutTab.qml @@ -110,7 +110,7 @@ ColumnLayout { Layout.preferredWidth: updateText.implicitWidth + 46 * scaling Layout.preferredHeight: Style.barHeight * scaling radius: Style.radiusLarge * scaling - color: updateArea.containsMouse ? Color.mPrimary : "transparent" + color: updateArea.containsMouse ? Color.mPrimary : Color.transparent border.color: Color.mPrimary border.width: Math.max(1, Style.borderThin * scaling) visible: { @@ -200,7 +200,7 @@ ColumnLayout { width: contributorsGrid.cellWidth - Style.marginLarge * scaling height: contributorsGrid.cellHeight - Style.marginTiny * scaling radius: Style.radiusLarge * scaling - color: contributorArea.containsMouse ? Color.mTertiary : "transparent" + color: contributorArea.containsMouse ? Color.mTertiary : Color.transparent RowLayout { anchors.fill: parent diff --git a/Modules/SidePanel/Cards/MediaCard.qml b/Modules/SidePanel/Cards/MediaCard.qml index aee2166..df02609 100644 --- a/Modules/SidePanel/Cards/MediaCard.qml +++ b/Modules/SidePanel/Cards/MediaCard.qml @@ -76,7 +76,7 @@ NBox { visible: false // implicitWidth: 120 * scaling // implicitHeight: 30 * scaling - color: "transparent" + color: Color.transparent border.color: playerSelector.activeFocus ? Color.mTertiary : Color.mOutline border.width: Math.max(1, Style.borderThin * scaling) radius: Style.radiusMedium * scaling @@ -140,7 +140,7 @@ NBox { background: Rectangle { width: popup.width - Style.marginSmall * scaling * 2 - color: highlighted ? Color.mTertiary : "transparent" + color: highlighted ? Color.mTertiary : Color.transparent radius: Style.radiusTiny * scaling } } @@ -161,8 +161,8 @@ NBox { width: 90 * scaling height: 90 * scaling radius: width * 0.5 - color: trackArt.visible ? Color.mPrimary : "transparent" - border.color: trackArt.visible ? Color.mOutline : "transparent" + color: trackArt.visible ? Color.mPrimary : Color.transparent + border.color: trackArt.visible ? Color.mOutline : Color.transparent border.width: Math.max(1, Style.borderThin * scaling) clip: true diff --git a/Modules/SidePanel/PowerMenu.qml b/Modules/SidePanel/PowerMenu.qml index 02d6ed9..35c5c1b 100644 --- a/Modules/SidePanel/PowerMenu.qml +++ b/Modules/SidePanel/PowerMenu.qml @@ -57,7 +57,7 @@ NPanel { Layout.fillWidth: true Layout.preferredHeight: Style.barHeight * scaling radius: Style.radiusSmall * scaling - color: lockButtonArea.containsMouse ? Color.mTertiary : "transparent" + color: lockButtonArea.containsMouse ? Color.mTertiary : Color.transparent Item { anchors.left: parent.left @@ -117,7 +117,7 @@ NPanel { Layout.fillWidth: true Layout.preferredHeight: Style.barHeight * scaling radius: Style.radiusSmall * scaling - color: suspendButtonArea.containsMouse ? Color.mTertiary : "transparent" + color: suspendButtonArea.containsMouse ? Color.mTertiary : Color.transparent Item { anchors.left: parent.left @@ -175,7 +175,7 @@ NPanel { Layout.fillWidth: true Layout.preferredHeight: Style.barHeight * scaling radius: Style.radiusSmall * scaling - color: rebootButtonArea.containsMouse ? Color.mTertiary : "transparent" + color: rebootButtonArea.containsMouse ? Color.mTertiary : Color.transparent Item { anchors.left: parent.left @@ -233,7 +233,7 @@ NPanel { Layout.fillWidth: true Layout.preferredHeight: Style.barHeight * scaling radius: Style.radiusSmall * scaling - color: logoutButtonArea.containsMouse ? Color.mTertiary : "transparent" + color: logoutButtonArea.containsMouse ? Color.mTertiary : Color.transparent Item { anchors.left: parent.left @@ -291,7 +291,7 @@ NPanel { Layout.fillWidth: true Layout.preferredHeight: Style.barHeight * scaling radius: Style.radiusSmall * scaling - color: shutdownButtonArea.containsMouse ? Color.mTertiary : "transparent" + color: shutdownButtonArea.containsMouse ? Color.mTertiary : Color.transparent Item { anchors.left: parent.left diff --git a/Widgets/NCircleStat.qml b/Widgets/NCircleStat.qml index cc33052..a406649 100644 --- a/Widgets/NCircleStat.qml +++ b/Widgets/NCircleStat.qml @@ -18,9 +18,9 @@ Rectangle { width: 68 * scaling height: 92 * scaling - color: flat ? "transparent" : Color.mSurface + color: flat ? Color.transparent : Color.mSurface radius: Style.radiusSmall * scaling - border.color: flat ? "transparent" : Color.mSurfaceVariant + border.color: flat ? Color.transparent : Color.mSurfaceVariant border.width: flat ? 0 : Math.max(1, Style.borderThin * scaling) clip: true diff --git a/Widgets/NClock.qml b/Widgets/NClock.qml index b46a0c5..f1c0a9b 100644 --- a/Widgets/NClock.qml +++ b/Widgets/NClock.qml @@ -12,7 +12,7 @@ Rectangle { width: textItem.paintedWidth height: textItem.paintedHeight - color: "transparent" + color: Color.transparent NText { id: textItem diff --git a/Widgets/NComboBox.qml b/Widgets/NComboBox.qml index 671289e..85c6b7d 100644 --- a/Widgets/NComboBox.qml +++ b/Widgets/NComboBox.qml @@ -125,7 +125,7 @@ ColumnLayout { background: Rectangle { width: combo.width - Style.marginMedium * scaling * 3 - color: highlighted ? Color.mTertiary : "transparent" + color: highlighted ? Color.mTertiary : Color.transparent radius: Style.radiusSmall * scaling } } diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml index 2d4b3e8..13b4247 100644 --- a/Widgets/NIconButton.qml +++ b/Widgets/NIconButton.qml @@ -26,9 +26,9 @@ Rectangle { implicitWidth: size implicitHeight: size - color: (root.hovering || showFilled) ? Color.mPrimary : "transparent" + color: (root.hovering || showFilled) ? Color.mPrimary : Color.transparent radius: width * 0.5 - border.color: showBorder ? Color.mPrimary : "transparent" + border.color: showBorder ? Color.mPrimary : Color.transparent border.width: Math.max(1, Style.borderThin * scaling) NText { diff --git a/Widgets/NImageRounded.qml b/Widgets/NImageRounded.qml index edf3b2b..632e73f 100644 --- a/Widgets/NImageRounded.qml +++ b/Widgets/NImageRounded.qml @@ -7,13 +7,13 @@ import qs.Services Rectangle { id: root - color: "transparent" + color: Color.transparent property real imageRadius: width * 0.5 radius: imageRadius property string imagePath: "" property string fallbackIcon: "" - property color borderColor: "transparent" + property color borderColor: Color.transparent property real borderWidth: 0 anchors.margins: Style.marginTiniest * scaling @@ -22,7 +22,7 @@ Rectangle { Rectangle { anchors.fill: parent radius: parent.radius - color: "transparent" + color: Color.transparent border.color: parent.borderColor border.width: parent.borderWidth z: 10 diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 7fb636b..5717964 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -12,7 +12,7 @@ PanelWindow { property bool showOverlay: Settings.data.general.dimDesktop property int topMargin: Style.barHeight * scaling // Show dimming if this panel is opened OR if we're in a transition (to prevent flickering) - property color overlayColor: (showOverlay && (PanelService.openedPanel === root || isTransitioning)) ? Color.applyOpacity(Color.mShadow, "AA") : "transparent" + property color overlayColor: (showOverlay && (PanelService.openedPanel === root || isTransitioning)) ? Color.applyOpacity(Color.mShadow, "AA") : Color.transparent property bool isTransitioning: false signal dismissed @@ -49,7 +49,7 @@ PanelWindow { implicitWidth: screen.width implicitHeight: screen.height - color: visible ? overlayColor : "transparent" + color: visible ? overlayColor : Color.transparent visible: false WlrLayershell.exclusionMode: ExclusionMode.Ignore diff --git a/Widgets/NPill.qml b/Widgets/NPill.qml index b5ce985..403029c 100644 --- a/Widgets/NPill.qml +++ b/Widgets/NPill.qml @@ -80,7 +80,7 @@ Item { width: iconSize height: iconSize radius: width * 0.5 - color: showPill ? iconCircleColor : "transparent" + color: showPill ? iconCircleColor : Color.transparent anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right diff --git a/Widgets/NRadioButton.qml b/Widgets/NRadioButton.qml index b3c92cf..369f7e0 100644 --- a/Widgets/NRadioButton.qml +++ b/Widgets/NRadioButton.qml @@ -13,7 +13,7 @@ RadioButton { implicitWidth: Style.baseWidgetSize * 0.625 * scaling implicitHeight: Style.baseWidgetSize * 0.625 * scaling radius: width * 0.5 - color: "transparent" + color: Color.transparent border.color: root.checked ? Color.mPrimary : Color.mOnSurface border.width: Math.max(1, Style.borderMedium * scaling) anchors.verticalCenter: parent.verticalCenter diff --git a/Widgets/NTextInput.qml b/Widgets/NTextInput.qml index 7441001..5064c47 100644 --- a/Widgets/NTextInput.qml +++ b/Widgets/NTextInput.qml @@ -55,8 +55,8 @@ Item { Rectangle { anchors.fill: parent radius: frame.radius - color: "transparent" - border.color: input.activeFocus ? Color.mTertiary : "transparent" + color: Color.transparent + border.color: input.activeFocus ? Color.mTertiary : Color.transparent border.width: input.activeFocus ? Math.max(1, Style.borderThin * scaling) : 0 } diff --git a/Widgets/NTooltip.qml b/Widgets/NTooltip.qml index 2d02be9..9e7eb68 100644 --- a/Widgets/NTooltip.qml +++ b/Widgets/NTooltip.qml @@ -12,7 +12,7 @@ Window { property bool positionAbove: false flags: Qt.ToolTip | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint - color: "transparent" + color: Color.transparent visible: false onIsVisibleChanged: { From 5aeeb8458f2256c1db05c4a036d39ef6a424c9cd Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 17 Aug 2025 12:33:28 +0200 Subject: [PATCH 371/394] Add light/dark mode toggle for matugen --- Commons/Settings.qml | 1 + Modules/SettingsPanel/Tabs/ColorSchemeTab.qml | 16 +++++++++++++++- Services/ColorSchemeService.qml | 10 +++++++++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 0b1f496..be7f21b 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -199,6 +199,7 @@ Singleton { colorSchemes: JsonObject { property bool useWallpaperColors: false property string predefinedScheme: "" + property bool darkMode: true } } } diff --git a/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml b/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml index e159e32..75f3bce 100644 --- a/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml +++ b/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml @@ -138,6 +138,20 @@ ColumnLayout { } } + // Dark Mode Toggle + NToggle { + label: "Dark Mode" + description: "Generate dark theme colors when using Matugen. Disable for light theme." + checked: Settings.data.colorSchemes.darkMode + enabled: Settings.data.colorSchemes.useWallpaperColors + onToggled: checked => { + Settings.data.colorSchemes.darkMode = checked + if (Settings.data.colorSchemes.useWallpaperColors) { + ColorSchemeService.changedWallpaper() + } + } + } + NDivider { Layout.fillWidth: true Layout.topMargin: Style.marginLarge * scaling @@ -157,7 +171,7 @@ ColumnLayout { } NText { - text: "These color schemes only apply when 'Use Matugen' is disabled. When enabled, Matugen will generate colors based on your wallpaper instead." + text: "These color schemes only apply when 'Use Matugen' is disabled. When enabled, Matugen will generate colors based on your wallpaper instead. You can toggle between light and dark themes when using Matugen." font.pointSize: Style.fontSizeSmall * scaling color: Color.mOnSurface Layout.fillWidth: true diff --git a/Services/ColorSchemeService.qml b/Services/ColorSchemeService.qml index 148a05c..2dcab0f 100644 --- a/Services/ColorSchemeService.qml +++ b/Services/ColorSchemeService.qml @@ -62,7 +62,15 @@ Singleton { Process { id: generateColorsProcess - command: ["matugen", "image", WallpaperService.currentWallpaper, "--config", Quickshell.shellDir + "/Assets/Matugen/matugen.toml"] + command: { + var cmd = ["matugen", "image", WallpaperService.currentWallpaper, "--config", Quickshell.shellDir + "/Assets/Matugen/matugen.toml"] + if (!Settings.data.colorSchemes.darkMode) { + cmd.push("--mode", "light") + } else { + cmd.push("--mode", "dark") + } + return cmd + } workingDirectory: Quickshell.shellDir running: false stdout: StdioCollector { From 5f850e1ce9bddd4f7cc733a84bca6652797f3f9c Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 17 Aug 2025 06:37:26 -0400 Subject: [PATCH 372/394] Hiding demo panel for release --- Modules/Bar/Bar.qml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 8f4b023..bce4975 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -128,17 +128,17 @@ Variants { anchors.verticalCenter: parent.verticalCenter } - NIconButton { - id: demoPanelToggle - icon: "experiment" - tooltipText: "Open Demo Panel" - sizeMultiplier: 0.8 - showBorder: false - anchors.verticalCenter: parent.verticalCenter - onClicked: { - demoPanel.isLoaded = !demoPanel.isLoaded - } - } + // NIconButton { + // id: demoPanelToggle + // icon: "experiment" + // tooltipText: "Open Demo Panel" + // sizeMultiplier: 0.8 + // showBorder: false + // anchors.verticalCenter: parent.verticalCenter + // onClicked: { + // demoPanel.isLoaded = !demoPanel.isLoaded + // } + // } NIconButton { id: sidePanelToggle From db97de79eb765d23c4f15424fff140a38cc253cf Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 17 Aug 2025 06:39:08 -0400 Subject: [PATCH 373/394] Removed outdated comment --- Modules/SettingsPanel/Tabs/ColorSchemeTab.qml | 1 - 1 file changed, 1 deletion(-) diff --git a/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml b/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml index 75f3bce..5810740 100644 --- a/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml +++ b/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml @@ -212,7 +212,6 @@ ColumnLayout { anchors.fill: parent onClicked: { // Disable useWallpaperColors when picking a predefined color scheme - // TBC: broken uncheck useWallpaperColors Settings.data.colorSchemes.useWallpaperColors = false Settings.data.colorSchemes.predefinedScheme = schemePath ColorSchemeService.applyScheme(schemePath) From c992584082dd9ef8d446e862821bf835406a8b6b Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 17 Aug 2025 06:45:22 -0400 Subject: [PATCH 374/394] Weather: fixed another TBC, reset was not called. --- Modules/SettingsPanel/Tabs/TimeWeatherTab.qml | 1 + Services/LocationService.qml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/SettingsPanel/Tabs/TimeWeatherTab.qml b/Modules/SettingsPanel/Tabs/TimeWeatherTab.qml index 5d93ee1..4b9b88e 100644 --- a/Modules/SettingsPanel/Tabs/TimeWeatherTab.qml +++ b/Modules/SettingsPanel/Tabs/TimeWeatherTab.qml @@ -55,6 +55,7 @@ ColumnLayout { Layout.fillWidth: true onEditingFinished: { Settings.data.location.name = text + LocationService.resetWeather() } } } diff --git a/Services/LocationService.qml b/Services/LocationService.qml index 23243d2..3a4744e 100644 --- a/Services/LocationService.qml +++ b/Services/LocationService.qml @@ -91,7 +91,7 @@ Singleton { if ((data.latitude === "") || (data.longitude === "") || (data.name !== Settings.data.location.name)) { _geocodeLocation(Settings.data.location.name, function (latitude, longitude) { - Logger.log("Location", " Geocoded", Settings.data.location.name, "to:", latitude, "/", longitude) + Logger.log("Location", "Geocoded", Settings.data.location.name, "to:", latitude, "/", longitude) // Save location name data.name = Settings.data.location.name From 8178e96d04886814b90f4c824ce74013893d7c12 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 17 Aug 2025 07:02:40 -0400 Subject: [PATCH 375/394] WiFi: removing those two icons which leads to confusion / not working. Need to revisit later. --- Modules/Bar/WiFiMenu.qml | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/Modules/Bar/WiFiMenu.qml b/Modules/Bar/WiFiMenu.qml index 71c02fe..d819b55 100644 --- a/Modules/Bar/WiFiMenu.qml +++ b/Modules/Bar/WiFiMenu.qml @@ -304,26 +304,6 @@ NLoader { anchors.centerIn: parent size: Style.baseWidgetSize * 0.7 * scaling } - - // TBC: Does nothing on my setup - NText { - visible: NetworkService.connectStatus === "success" && !NetworkService.connectingSsid - text: "check_circle" - font.family: "Material Symbols Outlined" - font.pointSize: Style.fontSizeXL * scaling - color: "#43a047" // TBC: No! - anchors.centerIn: parent - } - - // TBC: Does nothing on my setup - NText { - visible: NetworkService.connectStatus === "error" && !NetworkService.connectingSsid - text: "error" - font.family: "Material Symbols Outlined" - font.pointSize: Style.fontSizeSmall * scaling - color: Color.mError - anchors.centerIn: parent - } } NText { From d66e92bafacfc150e59208ed3112ad4c1d950b2d Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 17 Aug 2025 13:06:15 +0200 Subject: [PATCH 376/394] Fix Brightness step size --- Services/BrightnessService.qml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Services/BrightnessService.qml b/Services/BrightnessService.qml index 1fcfee9..58aa084 100644 --- a/Services/BrightnessService.qml +++ b/Services/BrightnessService.qml @@ -146,11 +146,13 @@ Singleton { } function increaseBrightness(): void { - setBrightnessDebounced(brightness + 0.1) + var stepSize = Settings.data.brightness.brightnessStep / 100.0 + setBrightnessDebounced(brightness + stepSize) } function decreaseBrightness(): void { - setBrightnessDebounced(monitor.brightness - 0.1) + var stepSize = Settings.data.brightness.brightnessStep / 100.0 + setBrightnessDebounced(monitor.brightness - stepSize) } function getStoredBrightness(): real { From e6005238ecc177470269bfd0b54214b58f877fe9 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 17 Aug 2025 07:20:27 -0400 Subject: [PATCH 377/394] Settings tab order --- Modules/SettingsPanel/SettingsPanel.qml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Modules/SettingsPanel/SettingsPanel.qml b/Modules/SettingsPanel/SettingsPanel.qml index 62a0d24..a35bead 100644 --- a/Modules/SettingsPanel/SettingsPanel.qml +++ b/Modules/SettingsPanel/SettingsPanel.qml @@ -114,6 +114,7 @@ NLoader { Tabs.AboutTab {} } + // Order *DOES* matter property var tabsModel: [{ "id": SettingsPanel.Tab.General, "label": "General", @@ -129,12 +130,7 @@ NLoader { "label": "Audio", "icon": "volume_up", "source": audioTab - }, { - "id": SettingsPanel.Tab.Brightness, - "label": "Brightness", - "icon": "brightness_6", - "source": brightnessTab - }, { + },{ "id": SettingsPanel.Tab.Display, "label": "Display", "icon": "monitor", @@ -145,6 +141,11 @@ NLoader { "icon": "lan", "source": networkTab }, { + "id": SettingsPanel.Tab.Brightness, + "label": "Brightness", + "icon": "brightness_6", + "source": brightnessTab + }, { "id": SettingsPanel.Tab.TimeWeather, "label": "Time & Weather", "icon": "schedule", From 1e4ed52c51d49273744081377b7a14f17f6f54a0 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 17 Aug 2025 07:35:44 -0400 Subject: [PATCH 378/394] BrightnessWIP --- Services/BrightnessService.qml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/Services/BrightnessService.qml b/Services/BrightnessService.qml index 58aa084..ea8d9b8 100644 --- a/Services/BrightnessService.qml +++ b/Services/BrightnessService.qml @@ -61,10 +61,12 @@ Singleton { command: ["ddcutil", "detect", "--brief"] stdout: StdioCollector { onStreamFinished: { - var displays = text.trim().split("\n\n").filter(d => d.startsWith("Display ")) + // Do not filter out invalid displays. For some reason --brief returns some invalid which works fine + var displays = text.trim().split("\n\n") root.ddcMonitors = displays.map(d => { var modelMatch = d.match(/Monitor:.*:(.*):.*/) var busMatch = d.match(/I2C bus:[ ]*\/dev\/i2c-([0-9]+)/) + console.log(modelMatch) return { "model": modelMatch ? modelMatch[1] : "", "busNum": busMatch ? busMatch[1] : "" @@ -94,16 +96,20 @@ Singleton { readonly property Process initProc: Process { stdout: StdioCollector { onStreamFinished: { - Logger.log("Brightness", "Raw brightness data for", monitor.modelData.name + ":", text.trim()) + var dataText = text.trim(); + if (dataText === "") { + return + } + Logger.log("Brightness", "Raw brightness data for", monitor.modelData.name + ":", dataText) if (monitor.isAppleDisplay) { - var val = parseInt(text.trim()) + var val = parseInt(dataText) if (!isNaN(val)) { monitor.brightness = val / 101 Logger.log("Brightness", "Apple display brightness:", monitor.brightness) } } else if (monitor.isDdc) { - var parts = text.trim().split(" ") + var parts = dataText.split(" ") if (parts.length >= 2) { var current = parseInt(parts[0]) var max = parseInt(parts[1]) @@ -114,7 +120,7 @@ Singleton { } } else { // Internal backlight - var parts = text.trim().split(" ") + var parts = dataText.split(" ") if (parts.length >= 2) { var current = parseInt(parts[0]) var max = parseInt(parts[1]) From 593821e998415ae2a9861a9f894f7bd59c4373fe Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 17 Aug 2025 07:44:40 -0400 Subject: [PATCH 379/394] Brightness improvements - we dont want to save brightness in settings as it is saved in the hardware - fixed a few DDCutil weirdness --- Commons/Settings.qml | 3 -- Modules/Bar/Brightness.qml | 2 +- Services/BrightnessService.qml | 60 ++++------------------------------ 3 files changed, 7 insertions(+), 58 deletions(-) diff --git a/Commons/Settings.qml b/Commons/Settings.qml index be7f21b..b4048a7 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -188,9 +188,6 @@ Singleton { property JsonObject brightness brightness: JsonObject { - property real lastBrightness: 50.0 - property string lastMethod: "internal" - property list monitorBrightness: [] property int brightnessStep: 5 } diff --git a/Modules/Bar/Brightness.qml b/Modules/Bar/Brightness.qml index 7d47b53..882fb49 100644 --- a/Modules/Bar/Brightness.qml +++ b/Modules/Bar/Brightness.qml @@ -10,7 +10,7 @@ Item { width: pill.width height: pill.height - visible: Settings.data.bar.showBrightness + visible: Settings.data.bar.showBrightness && firstBrightnessReceived // Used to avoid opening the pill on Quickshell startup property bool firstBrightnessReceived: false diff --git a/Services/BrightnessService.qml b/Services/BrightnessService.qml index ea8d9b8..d0fffb1 100644 --- a/Services/BrightnessService.qml +++ b/Services/BrightnessService.qml @@ -85,7 +85,7 @@ Singleton { readonly property bool isAppleDisplay: root.appleDisplayPresent && modelData.model.startsWith("StudioDisplay") readonly property string method: isAppleDisplay ? "apple" : (isDdc ? "ddcutil" : "internal") - property real brightness: getStoredBrightness() + property real brightness property real lastBrightness: 0 property real queuedBrightness: NaN @@ -110,9 +110,9 @@ Singleton { } } else if (monitor.isDdc) { var parts = dataText.split(" ") - if (parts.length >= 2) { - var current = parseInt(parts[0]) - var max = parseInt(parts[1]) + if (parts.length >= 4) { + var current = parseInt(parts[3]) + var max = parseInt(parts[4]) if (!isNaN(current) && !isNaN(max) && max > 0) { monitor.brightness = current / max Logger.log("Brightness", "DDC brightness:", current + "/" + max + " =", monitor.brightness) @@ -131,11 +131,8 @@ Singleton { } } - if (monitor.brightness > 0) { - // Save the detected brightness to settings - monitor.saveBrightness(monitor.brightness) - monitor.brightnessUpdated(monitor.brightness) - } + // Always update + monitor.brightnessUpdated(monitor.brightness) } } } @@ -161,48 +158,6 @@ Singleton { setBrightnessDebounced(monitor.brightness - stepSize) } - function getStoredBrightness(): real { - // Try to get stored brightness for this specific monitor - var stored = Settings.data.brightness.monitorBrightness.find(m => { - if (m !== null) { - return m.name === modelData.name - } - return false - }) - if (stored) { - return stored.brightness / 100 - } - // Fallback to general last brightness - return Settings.data.brightness.lastBrightness / 100 - } - - function saveBrightness(value: real): void { - var brightnessPercent = Math.round(value * 100) - - // Update general last brightness - Settings.data.brightness.lastBrightness = brightnessPercent - Settings.data.brightness.lastMethod = method - - // Update monitor-specific brightness - var monitorIndex = Settings.data.brightness.monitorBrightness.findIndex(m => { - if (m !== null) { - return m.name === modelData.name - } - return -1 - }) - var monitorData = { - "name": modelData.name, - "brightness": brightnessPercent, - "method": method - } - - if (monitorIndex >= 0) { - Settings.data.brightness.monitorBrightness[monitorIndex] = monitorData - } else { - Settings.data.brightness.monitorBrightness.push(monitorData) - } - } - function setBrightness(value: real): void { value = Math.max(0, Math.min(1, value)) var rounded = Math.round(value * 100) @@ -218,9 +173,6 @@ Singleton { brightness = value brightnessUpdated(brightness) - // Save to settings - saveBrightness(value) - if (isAppleDisplay) { Quickshell.execDetached(["asdbctl", "set", rounded]) } else if (isDdc) { From 4ed7324a99bd4f8989b1411453c788d356333205 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 17 Aug 2025 08:03:09 -0400 Subject: [PATCH 380/394] Brightness, fully working (at least on my machine) --- Modules/Bar/Bar.qml | 1 - Modules/Bar/Brightness.qml | 5 +- Modules/LockScreen/LockScreen.qml | 2 +- Modules/SettingsPanel/SettingsPanel.qml | 4 +- Modules/SettingsPanel/Tabs/BrightnessTab.qml | 142 ++++++++----------- Modules/SidePanel/PowerMenu.qml | 2 - Services/BrightnessService.qml | 3 +- Services/CompositorService.qml | 109 +++++++------- Services/WorkspaceService.qml | 2 +- Widgets/NPanel.qml | 4 +- 10 files changed, 124 insertions(+), 150 deletions(-) diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index bce4975..8a2c3f6 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -139,7 +139,6 @@ Variants { // demoPanel.isLoaded = !demoPanel.isLoaded // } // } - NIconButton { id: sidePanelToggle icon: "widgets" diff --git a/Modules/Bar/Brightness.qml b/Modules/Bar/Brightness.qml index 882fb49..0d26dfc 100644 --- a/Modules/Bar/Brightness.qml +++ b/Modules/Bar/Brightness.qml @@ -17,7 +17,8 @@ Item { function getIcon() { var brightness = BrightnessService.getMonitorForScreen(screen).brightness - return brightness <= 0 ? "brightness_1" : brightness < 0.33 ? "brightness_low" : brightness < 0.66 ? "brightness_medium" : "brightness_high" + return brightness <= 0 ? "brightness_1" : brightness < 0.33 ? "brightness_low" : brightness + < 0.66 ? "brightness_medium" : "brightness_high" } // Connection used to open the pill when brightness changes @@ -26,7 +27,7 @@ Item { function onBrightnessUpdated() { Logger.log("Bar-Brightness", "OnBrightnessUpdated") - var monitor = BrightnessService.getMonitorForScreen(screen); + var monitor = BrightnessService.getMonitorForScreen(screen) var currentBrightness = monitor.brightness // Ignore if this is the first time or if brightness hasn't actually changed diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index 4e88187..6a9bcf7 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -95,7 +95,7 @@ WlSessionLock { // Battery indicator component Item { id: batteryIndicator - + // Import UPower for battery data property var battery: UPower.displayDevice property bool isReady: battery && battery.ready && battery.isLaptopBattery && battery.isPresent diff --git a/Modules/SettingsPanel/SettingsPanel.qml b/Modules/SettingsPanel/SettingsPanel.qml index a35bead..ad0bff6 100644 --- a/Modules/SettingsPanel/SettingsPanel.qml +++ b/Modules/SettingsPanel/SettingsPanel.qml @@ -130,7 +130,7 @@ NLoader { "label": "Audio", "icon": "volume_up", "source": audioTab - },{ + }, { "id": SettingsPanel.Tab.Display, "label": "Display", "icon": "monitor", @@ -145,7 +145,7 @@ NLoader { "label": "Brightness", "icon": "brightness_6", "source": brightnessTab - }, { + }, { "id": SettingsPanel.Tab.TimeWeather, "label": "Time & Weather", "icon": "schedule", diff --git a/Modules/SettingsPanel/Tabs/BrightnessTab.qml b/Modules/SettingsPanel/Tabs/BrightnessTab.qml index 4f9f7df..142ef6e 100644 --- a/Modules/SettingsPanel/Tabs/BrightnessTab.qml +++ b/Modules/SettingsPanel/Tabs/BrightnessTab.qml @@ -143,102 +143,76 @@ Item { } // Single monitor display using the same data source as the bar icon - Rectangle { - Layout.fillWidth: true - radius: Style.radiusMedium * scaling - color: Color.mSurface - border.color: Color.mOutline - border.width: Math.max(1, Style.borderThin * scaling) - implicitHeight: contentCol.implicitHeight + Style.marginXL * 2 * scaling + Repeater { + model: BrightnessService.monitors + Rectangle { + Layout.fillWidth: true + radius: Style.radiusMedium * scaling + color: Color.mSurface + border.color: Color.mOutline + border.width: Math.max(1, Style.borderThin * scaling) + implicitHeight: contentCol.implicitHeight + Style.marginXL * 2 * scaling - ColumnLayout { - id: contentCol - anchors.fill: parent - anchors.margins: Style.marginLarge * scaling - spacing: Style.marginMedium * scaling - - RowLayout { - Layout.fillWidth: true + ColumnLayout { + id: contentCol + anchors.fill: parent + anchors.margins: Style.marginLarge * scaling spacing: Style.marginMedium * scaling - NText { - text: "Primary Monitor" - font.pointSize: Style.fontSizeLarge * scaling - font.weight: Style.fontWeightBold - color: Color.mSecondary - } - - Item { + RowLayout { Layout.fillWidth: true - } + spacing: Style.marginMedium * scaling - NText { - text: BrightnessService.currentMethod === "ddcutil" ? "External (DDC)" : "Internal" - font.pointSize: Style.fontSizeSmall * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignRight - } - } + NText { + text: `${model.modelData.name} [${model.modelData.model}]` + font.pointSize: Style.fontSizeLarge * scaling + font.weight: Style.fontWeightBold + color: Color.mSecondary + } - RowLayout { - Layout.fillWidth: true - spacing: Style.marginMedium * scaling + Item { + Layout.fillWidth: true + } - NText { - text: "Brightness:" - font.pointSize: Style.fontSizeMedium * scaling - color: Color.mOnSurface - } - - NSlider { - Layout.fillWidth: true - from: 0 - to: 100 - value: BrightnessService.brightness - stepSize: 1 - enabled: BrightnessService.available - onPressedChanged: { - if (!pressed && BrightnessService.available) { - BrightnessService.setBrightness(value) - } + NText { + text: model.method + font.pointSize: Style.fontSizeSmall * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignRight } } - NText { - text: BrightnessService.available ? Math.round(BrightnessService.brightness) + "%" : "N/A" - font.pointSize: Style.fontSizeMedium * scaling - font.weight: Style.fontWeightBold - color: BrightnessService.available ? Color.mPrimary : Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignRight - } - } - - RowLayout { - Layout.fillWidth: true - spacing: Style.marginMedium * scaling - - NText { - text: "Method:" - font.pointSize: Style.fontSizeSmall * scaling - color: Color.mOnSurfaceVariant - } - - NText { - text: BrightnessService.currentMethod || "Unknown" - font.pointSize: Style.fontSizeSmall * scaling - color: Color.mOnSurface - Layout.alignment: Qt.AlignLeft - } - - Item { + RowLayout { Layout.fillWidth: true - } + spacing: Style.marginMedium * scaling - NText { - text: BrightnessService.available ? "Available" : "Unavailable" - font.pointSize: Style.fontSizeSmall * scaling - color: BrightnessService.available ? Color.mPrimary : Color.mError - Layout.alignment: Qt.AlignRight + NText { + text: "Brightness:" + font.pointSize: Style.fontSizeMedium * scaling + color: Color.mOnSurface + } + + NSlider { + Layout.fillWidth: true + from: 0 + to: 1 + value: model.brightness + stepSize: 0.05 + onPressedChanged: { + if (!pressed) { + var monitor = BrightnessService.getMonitorForScreen(model.modelData) + monitor.setBrightness(value) + } + } + } + + NText { + text: Math.round(model.brightness * 100) + "%" + font.pointSize: Style.fontSizeMedium * scaling + font.weight: Style.fontWeightBold + color: Color.mPrimary + Layout.alignment: Qt.AlignRight + } } } } diff --git a/Modules/SidePanel/PowerMenu.qml b/Modules/SidePanel/PowerMenu.qml index 35c5c1b..5a8d7e8 100644 --- a/Modules/SidePanel/PowerMenu.qml +++ b/Modules/SidePanel/PowerMenu.qml @@ -384,8 +384,6 @@ NPanel { running: false } - - Process { id: logoutProcess diff --git a/Services/BrightnessService.qml b/Services/BrightnessService.qml index d0fffb1..f75404e 100644 --- a/Services/BrightnessService.qml +++ b/Services/BrightnessService.qml @@ -66,7 +66,6 @@ Singleton { root.ddcMonitors = displays.map(d => { var modelMatch = d.match(/Monitor:.*:(.*):.*/) var busMatch = d.match(/I2C bus:[ ]*\/dev\/i2c-([0-9]+)/) - console.log(modelMatch) return { "model": modelMatch ? modelMatch[1] : "", "busNum": busMatch ? busMatch[1] : "" @@ -96,7 +95,7 @@ Singleton { readonly property Process initProc: Process { stdout: StdioCollector { onStreamFinished: { - var dataText = text.trim(); + var dataText = text.trim() if (dataText === "") { return } diff --git a/Services/CompositorService.qml b/Services/CompositorService.qml index c74457b..745ed23 100644 --- a/Services/CompositorService.qml +++ b/Services/CompositorService.qml @@ -16,20 +16,20 @@ Singleton { property string compositorType: "unknown" // "hyprland", "niri", or "unknown" property bool isHyprland: false property bool isNiri: false - + // Generic workspace and window data property ListModel workspaces: ListModel {} property var windows: [] property int focusedWindowIndex: -1 property string focusedWindowTitle: "(No active window)" property bool inOverview: false - + // Generic events - signal workspaceChanged() - signal activeWindowChanged() - signal overviewStateChanged() - signal windowListChanged() - + signal workspaceChanged + signal activeWindowChanged + signal overviewStateChanged + signal windowListChanged + // Compositor detection Component.onCompleted: { detectCompositor() @@ -54,8 +54,6 @@ Singleton { } } - - function detectCompositor() { try { // Try Hyprland first @@ -67,6 +65,7 @@ Singleton { return } } catch (e) { + // Hyprland not available } @@ -98,13 +97,13 @@ Singleton { } } - function setupHyprlandConnections() { - // Connections are set up at the top level, this function just marks that Hyprland is ready + function setupHyprlandConnections() {// Connections are set up at the top level, this function just marks that Hyprland is ready } function updateHyprlandWorkspaces() { - if (!isHyprland) return - + if (!isHyprland) + return + workspaces.clear() try { const hlWorkspaces = Hyprland.workspaces.values @@ -113,14 +112,14 @@ Singleton { // Only append workspaces with id >= 1 if (ws.id >= 1) { workspaces.append({ - "id": i, - "idx": ws.id, - "name": ws.name || "", - "output": ws.monitor?.name || "", - "isActive": ws.active === true, - "isFocused": ws.focused === true, - "isUrgent": ws.urgent === true - }) + "id": i, + "idx": ws.id, + "name": ws.name || "", + "output": ws.monitor?.name || "", + "isActive": ws.active === true, + "isFocused": ws.focused === true, + "isUrgent": ws.urgent === true + }) } } } catch (e) { @@ -145,15 +144,17 @@ Singleton { } function updateNiriWorkspaces() { - if (!isNiri) return - + if (!isNiri) + return + // Get workspaces from the Niri process niriWorkspaceProcess.running = true } function updateNiriWindows() { - if (!isNiri) return - + if (!isNiri) + return + // Get windows from the Niri process niriWindowsProcess.running = true } @@ -172,23 +173,23 @@ Singleton { for (const ws of workspacesData) { workspacesList.push({ - "id": ws.id, - "idx": ws.idx, - "name": ws.name || "", - "output": ws.output || "", - "isFocused": ws.is_focused === true, - "isActive": ws.is_active === true, - "isUrgent": ws.is_urgent === true, - "isOccupied": ws.active_window_id ? true : false - }) + "id": ws.id, + "idx": ws.idx, + "name": ws.name || "", + "output": ws.output || "", + "isFocused": ws.is_focused === true, + "isActive": ws.is_active === true, + "isUrgent": ws.is_urgent === true, + "isOccupied": ws.active_window_id ? true : false + }) } workspacesList.sort((a, b) => { - if (a.output !== b.output) { - return a.output.localeCompare(b.output) - } - return a.id - b.id - }) + if (a.output !== b.output) { + return a.output.localeCompare(b.output) + } + return a.id - b.id + }) // Update the workspaces ListModel workspaces.clear() @@ -222,18 +223,18 @@ Singleton { const windowsList = [] for (const win of windowsData) { windowsList.push({ - "id": win.id, - "title": win.title || "", - "appId": win.app_id || "", - "workspaceId": win.workspace_id || null, - "isFocused": win.is_focused === true - }) + "id": win.id, + "title": win.title || "", + "appId": win.app_id || "", + "workspaceId": win.workspace_id || null, + "isFocused": win.is_focused === true + }) } windowsList.sort((a, b) => a.id - b.id) windows = windowsList windowListChanged() - + // Update focused window index for (var i = 0; i < windowsList.length; i++) { if (windowsList[i].isFocused) { @@ -292,18 +293,18 @@ Singleton { const windowsList = [] for (const win of windowsData) { windowsList.push({ - "id": win.id, - "title": win.title || "", - "appId": win.app_id || "", - "workspaceId": win.workspace_id || null, - "isFocused": win.is_focused === true - }) + "id": win.id, + "title": win.title || "", + "appId": win.app_id || "", + "workspaceId": win.workspace_id || null, + "isFocused": win.is_focused === true + }) } windowsList.sort((a, b) => a.id - b.id) windows = windowsList windowListChanged() - + // Update focused window index for (var i = 0; i < windowsList.length; i++) { if (windowsList[i].isFocused) { @@ -384,4 +385,4 @@ Singleton { } return null } -} \ No newline at end of file +} diff --git a/Services/WorkspaceService.qml b/Services/WorkspaceService.qml index f928eea..4e2d3d0 100644 --- a/Services/WorkspaceService.qml +++ b/Services/WorkspaceService.qml @@ -14,7 +14,7 @@ Singleton { property ListModel workspaces: ListModel {} property bool isHyprland: false property bool isNiri: false - + Component.onCompleted: { // Connect to CompositorService workspace changes CompositorService.workspaceChanged.connect(updateWorkspaces) diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 5717964..364af00 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -12,7 +12,9 @@ PanelWindow { property bool showOverlay: Settings.data.general.dimDesktop property int topMargin: Style.barHeight * scaling // Show dimming if this panel is opened OR if we're in a transition (to prevent flickering) - property color overlayColor: (showOverlay && (PanelService.openedPanel === root || isTransitioning)) ? Color.applyOpacity(Color.mShadow, "AA") : Color.transparent + property color overlayColor: (showOverlay && (PanelService.openedPanel === root + || isTransitioning)) ? Color.applyOpacity(Color.mShadow, + "AA") : Color.transparent property bool isTransitioning: false signal dismissed From 3e1fcb2bb1515404e8fbc2b5314b38fa21c5ff6f Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 17 Aug 2025 14:30:20 +0200 Subject: [PATCH 381/394] Update README --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 06fbd0e..4ef6b00 100644 --- a/README.md +++ b/README.md @@ -7,14 +7,14 @@ **_quiet by design_**

- - Last commit + + Last commit - - GitHub stars + + GitHub stars - - GitHub contributors + + GitHub contributors Discord From 68ee123f004a8a507153aef600aba148bdf394bc Mon Sep 17 00:00:00 2001 From: Lysec <52084453+Ly-sec@users.noreply.github.com> Date: Sun, 17 Aug 2025 14:31:18 +0200 Subject: [PATCH 382/394] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4ef6b00..044801c 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ A sleek, minimal, and thoughtfully crafted desktop shell for Wayland using **Qui yay -S quickshell-git # Download and install Noctalia -mkdir -p ~/.config/quickshell && curl -sL https://github.com/Ly-sec/Noctalia/releases/latest/download/noctalia-latest.tar.gz | tar -xz --strip-components=1 -C ~/.config/quickshell/ +mkdir -p ~/.config/quickshell && curl -sL https://github.com/noctalia-dev/noctalia-shell/releases/latest/download/noctalia-shell-latest.tar.gz | tar -xz --strip-components=1 -C ~/.config/quickshell/ ``` ### Usage From 48d218b49e17bae8d535fbdaa9261b5a16d3ed3b Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 17 Aug 2025 14:46:57 +0200 Subject: [PATCH 383/394] Fix AppLauncher color & alignment --- Modules/AppLauncher/AppLauncher.qml | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Modules/AppLauncher/AppLauncher.qml b/Modules/AppLauncher/AppLauncher.qml index 4d25076..ed12ac0 100644 --- a/Modules/AppLauncher/AppLauncher.qml +++ b/Modules/AppLauncher/AppLauncher.qml @@ -288,26 +288,31 @@ NLoader { 1, searchInput.activeFocus ? Style.borderMedium * scaling : Style.borderThin * scaling) - Row { + Item { anchors.fill: parent anchors.margins: Style.marginMedium * scaling - spacing: Style.marginSmall * scaling Text { + id: searchIcon text: "search" font.family: "Material Symbols Outlined" font.pointSize: Style.fontSizeLarger * scaling color: searchInput.activeFocus ? Color.mPrimary : Color.mOnSurface + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter } TextField { id: searchInput - placeholderText: "Search applications..." + placeholderText: searchText === "" ? "Search applications... (use > to view commands)" : "Search applications..." color: Color.mOnSurface - placeholderTextColor: Color.mOnSurface + placeholderTextColor: Color.mOnSurfaceVariant background: null font.pointSize: Style.fontSizeLarge * scaling - Layout.fillWidth: true + anchors.left: searchIcon.right + anchors.leftMargin: Style.marginSmall * scaling + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter onTextChanged: { searchText = text selectedIndex = 0 // Reset selection when search changes @@ -473,7 +478,7 @@ NLoader { text: modelData.name || "Unknown" font.pointSize: Style.fontSizeLarge * scaling font.weight: Font.Bold - color: Color.mOnSurface + color: (appCardArea.containsMouse || isSelected) ? Color.mOnPrimary : Color.mOnSurface elide: Text.ElideRight Layout.fillWidth: true } @@ -481,7 +486,7 @@ NLoader { NText { text: modelData.isCalculator ? (modelData.expr + " = " + modelData.result) : modelData.isClipboard ? modelData.content : modelData.isCommand ? modelData.content : (modelData.genericName || modelData.comment || "") font.pointSize: Style.fontSizeMedium * scaling - color: (appCardArea.containsMouse || isSelected) ? Color.mOnSurface : Color.mOnSurface + color: (appCardArea.containsMouse || isSelected) ? Color.mOnPrimary : Color.mOnSurface elide: Text.ElideRight Layout.fillWidth: true visible: text !== "" From 7a0f05cd212b6eabf3e3a55c8461af2fe96f698a Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 17 Aug 2025 14:57:27 +0200 Subject: [PATCH 384/394] Add release workflow --- .github/workflows/release.yml | 38 +++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1367300 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,38 @@ +name: Build and Release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Create release archive + run: | + mkdir -p ../noctalia-release + # Copy all files except .git and .github + rsync -av --exclude='.git' --exclude='.github' ./ ../noctalia-release/ + cd .. + tar -czf noctalia-${{ github.ref_name }}.tar.gz noctalia-release/ + cp noctalia-${{ github.ref_name }}.tar.gz noctalia-latest.tar.gz + mv *.tar.gz ${{ github.workspace }}/ + + - name: Create GitHub Release + uses: softprops/action-gh-release@v1 + with: + files: | + noctalia-${{ github.ref_name }}.tar.gz + noctalia-latest.tar.gz + generate_release_notes: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 4c09501b79f33e00d566576af6337210d49ac1c2 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 17 Aug 2025 08:57:37 -0400 Subject: [PATCH 385/394] No need for pragma ComponentBehavior --- Services/CompositorService.qml | 2 -- Services/WorkspaceService.qml | 2 -- 2 files changed, 4 deletions(-) diff --git a/Services/CompositorService.qml b/Services/CompositorService.qml index 745ed23..b9bd2ab 100644 --- a/Services/CompositorService.qml +++ b/Services/CompositorService.qml @@ -1,7 +1,5 @@ pragma Singleton -pragma ComponentBehavior - import QtQuick import Quickshell import Quickshell.Io diff --git a/Services/WorkspaceService.qml b/Services/WorkspaceService.qml index 4e2d3d0..e4c24be 100644 --- a/Services/WorkspaceService.qml +++ b/Services/WorkspaceService.qml @@ -1,7 +1,5 @@ pragma Singleton -pragma ComponentBehavior - import QtQuick import Quickshell import qs.Commons From 5953ba84fceee8d7635bf84a7896d22bd1c2d9e5 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 17 Aug 2025 08:58:22 -0400 Subject: [PATCH 386/394] No need for pragma ComponentBehavior --- Services/BrightnessService.qml | 2 -- 1 file changed, 2 deletions(-) diff --git a/Services/BrightnessService.qml b/Services/BrightnessService.qml index f75404e..0fd59f4 100644 --- a/Services/BrightnessService.qml +++ b/Services/BrightnessService.qml @@ -1,7 +1,5 @@ pragma Singleton -pragma ComponentBehavior - import QtQuick import Quickshell import Quickshell.Io From a0cbc789237e8a9d0de7fa73fa7060711e8396ea Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 17 Aug 2025 09:09:12 -0400 Subject: [PATCH 387/394] BT added log --- Services/BluetoothService.qml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Services/BluetoothService.qml b/Services/BluetoothService.qml index 5baa609..18f2794 100644 --- a/Services/BluetoothService.qml +++ b/Services/BluetoothService.qml @@ -102,6 +102,8 @@ Singleton { return } + Logger.log("Bluetooth", "refreshDevices") + // Remove duplicate check since we already did it above const connected = [] const paired = [] From 6b8e278e691ce17a19c8c157d8c1bb0298ea53c4 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 17 Aug 2025 15:28:09 +0200 Subject: [PATCH 388/394] Edit release workflow --- .github/workflows/release.yml | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1367300..094beb6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,19 +20,44 @@ jobs: - name: Create release archive run: | mkdir -p ../noctalia-release - # Copy all files except .git and .github rsync -av --exclude='.git' --exclude='.github' ./ ../noctalia-release/ cd .. tar -czf noctalia-${{ github.ref_name }}.tar.gz noctalia-release/ cp noctalia-${{ github.ref_name }}.tar.gz noctalia-latest.tar.gz mv *.tar.gz ${{ github.workspace }}/ + - name: Generate release notes + id: release_notes + run: | + PREV_TAG=$(git describe --tags --abbrev=0 @^ 2>/dev/null || echo "") + RANGE="${PREV_TAG}..HEAD" + + PR_NOTES="" + COMMIT_NOTES="" + + git log $RANGE --pretty=format:"%H|%s|%an" | while IFS='|' read -r SHA MSG AUTHOR; do + SHORT_MSG=$(echo "$MSG" | cut -c1-80) + if [[ "$MSG" =~ Merge\ pull\ request\ \#([0-9]+) ]]; then + PR_NUM="${BASH_REMATCH[1]}" + PR_TITLE=$(echo "$SHORT_MSG" | sed -E "s/Merge pull request #$PR_NUM //") + PR_NOTES+="- [PR #$PR_NUM](https://github.com/${GITHUB_REPOSITORY}/pull/$PR_NUM): $PR_TITLE by $AUTHOR\n" + else + COMMIT_NOTES+="- [$SHA](https://github.com/${GITHUB_REPOSITORY}/commit/$SHA): $SHORT_MSG by $AUTHOR\n" + fi + done + + NOTES="### Merged PRs\n$PR_NOTES\n### Direct commits\n$COMMIT_NOTES" + + echo "RELEASE_NOTES<> $GITHUB_ENV + echo -e "$NOTES" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + - name: Create GitHub Release uses: softprops/action-gh-release@v1 with: files: | noctalia-${{ github.ref_name }}.tar.gz noctalia-latest.tar.gz - generate_release_notes: true + body: ${{ env.RELEASE_NOTES }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 654be79ccd96ab7e9831acc9cad81aefac211469 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 17 Aug 2025 15:29:58 +0200 Subject: [PATCH 389/394] Small text edits in BarTab --- Modules/SettingsPanel/Tabs/BarTab.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/SettingsPanel/Tabs/BarTab.qml b/Modules/SettingsPanel/Tabs/BarTab.qml index 71f9f64..9dce857 100644 --- a/Modules/SettingsPanel/Tabs/BarTab.qml +++ b/Modules/SettingsPanel/Tabs/BarTab.qml @@ -42,7 +42,7 @@ ColumnLayout { NToggle { label: "Show Active Window" - description: "Display the title of the currently focused window below the bar" + description: "Display the title of the currently focused window on the left side of the bar" checked: Settings.data.bar.showActiveWindow onToggled: checked => { Settings.data.bar.showActiveWindow = checked From ce15aa14fd40e68fe713e56f19ae963808627afb Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 17 Aug 2025 15:30:57 +0200 Subject: [PATCH 390/394] Update GithubService URLs --- Services/GitHubService.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Services/GitHubService.qml b/Services/GitHubService.qml index d0159f7..522ca80 100644 --- a/Services/GitHubService.qml +++ b/Services/GitHubService.qml @@ -107,7 +107,7 @@ Singleton { Process { id: versionProcess - command: ["curl", "-s", "https://api.github.com/repos/Ly-sec/Noctalia/releases/latest"] + command: ["curl", "-s", "https://api.github.com/repos/noctalia-dev/noctalia-shell/releases/latest"] stdout: StdioCollector { onStreamFinished: { @@ -139,7 +139,7 @@ Singleton { Process { id: contributorsProcess - command: ["curl", "-s", "https://api.github.com/repos/Ly-sec/Noctalia/contributors?per_page=100"] + command: ["curl", "-s", "https://api.github.com/repos/noctalia-dev/noctalia-shell/contributors?per_page=100"] stdout: StdioCollector { onStreamFinished: { From a390af2aaeb091050c7dbb2c9983cf2ebe2250b6 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 17 Aug 2025 10:42:54 -0400 Subject: [PATCH 391/394] formatting --- Modules/AppLauncher/AppLauncher.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/AppLauncher/AppLauncher.qml b/Modules/AppLauncher/AppLauncher.qml index ed12ac0..d296f62 100644 --- a/Modules/AppLauncher/AppLauncher.qml +++ b/Modules/AppLauncher/AppLauncher.qml @@ -304,7 +304,8 @@ NLoader { TextField { id: searchInput - placeholderText: searchText === "" ? "Search applications... (use > to view commands)" : "Search applications..." + placeholderText: searchText + === "" ? "Search applications... (use > to view commands)" : "Search applications..." color: Color.mOnSurface placeholderTextColor: Color.mOnSurfaceVariant background: null From d2acdd1c19ef7e9f5a52f358dbe26ec1bbee9b47 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 17 Aug 2025 10:43:17 -0400 Subject: [PATCH 392/394] Bluetooth WIP but way more reliable than before --- Modules/Bar/Bluetooth.qml | 11 +- Modules/Bar/BluetoothMenu.qml | 463 ++++++++++++++++++-------------- Services/BluetoothService.qml | 490 +++++++--------------------------- 3 files changed, 371 insertions(+), 593 deletions(-) diff --git a/Modules/Bar/Bluetooth.qml b/Modules/Bar/Bluetooth.qml index c8792d9..6536a82 100644 --- a/Modules/Bar/Bluetooth.qml +++ b/Modules/Bar/Bluetooth.qml @@ -15,18 +15,11 @@ NIconButton { showBorder: false visible: bluetoothEnabled - Component.onCompleted: { - Logger.log("Bluetooth", "Component loaded, bluetoothEnabled:", bluetoothEnabled) - Logger.log("Bluetooth", "BluetoothService available:", typeof BluetoothService !== 'undefined') - if (typeof BluetoothService !== 'undefined') { - Logger.log("Bluetooth", "Connected devices:", BluetoothService.connectedDevices.length) - } - } icon: { // Show different icons based on connection status - if (BluetoothService.connectedDevices.length > 0) { + if (BluetoothService.pairedDevices.length > 0) { return "bluetooth_connected" - } else if (BluetoothService.isDiscovering) { + } else if (BluetoothService.discovering) { return "bluetooth_searching" } else { return "bluetooth" diff --git a/Modules/Bar/BluetoothMenu.qml b/Modules/Bar/BluetoothMenu.qml index 9153de8..6da9e29 100644 --- a/Modules/Bar/BluetoothMenu.qml +++ b/Modules/Bar/BluetoothMenu.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Layouts import QtQuick.Controls import Quickshell +import Quickshell.Bluetooth import Quickshell.Wayland import qs.Commons import qs.Services @@ -38,7 +39,7 @@ NLoader { onVisibleChanged: { if (visible && Settings.data.network.bluetoothEnabled) { // Always refresh devices when menu opens to get fresh device objects - BluetoothService.refreshDevices() + BluetoothService.adapter.discovering = true } else if (bluetoothMenuRect.opacityValue > 0) { // Start hide animation bluetoothMenuRect.scaleValue = 0.8 @@ -63,11 +64,14 @@ NLoader { Rectangle { id: bluetoothMenuRect + + property var deviceData: null + color: Color.mSurface radius: Style.radiusLarge * scaling border.color: Color.mOutlineVariant border.width: Math.max(1, Style.borderThin * scaling) - width: 340 * scaling + width: 400 * scaling height: 500 * scaling anchors.top: parent.top anchors.right: parent.right @@ -81,6 +85,11 @@ NLoader { scale: scaleValue opacity: opacityValue + // Prevent closing the window if clicking inside it + MouseArea { + anchors.fill: parent + } + // Animate in when component is completed Component.onCompleted: { scaleValue = 1.0 @@ -107,6 +116,7 @@ NLoader { anchors.margins: Style.marginLarge * scaling spacing: Style.marginMedium * scaling + // HEADER RowLayout { Layout.fillWidth: true spacing: Style.marginMedium * scaling @@ -121,18 +131,19 @@ NLoader { NText { text: "Bluetooth" font.pointSize: Style.fontSizeLarge * scaling - font.bold: true + font.weight: Style.fontWeightBold color: Color.mOnSurface Layout.fillWidth: true } NIconButton { - icon: "refresh" + icon: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop_circle" : "refresh" tooltipText: "Refresh Devices" sizeMultiplier: 0.8 - enabled: Settings.data.network.bluetoothEnabled && !BluetoothService.isDiscovering onClicked: { - BluetoothService.refreshDevices() + if (BluetoothService.adapter) { + BluetoothService.adapter.discovering = !BluetoothService.adapter.discovering + } } } @@ -148,247 +159,311 @@ NLoader { NDivider {} - Item { - Layout.fillWidth: true - Layout.fillHeight: true + // Available devices + Column { + id: column - // Loading indicator - ColumnLayout { - anchors.centerIn: parent - visible: Settings.data.network.bluetoothEnabled && BluetoothService.isDiscovering - spacing: Style.marginMedium * scaling + width: parent.width + spacing: Style.marginMedium * scaling + visible: BluetoothService.adapter && BluetoothService.adapter.enabled + - NBusyIndicator { - running: BluetoothService.isDiscovering - color: Color.mPrimary - size: Style.baseWidgetSize * scaling - Layout.alignment: Qt.AlignHCenter - } - - NText { - text: "Scanning for devices..." - font.pointSize: Style.fontSizeNormal * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter - } - } - - // Bluetooth disabled message - ColumnLayout { - anchors.centerIn: parent - visible: !Settings.data.network.bluetoothEnabled + RowLayout { + width: parent.width spacing: Style.marginMedium * scaling NText { - text: "bluetooth_disabled" - font.family: "Material Symbols Outlined" - font.pointSize: Style.fontSizeXXL * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter - } - - NText { - text: "Bluetooth is disabled" + text: "Available Devices" font.pointSize: Style.fontSizeLarge * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter - } - - NText { - text: "Enable Bluetooth to see available devices" - font.pointSize: Style.fontSizeNormal * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter + color: Color.mOnSurface + font.weight: Style.fontWeightMedium + anchors.verticalCenter: parent.verticalCenter } } - // Device list - ListView { - id: deviceList - anchors.fill: parent - visible: Settings.data.network.bluetoothEnabled && !BluetoothService.isDiscovering - model: [] - spacing: Style.marginMedium * scaling - clip: true + Repeater { + model: { + if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices) + return [] - // Combine all devices into a single list for the ListView - property var allDevices: { - const devices = [] - - // Add connected devices first - for (const device of BluetoothService.connectedDevices) { - devices.push({ - "device": device, - "type": 'connected', - "section": 'Connected Devices' - }) - } - - // Add paired devices - for (const device of BluetoothService.pairedDevices) { - devices.push({ - "device": device, - "type": 'paired', - "section": 'Paired Devices' - }) - } - - // Add available devices - for (const device of BluetoothService.availableDevices) { - devices.push({ - "device": device, - "type": 'available', - "section": 'Available Devices' - }) - } - - return devices + var filtered = Bluetooth.devices.values.filter(dev => { + return dev && !dev.paired && !dev.pairing + && !dev.blocked && (dev.signalStrength === undefined + || dev.signalStrength > 0) + }) + return BluetoothService.sortDevices(filtered) } - // Update model when devices change - onAllDevicesChanged: { - deviceList.model = allDevices - } + Rectangle { + property bool canConnect: BluetoothService.canConnect(modelData) + property bool isBusy: BluetoothService.isDeviceBusy(modelData) - // Also watch for changes in the service arrays - Connections { - target: BluetoothService - function onConnectedDevicesChanged() { - deviceList.model = deviceList.allDevices + width: parent.width + height: 70 + radius: Style.radiusMedium * scaling + color: { + if (availableDeviceArea.containsMouse && !isBusy) + return Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.08) + + if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) + return Qt.rgba(Color.mError.r, Color.mError.g, Color.mError.b, 0.12) + + if (modelData.blocked) + return Color.mError + + return Color.mSurfaceVariant } - function onPairedDevicesChanged() { - deviceList.model = deviceList.allDevices + border.color: { + if (modelData.pairing) + return Color.mError + + if (modelData.blocked) + return Color.mError + + return Color.mOutline } - function onAvailableDevicesChanged() { - deviceList.model = deviceList.allDevices - } - } + border.width: 1 - delegate: Item { - width: parent ? parent.width : 0 - height: Style.baseWidgetSize * 1.5 * scaling + Row { + anchors.left: parent.left + anchors.leftMargin: Style.marginMedium * scaling + anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginSmall * scaling - Rectangle { - anchors.fill: parent - radius: Style.radiusMedium * scaling - color: modelData.device.connected ? Color.mPrimary : (deviceMouseArea.containsMouse ? Color.mTertiary : Color.transparent) + NText { + text: BluetoothService.getDeviceIcon(modelData) + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXL * scaling + color: { + if (modelData.pairing) + return Color.mError - RowLayout { - anchors.fill: parent - anchors.margins: Style.marginSmall * scaling - spacing: Style.marginSmall * scaling + if (modelData.blocked) + return Color.mError + + return Color.mOnSurface + } + anchors.verticalCenter: parent.verticalCenter + } + + Column { + spacing: 2 + anchors.verticalCenter: parent.verticalCenter NText { - text: BluetoothService.getDeviceIcon(modelData.device) - font.family: "Material Symbols Outlined" - font.pointSize: Style.fontSizeXL * scaling - color: modelData.device.connected ? Color.mSurface : (deviceMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface) + text: modelData.name || modelData.deviceName + font.pointSize: Style.fonttSizeMedium * scaling + color: { + if (modelData.pairing) + return Color.mError + + if (modelData.blocked) + return Color.mError + + return Color.mOnSurface + } + font.weight: modelData.pairing ? Style.fontWeightMedium : Font.Normal } - ColumnLayout { - Layout.fillWidth: true + Row { spacing: Style.marginTiny * scaling - NText { - text: modelData.device.name || modelData.device.deviceName || "Unknown Device" - font.pointSize: Style.fontSizeNormal * scaling - elide: Text.ElideRight - Layout.fillWidth: true - color: modelData.device.connected ? Color.mSurface : (deviceMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface) - } + Row { + spacing: Style.marginSmall * spacing - NText { - text: { - if (modelData.device.connected) { - return "Connected" - } else if (modelData.device.paired) { - return "Paired" - } else { - return "Available" + NText { + text: { + if (modelData.pairing) + return "Pairing..." + + if (modelData.blocked) + return "Blocked" + + return BluetoothService.getSignalStrength(modelData) + } + font.pointSize: Style.fontSizeSmall * scaling + color: { + if (modelData.pairing) + return Color.mError + + if (modelData.blocked) + return Theme.error + + return Qt.rgba(Color.mOnSurface.r, Color.mOnSurface.g, Color.mOnSurface.b, 0.7) } } - font.pointSize: Style.fontSizeSmall * scaling - color: modelData.device.connected ? Color.mSurface : (deviceMouseArea.containsMouse ? Color.mSurface : Color.mOnSurfaceVariant) - } - NText { - text: BluetoothService.getBatteryText(modelData.device) - font.pointSize: Style.fontSizeSmall * scaling - color: modelData.device.connected ? Color.mSurface : (deviceMouseArea.containsMouse ? Color.mSurface : Color.mOnSurfaceVariant) - visible: modelData.device.batteryAvailable + NText { + text: BluetoothService.getSignalIcon(modelData) + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeSmall * scaling + color: Qt.rgba(Color.mOnSurface.r, Color.mOnSurface.g, Color.mOnSurface.b, 0.7) + visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0 + && !modelData.pairing && !modelData.blocked + } + + NText { + text: (modelData.signalStrength !== undefined + && modelData.signalStrength > 0) ? modelData.signalStrength + "%" : "" + font.pointSize: Style.fontSizeSmall * scaling + color: Qt.rgba(Color.mOnSurface.r, Color.mOnSurface.g, Color.mOnSurface.b, 0.5) + visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0 + && !modelData.pairing && !modelData.blocked + } } } + } + } - Item { - Layout.preferredWidth: Style.baseWidgetSize * 0.7 * scaling - Layout.preferredHeight: Style.baseWidgetSize * 0.7 * scaling - visible: modelData.device.pairing || modelData.device.state === 2 // Connecting state + Rectangle { + width: 80 + height: 28 + radius: Style.radiusMedium * scaling + anchors.right: parent.right + anchors.rightMargin: Style.marginMedium * scaling + anchors.verticalCenter: parent.verticalCenter + visible: modelData.state !== BluetoothDeviceState.Connecting + color: { + if (!canConnect && !isBusy) + return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) - NBusyIndicator { - visible: modelData.device.pairing || modelData.device.state === 2 - running: modelData.device.pairing || modelData.device.state === 2 - color: Color.mPrimary - anchors.centerIn: parent - size: Style.baseWidgetSize * 0.7 * scaling - } - } - - NText { - visible: modelData.device.connected - text: "connected" - font.pointSize: Style.fontSizeSmall * scaling - color: modelData.device.connected ? Color.mSurface : (deviceMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface) + if (actionButtonArea.containsMouse && !isBusy) + return Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.12) + + return "transparent" + } + border.color: canConnect || isBusy ? Color.mPrimary : Qt.rgba(Theme.outline.r, Theme.outline.g, + Theme.outline.b, 0.2) + border.width: 1 + opacity: canConnect || isBusy ? 1 : 0.5 + + NText { + anchors.centerIn: parent + text: { + if (modelData.pairing) + return "Pairing..." + + if (modelData.blocked) + return "Blocked" + + return "Connect" } + font.pointSize: Style.fontSizeSmall * scaling + color: canConnect || isBusy ? Color.mPrimary : Qt.rgba(Color.mOnSurface.r, Color.mOnSurface.g, + Color.mOnSurface.b, 0.5) + font.weight: Style.fontWeightMedium } MouseArea { - id: deviceMouseArea + id: actionButtonArea + anchors.fill: parent hoverEnabled: true + cursorShape: canConnect + && !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor) + enabled: canConnect && !isBusy onClicked: { - if (modelData.device.connected) { - BluetoothService.disconnectDevice(modelData.device) - } else if (modelData.device.paired) { - BluetoothService.connectDevice(modelData.device) - } else { - BluetoothService.pairDevice(modelData.device) - } + if (modelData) + BluetoothService.connectDeviceWithTrust(modelData) } } } + + MouseArea { + id: availableDeviceArea + + anchors.fill: parent + anchors.rightMargin: 90 + hoverEnabled: true + cursorShape: canConnect && !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor) + enabled: canConnect && !isBusy + onClicked: { + if (modelData) + BluetoothService.connectDeviceWithTrust(modelData) + } + } } } - // Empty state when no devices found - ColumnLayout { - anchors.centerIn: parent - visible: Settings.data.network.bluetoothEnabled && !BluetoothService.isDiscovering - && deviceList.count === 0 + Column { + width: parent.width spacing: Style.marginMedium * scaling + visible: { + if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices) + return false - NText { - text: "bluetooth_disabled" - font.family: "Material Symbols Outlined" - font.pointSize: Style.fontSizeXXL * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter + 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 + } + + Row { + anchors.horizontalCenter: parent.horizontalCenter + spacing: Style.marginMedium * scaling + + NText { + text: "sync" + font.family: "Material Symbols Outlined" + font.pointSize: 32 * scaling + color: Color.mPrimary + anchors.verticalCenter: parent.verticalCenter + + RotationAnimation on rotation { + running: true + loops: Animation.Infinite + from: 0 + to: 360 + duration: 2000 + } + } + + NText { + text: "Scanning for devices..." + font.pointSize: Style.fontSizeLarge * scaling + color: Color.mOnSurface + font.weight: Style.fontWeightMedium + anchors.verticalCenter: parent.verticalCenter + } } NText { - text: "No Bluetooth devices" - font.pointSize: Style.fontSizeLarge * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter - } - - NText { - text: "Click the refresh button to discover devices" - font.pointSize: Style.fontSizeNormal * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter + text: "Make sure your device is in pairing mode" + font.pointSize: Style.fontSizeMedium * scaling + color: Qt.rgba(Color.mOnSurface.r, Color.mOnSurface.g, Color.mOnSurface.b, 0.7) + anchors.horizontalCenter: parent.horizontalCenter } } + + NText { + text: "No devices found. Put your device in pairing mode and click Start Scanning." + font.pointSize: Style.fontSizeMedium * scaling + color: Qt.rgba(Color.mOnSurface.r, Color.mOnSurface.g, Color.mOnSurface.b, 0.7) + 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 + } } + + // This item takes up all the remaining vertical space. + Item { + Layout.fillHeight: true + } } } } diff --git a/Services/BluetoothService.qml b/Services/BluetoothService.qml index 18f2794..03c3a2a 100644 --- a/Services/BluetoothService.qml +++ b/Services/BluetoothService.qml @@ -3,432 +3,142 @@ pragma Singleton import QtQuick import Quickshell import Quickshell.Bluetooth -import qs.Commons Singleton { id: root - // Bluetooth state properties - property bool isEnabled: Settings.data.network.bluetoothEnabled - property bool isDiscovering: false - property var connectedDevices: [] - property var pairedDevices: [] - property var availableDevices: [] - property string lastConnectedDevice: "" - property string connectStatus: "" - property string connectStatusDevice: "" - property string connectError: "" + readonly property BluetoothAdapter adapter: Bluetooth.defaultAdapter + readonly property bool available: adapter !== null + readonly property bool enabled: (adapter && adapter.enabled) ?? false + 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 [] - // Timer for refreshing device lists - property Timer refreshTimer: Timer { - interval: 5000 // Refresh every 5 seconds when discovery is active - repeat: true - running: root.isEnabled && Bluetooth.defaultAdapter && Bluetooth.defaultAdapter.discovering - onTriggered: root.refreshDevices() + return adapter.devices.values.filter(dev => { + return dev && (dev.paired || dev.trusted) + }) + } + readonly property var allDevicesWithBattery: { + if (!adapter || !adapter.devices) + return [] + + return adapter.devices.values.filter(dev => { + return dev && dev.batteryAvailable && dev.battery > 0 + }) } - Component.onCompleted: { - Logger.log("Bluetooth", "Service started") + function sortDevices(devices) { + return devices.sort((a, b) => { + var aName = a.name || a.deviceName || "" + var bName = b.name || b.deviceName || "" - if (isEnabled && Bluetooth.defaultAdapter) { - // Ensure adapter is enabled - if (!Bluetooth.defaultAdapter.enabled) { - Bluetooth.defaultAdapter.enabled = true - } + var aHasRealName = aName.includes(" ") && aName.length > 3 + var bHasRealName = bName.includes(" ") && bName.length > 3 - // Start discovery to find devices - if (!Bluetooth.defaultAdapter.discovering) { - Bluetooth.defaultAdapter.discovering = true - } + if (aHasRealName && !bHasRealName) + return -1 + if (!aHasRealName && bHasRealName) + return 1 - // Refresh devices after a short delay to allow discovery to start - Qt.callLater(function () { - refreshDevices() - }) - } + var aSignal = (a.signalStrength !== undefined && a.signalStrength > 0) ? a.signalStrength : 0 + var bSignal = (b.signalStrength !== undefined && b.signalStrength > 0) ? b.signalStrength : 0 + return bSignal - aSignal + }) } - // Function to enable/disable Bluetooth - function setBluetoothEnabled(enabled) { - - if (enabled) { - // Store the currently connected devices before enabling - for (const device of connectedDevices) { - if (device.connected) { - lastConnectedDevice = device.name || device.deviceName - break - } - } - - // Enable Bluetooth - if (Bluetooth.defaultAdapter) { - Bluetooth.defaultAdapter.enabled = true - - // Start discovery to find devices - if (!Bluetooth.defaultAdapter.discovering) { - Bluetooth.defaultAdapter.discovering = true - } - - // Refresh devices after enabling - Qt.callLater(refreshDevices) - } else { - Logger.warn("Bluetooth", "No adapter found") - } - } else { - // Disconnect from current devices before disabling - for (const device of connectedDevices) { - if (device.connected) { - device.disconnect() - } - } - - // Disable Bluetooth - if (Bluetooth.defaultAdapter) { - Logger.log("Bluetooth", "Disabling adapter") - Bluetooth.defaultAdapter.enabled = false - } - } - - Settings.data.network.bluetoothEnabled = enabled - isEnabled = enabled - } - - // Function to refresh device lists - function refreshDevices() { - if (!isEnabled || !Bluetooth.defaultAdapter) { - connectedDevices = [] - pairedDevices = [] - availableDevices = [] - return - } - - Logger.log("Bluetooth", "refreshDevices") - - // Remove duplicate check since we already did it above - const connected = [] - const paired = [] - const available = [] - - let devices = null - - // Try adapter devices first - if (Bluetooth.defaultAdapter.enabled && Bluetooth.defaultAdapter.devices) { - devices = Bluetooth.defaultAdapter.devices - } - - // Fallback to global devices list - if (!devices && Bluetooth.devices) { - devices = Bluetooth.devices - } - - if (!devices) { - connectedDevices = [] - pairedDevices = [] - availableDevices = [] - return - } - - // Use Qt model methods to iterate through the ObjectModel - let deviceFound = false - - try { - // Get the row count using the Qt model method - const rowCount = devices.rowCount() - - if (rowCount > 0) { - // Iterate through each row using the Qt model data() method - for (var i = 0; i < rowCount; i++) { - try { - // Create a model index for this row - const modelIndex = devices.index(i, 0) - if (!modelIndex.valid) - continue - - // Get the device object using the Qt.UserRole (typically 256) - const device = devices.data(modelIndex, 256) // Qt.UserRole - if (!device) { - // Try alternative role values - const deviceAlt = devices.data(modelIndex, 0) // Qt.DisplayRole - if (deviceAlt) { - device = deviceAlt - } else { - continue - } - } - - deviceFound = true - - if (device.connected) { - connected.push(device) - } else if (device.paired) { - paired.push(device) - } else { - available.push(device) - } - } catch (e) { - - // Silent error handling - } - } - } - - // Alternative method: try the values property if available - if (!deviceFound && devices.values) { - try { - const values = devices.values - if (values && typeof values === 'object') { - // Try to iterate through values if it's iterable - if (values.length !== undefined) { - for (var i = 0; i < values.length; i++) { - const device = values[i] - if (device) { - deviceFound = true - if (device.connected) { - connected.push(device) - } else if (device.paired) { - paired.push(device) - } else { - available.push(device) - } - } - } - } - } - } catch (e) { - - // Silent error handling - } - } - } catch (e) { - Logger.warn("Bluetooth", "Error accessing device model:", e) - } - - if (!deviceFound) { - Logger.log("Bluetooth", "No device found") - } - - connectedDevices = connected - pairedDevices = paired - availableDevices = available - } - - // Function to start discovery - function startDiscovery() { - if (!isEnabled || !Bluetooth.defaultAdapter) - return - - isDiscovering = true - Bluetooth.defaultAdapter.discovering = true - } - - // Function to stop discovery - function stopDiscovery() { - if (!Bluetooth.defaultAdapter) - return - - isDiscovering = false - Bluetooth.defaultAdapter.discovering = false - } - - // Function to connect to a device - function connectDevice(device) { - if (!device) - return - - // Check if device is still valid (not stale from previous session) - if (!device.connect || typeof device.connect !== 'function') { - Logger.warn("Bluetooth", "Device object is stale, refreshing devices") - refreshDevices() - return - } - - connectStatus = "connecting" - connectStatusDevice = device.name || device.deviceName - connectError = "" - - try { - device.connect() - } catch (error) { - Logger.error("Bluetooth", "Error connecting to device:", error) - connectStatus = "error" - connectError = error.toString() - Qt.callLater(refreshDevices) - } - } - - // Function to disconnect from a device - function disconnectDevice(device) { - if (!device) - return - - // Check if device is still valid (not stale from previous session) - if (!device.disconnect || typeof device.disconnect !== 'function') { - Logger.warn("Bluetooth", "Device object is stale, refreshing devices") - refreshDevices() - return - } - - try { - device.disconnect() - // Clear connection status - connectStatus = "" - connectStatusDevice = "" - connectError = "" - } catch (error) { - Logger.warn("Bluetooth", "Error disconnecting device:", error) - Qt.callLater(refreshDevices) - } - } - - // Function to pair with a device - function pairDevice(device) { - if (!device) - return - - // Check if device is still valid (not stale from previous session) - if (!device.pair || typeof device.pair !== 'function') { - Logger.warn("Bluetooth", "Device object is stale, refreshing devices") - refreshDevices() - return - } - - try { - device.pair() - } catch (error) { - Logger.warn("Bluetooth", "Error pairing device:", error) - Qt.callLater(refreshDevices) - } - } - - // Function to forget a device - function forgetDevice(device) { - if (!device) - return - - // Check if device is still valid (not stale from previous session) - if (!device.forget || typeof device.forget !== 'function') { - Logger.warn("Bluetooth", "Device object is stale, refreshing devices") - refreshDevices() - return - } - - // Store device info before forgetting (in case device object becomes invalid) - const deviceName = device.name || device.deviceName || "Unknown Device" - - try { - device.forget() - - // Clear any connection status that might be related to this device - if (connectStatusDevice === deviceName) { - connectStatus = "" - connectStatusDevice = "" - connectError = "" - } - - // Refresh devices after a delay to ensure the forget operation is complete - Qt.callLater(refreshDevices, 1000) - } catch (error) { - Logger.warn("Bluetooth", "Error forgetting device:", error) - Qt.callLater(refreshDevices, 500) - } - } - - // Function to get device icon function getDeviceIcon(device) { if (!device) return "bluetooth" - // Use device icon if available, otherwise fall back to device type - if (device.icon) { - return device.icon - } + 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")) + return "headset" - // Fallback icons based on common device types - const name = (device.name || device.deviceName || "").toLowerCase() - if (name.includes("headphone") || name.includes("earbud") || name.includes("airpods")) { - return "headphones" - } else if (name.includes("speaker")) { - return "speaker" - } else if (name.includes("keyboard")) { - return "keyboard" - } else if (name.includes("mouse")) { + if (icon.includes("mouse") || name.includes("mouse")) return "mouse" - } else if (name.includes("phone") || name.includes("mobile")) { + + 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")) return "smartphone" - } else if (name.includes("laptop") || name.includes("computer")) { - return "laptop" - } + + if (icon.includes("watch") || name.includes("watch")) + return "watch" + + if (icon.includes("speaker") || name.includes("speaker")) + return "speaker" + + if (icon.includes("display") || name.includes("tv")) + return "tv" return "bluetooth" } - // Function to get device status text - function getDeviceStatus(device) { + function canConnect(device) { if (!device) - return "" + return false - if (device.connected) { - return "Connected" - } else if (device.pairing) { - return "Pairing..." - } else if (device.paired) { - return "Paired" - } else { - return "Available" - } + return !device.paired && !device.pairing && !device.blocked } - // Function to get battery level text - function getBatteryText(device) { - if (!device || !device.batteryAvailable) - return "" + function getSignalStrength(device) { + if (!device || device.signalStrength === undefined || device.signalStrength <= 0) + return "Unknown" - const percentage = Math.round(device.battery * 100) - return `${percentage}%` + var signal = device.signalStrength + if (signal >= 80) + return "Excellent" + + if (signal >= 60) + return "Good" + + if (signal >= 40) + return "Fair" + + if (signal >= 20) + return "Poor" + + return "Very Poor" } - // Watch for Bluetooth adapter changes - Connections { - target: Bluetooth.defaultAdapter - ignoreUnknownSignals: true + function getSignalIcon(device) { + if (!device || device.signalStrength === undefined || device.signalStrength <= 0) + return "signal_cellular_null" - function onEnabledChanged() { - root.isEnabled = Bluetooth.defaultAdapter.enabled - Settings.data.network.bluetoothEnabled = root.isEnabled - if (root.isEnabled) { - Qt.callLater(refreshDevices) - } else { - connectedDevices = [] - pairedDevices = [] - availableDevices = [] - } - } + var signal = device.signalStrength + if (signal >= 80) + return "signal_cellular_4_bar" - function onDiscoveringChanged() { - root.isDiscovering = Bluetooth.defaultAdapter.discovering - if (Bluetooth.defaultAdapter.discovering) { - Qt.callLater(refreshDevices) - } - } + if (signal >= 60) + return "signal_cellular_3_bar" - function onStateChanged() { - if (Bluetooth.defaultAdapter.state >= 4) { - Qt.callLater(refreshDevices) - } - } + if (signal >= 40) + return "signal_cellular_2_bar" - function onDevicesChanged() { - Qt.callLater(refreshDevices) - } + if (signal >= 20) + return "signal_cellular_1_bar" + + return "signal_cellular_0_bar" } - // Watch for global device changes - Connections { - target: Bluetooth - ignoreUnknownSignals: true + function isDeviceBusy(device) { + if (!device) + return false + return device.pairing || device.state === BluetoothDeviceState.Disconnecting + || device.state === BluetoothDeviceState.Connecting + } - function onDevicesChanged() { - Qt.callLater(refreshDevices) - } + function connectDeviceWithTrust(device) { + if (!device) + return + + device.trusted = true + device.connect() } } From 32b6a363af7f94884ccd0659e2345c4da2cf88eb Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 17 Aug 2025 11:34:51 -0400 Subject: [PATCH 393/394] BT Menu --- Modules/Bar/BluetoothMenu.qml | 527 ++++++++++++++++++---------------- 1 file changed, 276 insertions(+), 251 deletions(-) diff --git a/Modules/Bar/BluetoothMenu.qml b/Modules/Bar/BluetoothMenu.qml index 6da9e29..c1d006a 100644 --- a/Modules/Bar/BluetoothMenu.qml +++ b/Modules/Bar/BluetoothMenu.qml @@ -71,7 +71,7 @@ NLoader { radius: Style.radiusLarge * scaling border.color: Color.mOutlineVariant border.width: Math.max(1, Style.borderThin * scaling) - width: 400 * scaling + width: 380 * scaling height: 500 * scaling anchors.top: parent.top anchors.right: parent.right @@ -159,203 +159,242 @@ NLoader { NDivider {} - // Available devices - Column { - id: column + ScrollView { + id: scrollView - width: parent.width - spacing: Style.marginMedium * scaling - visible: BluetoothService.adapter && BluetoothService.adapter.enabled - + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + + // Available devices + Column { + id: column - RowLayout { width: parent.width spacing: Style.marginMedium * scaling + visible: BluetoothService.adapter && BluetoothService.adapter.enabled - NText { - text: "Available Devices" - font.pointSize: Style.fontSizeLarge * scaling - color: Color.mOnSurface - font.weight: Style.fontWeightMedium - anchors.verticalCenter: parent.verticalCenter - } - } - - Repeater { - model: { - if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !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 BluetoothService.sortDevices(filtered) - } - - Rectangle { - property bool canConnect: BluetoothService.canConnect(modelData) - property bool isBusy: BluetoothService.isDeviceBusy(modelData) - + RowLayout { width: parent.width - height: 70 - radius: Style.radiusMedium * scaling - color: { - if (availableDeviceArea.containsMouse && !isBusy) - return Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.08) + spacing: Style.marginMedium * scaling - if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) - return Qt.rgba(Color.mError.r, Color.mError.g, Color.mError.b, 0.12) - - if (modelData.blocked) - return Color.mError - - return Color.mSurfaceVariant + NText { + text: "Available Devices" + font.pointSize: Style.fontSizeLarge * scaling + color: Color.mOnSurface + font.weight: Style.fontWeightMedium } - border.color: { - if (modelData.pairing) - return Color.mError + } - if (modelData.blocked) - return Color.mError + Repeater { + model: { + if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices) + return [] - return Color.mOutline - } - border.width: 1 - - Row { - anchors.left: parent.left - anchors.leftMargin: Style.marginMedium * scaling - anchors.verticalCenter: parent.verticalCenter - spacing: Style.marginSmall * scaling - - NText { - text: BluetoothService.getDeviceIcon(modelData) - font.family: "Material Symbols Outlined" - font.pointSize: Style.fontSizeXL * scaling - color: { - if (modelData.pairing) - return Color.mError - - if (modelData.blocked) - return Color.mError - - return Color.mOnSurface - } - anchors.verticalCenter: parent.verticalCenter - } - - Column { - spacing: 2 - anchors.verticalCenter: parent.verticalCenter - - NText { - text: modelData.name || modelData.deviceName - font.pointSize: Style.fonttSizeMedium * scaling - color: { - if (modelData.pairing) - return Color.mError - - if (modelData.blocked) - return Color.mError - - return Color.mOnSurface - } - font.weight: modelData.pairing ? Style.fontWeightMedium : Font.Normal - } - - Row { - spacing: Style.marginTiny * scaling - - Row { - spacing: Style.marginSmall * spacing - - NText { - text: { - if (modelData.pairing) - return "Pairing..." - - if (modelData.blocked) - return "Blocked" - - return BluetoothService.getSignalStrength(modelData) - } - font.pointSize: Style.fontSizeSmall * scaling - color: { - if (modelData.pairing) - return Color.mError - - if (modelData.blocked) - return Theme.error - - return Qt.rgba(Color.mOnSurface.r, Color.mOnSurface.g, Color.mOnSurface.b, 0.7) - } - } - - NText { - text: BluetoothService.getSignalIcon(modelData) - font.family: "Material Symbols Outlined" - font.pointSize: Style.fontSizeSmall * scaling - color: Qt.rgba(Color.mOnSurface.r, Color.mOnSurface.g, Color.mOnSurface.b, 0.7) - visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0 - && !modelData.pairing && !modelData.blocked - } - - NText { - text: (modelData.signalStrength !== undefined - && modelData.signalStrength > 0) ? modelData.signalStrength + "%" : "" - font.pointSize: Style.fontSizeSmall * scaling - color: Qt.rgba(Color.mOnSurface.r, Color.mOnSurface.g, Color.mOnSurface.b, 0.5) - visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0 - && !modelData.pairing && !modelData.blocked - } - } - } - } + var filtered = Bluetooth.devices.values.filter(dev => { + return dev && !dev.paired && !dev.pairing + && !dev.blocked && (dev.signalStrength === undefined + || dev.signalStrength > 0) + }) + return BluetoothService.sortDevices(filtered) } Rectangle { - width: 80 - height: 28 + property bool canConnect: BluetoothService.canConnect(modelData) + property bool isBusy: BluetoothService.isDeviceBusy(modelData) + + width: parent.width + height: 70 radius: Style.radiusMedium * scaling - anchors.right: parent.right - anchors.rightMargin: Style.marginMedium * scaling - anchors.verticalCenter: parent.verticalCenter - visible: modelData.state !== BluetoothDeviceState.Connecting color: { - if (!canConnect && !isBusy) - return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + if (availableDeviceArea.containsMouse && !isBusy) + return Color.mTertiary - if (actionButtonArea.containsMouse && !isBusy) - return Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.12) + if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) + return Color.mPrimary - return "transparent" + if (modelData.blocked) + return Color.mError + + return Color.mSurfaceVariant } - border.color: canConnect || isBusy ? Color.mPrimary : Qt.rgba(Theme.outline.r, Theme.outline.g, - Theme.outline.b, 0.2) - border.width: 1 - opacity: canConnect || isBusy ? 1 : 0.5 + border.color: Color.mOutline + border.width: Math.max(1, Style.borderThin * scaling) - NText { - anchors.centerIn: parent - text: { - if (modelData.pairing) - return "Pairing..." + Row { + anchors.left: parent.left + anchors.leftMargin: Style.marginMedium * scaling + anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginSmall * scaling - if (modelData.blocked) - return "Blocked" + // One device BT icon + NText { + text: BluetoothService.getDeviceIcon(modelData) + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXL * scaling + color: { + if (availableDeviceArea.containsMouse) + return Color.mOnTertiary - return "Connect" + 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.marginTiniest * 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.marginTiny * scaling + + Row { + spacing: Style.marginSmall * 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.fontSizeSmall * 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 + } + } + + NText { + text: BluetoothService.getSignalIcon(modelData) + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeSmall * 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.fontSizeSmall * 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.radiusMedium * scaling + anchors.right: parent.right + anchors.rightMargin: Style.marginMedium * 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.borderThin * 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.fontSizeSmall * scaling + font.weight: Style.fontWeightMedium + color: { + if (availableDeviceArea.containsMouse) { + return Color.mOnTertiary + } else { + return Color.mPrimary + } + } } - font.pointSize: Style.fontSizeSmall * scaling - color: canConnect || isBusy ? Color.mPrimary : Qt.rgba(Color.mOnSurface.r, Color.mOnSurface.g, - Color.mOnSurface.b, 0.5) - font.weight: Style.fontWeightMedium } MouseArea { - id: actionButtonArea + id: availableDeviceArea anchors.fill: parent hoverEnabled: true @@ -368,102 +407,88 @@ NLoader { } } } - - MouseArea { - id: availableDeviceArea - - anchors.fill: parent - anchors.rightMargin: 90 - hoverEnabled: true - cursorShape: canConnect && !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor) - enabled: canConnect && !isBusy - onClicked: { - if (modelData) - BluetoothService.connectDeviceWithTrust(modelData) - } - } - } - } - - Column { - width: parent.width - spacing: Style.marginMedium * scaling - visible: { - if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices) - return false - - 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 } - Row { - anchors.horizontalCenter: parent.horizontalCenter + // Fallback if nothing available + Column { + width: parent.width spacing: Style.marginMedium * scaling + visible: { + if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices) + return false - NText { - text: "sync" - font.family: "Material Symbols Outlined" - font.pointSize: 32 * scaling - color: Color.mPrimary - anchors.verticalCenter: parent.verticalCenter + 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 + } - RotationAnimation on rotation { - running: true - loops: Animation.Infinite - from: 0 - to: 360 - duration: 2000 + Row { + anchors.horizontalCenter: parent.horizontalCenter + spacing: Style.marginMedium * scaling + + NText { + text: "sync" + font.family: "Material Symbols Outlined" + font.pointSize: Style.fontSizeXLL * 1.5 * scaling + color: Color.mPrimary + anchors.verticalCenter: parent.verticalCenter + + RotationAnimation on rotation { + running: true + loops: Animation.Infinite + from: 0 + to: 360 + duration: 2000 + } + } + + NText { + text: "Scanning for devices..." + font.pointSize: Style.fontSizeLarge * scaling + color: Color.mOnSurface + font.weight: Style.fontWeightMedium + anchors.verticalCenter: parent.verticalCenter } } NText { - text: "Scanning for devices..." - font.pointSize: Style.fontSizeLarge * scaling - color: Color.mOnSurface - font.weight: Style.fontWeightMedium - anchors.verticalCenter: parent.verticalCenter + text: "Make sure your device is in pairing mode" + font.pointSize: Style.fontSizeMedium * scaling + color: Color.mOnSurfaceVariant + anchors.horizontalCenter: parent.horizontalCenter } } NText { - text: "Make sure your device is in pairing mode" + text: "No devices found. Put your device in pairing mode and click Start Scanning." font.pointSize: Style.fontSizeMedium * scaling - color: Qt.rgba(Color.mOnSurface.r, Color.mOnSurface.g, Color.mOnSurface.b, 0.7) - anchors.horizontalCenter: parent.horizontalCenter - } - } + color: Color.mOnSurfaceVariant + visible: { + if (!BluetoothService.adapter || !Bluetooth.devices) + return true - NText { - text: "No devices found. Put your device in pairing mode and click Start Scanning." - font.pointSize: Style.fontSizeMedium * scaling - color: Qt.rgba(Color.mOnSurface.r, Color.mOnSurface.g, Color.mOnSurface.b, 0.7) - 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 + 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 } - wrapMode: Text.WordWrap - width: parent.width - horizontalAlignment: Text.AlignHCenter } } - - // This item takes up all the remaining vertical space. - Item { - Layout.fillHeight: true - } + // This item takes up all the remaining vertical space. + Item { + Layout.fillHeight: true + } } } } From 15a67ca5ccfde516b56e5dbe79d7afe7130df960 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Sun, 17 Aug 2025 11:38:11 -0400 Subject: [PATCH 394/394] Using alias rather than var for JsonAdapter --- Commons/Settings.qml | 2 +- Services/GitHubService.qml | 2 +- Services/LocationService.qml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Commons/Settings.qml b/Commons/Settings.qml index b4048a7..009cdc9 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -24,7 +24,7 @@ Singleton { property string defaultAvatar: Quickshell.env("HOME") + "/.face" // Used to access via Settings.data.xxx.yyy - property var data: adapter + property alias data: adapter // Flag to prevent unnecessary wallpaper calls during reloads property bool isInitialLoad: true diff --git a/Services/GitHubService.qml b/Services/GitHubService.qml index 522ca80..98bf6e4 100644 --- a/Services/GitHubService.qml +++ b/Services/GitHubService.qml @@ -11,8 +11,8 @@ Singleton { property string githubDataFile: Quickshell.env("NOCTALIA_GITHUB_FILE") || (Settings.cacheDir + "github.json") property int githubUpdateFrequency: 60 * 60 // 1 hour expressed in seconds - property var data: adapter // Used to access via GitHubService.data.xxx.yyy property bool isFetchingData: false + property alias data: adapter // Used to access via GitHubService.data.xxx.yyy // Public properties for easy access property string latestVersion: "Unknown" diff --git a/Services/LocationService.qml b/Services/LocationService.qml index 3a4744e..3eb96ed 100644 --- a/Services/LocationService.qml +++ b/Services/LocationService.qml @@ -11,8 +11,8 @@ Singleton { property string locationFile: Quickshell.env("NOCTALIA_WEATHER_FILE") || (Settings.cacheDir + "location.json") property int weatherUpdateFrequency: 30 * 60 // 30 minutes expressed in seconds - property var data: adapter // Used to access via LocationService.data.xxx property bool isFetchingWeather: false + property alias data: adapter // Used to access via LocationService.data.xxx FileView { path: locationFile

- - Last commit - - - GitHub stars - - - GitHub contributors - - - Discord - - - - -