Compare commits

...

253 Commits

Author SHA1 Message Date
Ly-sec 349ef85648 Release v2.14.0
This release introduces new themes, a native file picker, multi-language support, a redesigned clock/calendar widget, unified controls, and major quality-of-life improvements alongside numerous fixes and refinements—delivering a smoother and more polished experience.

- **Brand new themes:** Try the beautiful Noctalia and Aya themes for an upgraded look.
- **New file picker:** Picking files just got easier with a seamless native picker.
- **International:** Noctalia is now available in English, French, German, Spanish and Portuguese, with more languages on the way.
- **Revamped clock/calendar:** Enjoy a sleeker, more compact calendar integrated right into your bar.
- **Unified Volume & Brightness controls:** Our new On-Screen Display (OSD) feature lets you see brightness and volume adjustments in real-time, directly on your screen as you make them.
- **Pin your dock apps:** Pin favorites, group them better, and access everything with a right click.
- **Bar Widget Setting addition:** Now you can easily move widgets from one section to another.

- **ActiveWindow and MediaMini widgets:** Cleaner display, better media controls, and improved logic if nothing’s playing.
- **Notification system:** Choose where notifications appear, see progress bars, and enjoy refined layouts and scaling.
- **Workspace switching:** Switch workspaces just by scrolling - no extra clicks needed.
- **System widgets:** New monitor and side panel for greater control.
- **Bar & dock:** Faster, more reliable dragging, better icons, tooltips, and search for widgets.
- **Icons:** We have incorporated the Hyprland logo into the font as a new glyph.

- Reduced margin/alignment issues and bugs in the lock screen, notifications, and OSD.
- The volume system is now smarter and works seamlessly across sinks and sources.
- Lots of little bug fixes for panels, widgets, and popups, all aimed at a smoother experience.
2025-09-25 16:30:08 +02:00
ItsLemmy b38cf8ef66 i18n: json check script with more colors 2025-09-25 09:51:00 -04:00
ItsLemmy 23c83a49c3 i18n-es: 100% 2025-09-25 09:42:33 -04:00
ItsLemmy 1926008315 i18n-pt: 100% 2025-09-25 09:37:45 -04:00
ItsLemmy deb75f5bab i18n: json check script now support an argument to review a single language 2025-09-25 09:31:32 -04:00
ItsLemmy 53baf1c86b Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-25 09:27:59 -04:00
ItsLemmy 8173919692 i18n-fr: 100% 2025-09-25 09:27:56 -04:00
Ly-sec ece8705e5d i18n: de - remove some keys 2025-09-25 15:23:00 +02:00
ItsLemmy 346d29d94a i18n: en: no audio codecs 2025-09-25 09:19:34 -04:00
ItsLemmy a3f604efc3 en: no audio codecs translation 2025-09-25 09:14:30 -04:00
ItsLemmy 0e8a920ee2 Do not translate audio codecs name 2025-09-25 09:13:43 -04:00
ItsLemmy e98e034a68 Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-25 09:11:33 -04:00
ItsLemmy 1f3cafb1b9 i18n-json-check: report line numbers and sort by descending for easier editing. 2025-09-25 09:11:31 -04:00
Ly-sec 316cd3114a Translations/de: remove extra keys, add missing keys 2025-09-25 15:07:26 +02:00
ItsLemmy 4c951cf380 i18n-json-check script 2025-09-25 09:00:14 -04:00
ItsLemmy 0f888fd734 MediaMini: autoHide 2025-09-25 08:49:01 -04:00
ItsLemmy 0690ac4996 Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-25 08:42:11 -04:00
ItsLemmy 3809f290ed ActiveWindow: better autohide 2025-09-25 08:42:10 -04:00
Ly-sec b1094bbfa0 NDateTimeTokens: replace ListView with js array 2025-09-25 14:37:43 +02:00
Ly-sec 644e24f409 ScreenRecorder: fix recording with both audio sources 2025-09-25 13:23:48 +02:00
Ly-sec 6f2d7516f0 Revert "MediaMini: hide when no media is playing"
This reverts commit 8dad25f79c.
2025-09-25 13:10:31 +02:00
Ly-sec 8dad25f79c MediaMini: hide when no media is playing 2025-09-25 12:11:49 +02:00
ItsLemmy 36489491e4 Bar new IPC: ipc call bar toggle 2025-09-24 22:18:22 -04:00
ItsLemmy 846730361d autoformatting 2025-09-24 22:17:26 -04:00
Lemmy 428f3627b6 Merge pull request #356 from lonerOrz/fix/osd
Initialize volume silently
2025-09-24 22:05:08 -04:00
ItsLemmy 68b328c982 Better colors for mediamini 2025-09-24 21:38:45 -04:00
ItsLemmy 4dac2ffe88 Autoformatting + cleanup 2025-09-24 21:33:00 -04:00
ItsLemmy f3535f22ba ActiveWindow: hyprland fix 2025-09-24 21:22:52 -04:00
loner deca5e1235 Initialize volume silently 2025-09-25 09:22:42 +08:00
ItsLemmy 8da903bb61 Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-24 21:14:46 -04:00
ItsLemmy b58f6f0a1b ActiveWindow: improve display when no active window 2025-09-24 21:14:44 -04:00
Ly-sec 946996917d Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-25 03:10:21 +02:00
Ly-sec b03b4b0f13 i18n: fix control-center 2025-09-25 03:10:10 +02:00
Lemmy 73f76e2275 Merge pull request #357 from MrDowntempo/NoctaliaTheme
Added New Noctalia theme
2025-09-24 20:53:42 -04:00
ItsLemmy 80442e2839 Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-24 20:48:05 -04:00
ItsLemmy a8a1b0a422 ActiveWindow: similar behavior to MediaMini 2025-09-24 20:48:03 -04:00
Ly-sec 346e27830a MediaMini: small fixes 2025-09-25 02:46:31 +02:00
Ly-sec ef616efcca i18n: small fix
autoformat
2025-09-25 02:44:27 +02:00
ItsLemmy 8c1153192d MediaMini: infinite scroll 2025-09-24 20:40:11 -04:00
ItsLemmy c46a84d794 MediaMini: some more tweaks 2025-09-24 20:37:40 -04:00
ItsLemmy 46d3465b50 MediaMini: clip fix 2025-09-24 20:25:41 -04:00
Corey Woodworth 7bd278d428 Added New Noctalia theme 2025-09-24 20:19:15 -04:00
Ly-sec 2123b55aab MediaMini: small fixes 2025-09-25 01:37:17 +02:00
Ly-sec 4de6489cbf Settings: set scrollingTitle default to false 2025-09-25 01:02:26 +02:00
Ly-sec 96c2817e06 MediaMini: add scrolling support (as requested in #293) 2025-09-25 01:02:01 +02:00
Ly-sec 35a7ed165f BarSectionEditor: add search option (fixes #347) 2025-09-25 00:43:04 +02:00
Ly-sec 1c5b02fab4 Notification add ipc to clear history 2025-09-25 00:07:58 +02:00
Ly-sec 2afec4cc46 NotificationsTab: fix i18n 2025-09-25 00:01:50 +02:00
ItsLemmy 6dd6c6af74 Icons: added hyprland icons 2025-09-24 17:47:48 -04:00
ItsLemmy d86686704c Bar: slightly more compact calendar 2025-09-24 17:17:09 -04:00
ItsLemmy 22b8edb023 OSD: Single component instance. Multi monitor support (follows notifications settings) 2025-09-24 17:05:57 -04:00
ItsLemmy b96deaa0c3 Notification: simpler active loader conditions 2025-09-24 17:04:02 -04:00
ItsLemmy 0cb619a787 Workspace: slight adjustment to the inactive ws color. So it works better in every situation (with or without capsule) 2025-09-24 16:11:45 -04:00
ItsLemmy 63951ced9e Added Portuguese translation (automatically generated) 2025-09-24 14:17:28 -04:00
ItsLemmy 84502f4c9f Added Spanish translation (automatically generated) 2025-09-24 14:12:51 -04:00
ItsLemmy 430cc64fdb NHeader: fix label visibility 2025-09-24 14:12:32 -04:00
ItsLemmy b93c733e7c autoformating 2025-09-24 13:52:44 -04:00
ItsLemmy fe58e5e92a Merge branch 'i18n' 2025-09-24 13:52:29 -04:00
ItsLemmy e6ae17cdd5 Audio: Debounce timer should not use Style.animationFast 2025-09-24 13:27:10 -04:00
Lemmy b445153444 Merge pull request #352 from FUFSoB/audio-fixes
Small fixes for audio and auto-hide widgets
2025-09-24 13:23:42 -04:00
Lysec 6f85747d92 Merge pull request #353 from MrDowntempo/AyaTheme
Added Aya theme
2025-09-24 19:16:08 +02:00
Corey Woodworth 66360c2379 Added Aya theme 2025-09-24 13:14:35 -04:00
Ly-sec 7fe504aa8a Merge branch 'i18n' of https://github.com/noctalia-dev/noctalia-shell into i18n 2025-09-24 17:01:41 +02:00
Ly-sec aca831e54d i18n: remove debug language 2025-09-24 17:01:31 +02:00
ItsLemmy 7da4b1d63c i18n: no debug 2025-09-24 10:58:31 -04:00
FUFSoB f21bda0de9 other: change desc of overdrive settings toggle 2025-09-24 19:54:29 +05:00
FUFSoB 24ffedd599 bugfix: always hide display mode wasn't working 2025-09-24 19:50:10 +05:00
Ly-sec 7f9acccce7 i18n: remove some entries, edit some entries 2025-09-24 16:48:43 +02:00
ItsLemmy 084fb39abd NComboBox: simple js function 2025-09-24 10:24:45 -04:00
FUFSoB 06694f2428 bugfix: when changing sink after volume change, changes were applying to other sink 2025-09-24 19:20:44 +05:00
ItsLemmy 9105ec6b0d i18n: no more close side panel as its called control center 2025-09-24 10:17:28 -04:00
Ly-sec 9cfe49dec3 NComboBox: fix other languages display
Translations/de: update accordingly
2025-09-24 16:02:24 +02:00
ItsLemmy 58fb397e79 AudioTab: warning fix 2025-09-24 09:46:59 -04:00
Ly-sec 5de4330199 i18n: even more things appeared 2025-09-24 15:31:11 +02:00
Lemmy 5669debd6b Merge pull request #351 from FUFSoB/audio-changes
Audio changes
2025-09-24 09:29:23 -04:00
Lemmy e71335f9b6 Update README.md 2025-09-24 09:17:54 -04:00
Ly-sec 24cb5823ee Merge branch 'i18n' of https://github.com/noctalia-dev/noctalia-shell into i18n 2025-09-24 14:53:11 +02:00
Ly-sec 1470a92556 i18n: more cases detected 2025-09-24 14:53:09 +02:00
ItsLemmy 1d98a657b2 i18n: service init asap, avoid spamming the console as some warnings are inevitable due to async loading behavior 2025-09-24 08:50:40 -04:00
ItsLemmy 2e1f6f0323 Font: auto reloading with cache busting. 2025-09-24 08:37:29 -04:00
Ly-sec 04f247905a i18n-check: updated detection
i18n: added some odd ones
2025-09-24 14:30:30 +02:00
Ly-sec 2bfed74851 i18n: even more integration
autoformat
2025-09-24 14:24:21 +02:00
Ly-sec 2a23b6afdd i18n: WAY more i18n conversion 2025-09-24 14:12:12 +02:00
Ly-sec df70f0c824 Possibly got everything transfered over to i18n 2025-09-24 13:47:59 +02:00
Ly-sec 2285a3fb18 SettingsWindow: add i18n support 2025-09-24 13:20:49 +02:00
FUFSoB ef5447d2fa bugfix: make volume consistent with wpctl get-volume 2025-09-24 14:11:44 +05:00
FUFSoB fb64b3ba43 feat: volume overdrive 2025-09-24 14:04:08 +05:00
FUFSoB 1673201916 bugfix: update volume on sink/source changes 2025-09-24 13:03:39 +05:00
Lemmy 72475cd29b Merge pull request #344 from FUFSoB/notifications-refine
Notifications improvements
2025-09-23 23:01:33 -04:00
FUFSoB 41b9eb1897 Merge remote-tracking branch 'upstream/main' into notifications-refine
Resolve conflicts due to project structure changes
2025-09-24 07:40:50 +05:00
ItsLemmy 31db195087 First stab at i18n 2025-09-23 22:39:38 -04:00
ItsLemmy 9a9d68c78d NButton: Simplified by removing the press state which was causing issues with Popups opening hover the button 2025-09-23 15:32:24 -04:00
ItsLemmy a2b57c5165 Panels: more reliable draggable toggling 2025-09-23 14:42:55 -04:00
ItsLemmy e9efab0d59 Cava: also enable during lockscreen 2025-09-23 14:23:41 -04:00
FUFSoB 5d58083ee5 feat: progress bar for notifs 2025-09-23 22:57:19 +05:00
Ly-sec 055c7d3c20 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-23 18:42:07 +02:00
Ly-sec 0b5ef30b34 OSD: fix race condition 2025-09-23 18:42:05 +02:00
ItsLemmy 6d4ca4ffc0 OSD: moved settings in the appropriate spot 2025-09-23 12:40:40 -04:00
Ly-sec 4cd53c4083 OSD: unified Volume & Brightness OSD into one file (OSD.qml), move OSD settings to NotificationTab 2025-09-23 18:07:14 +02:00
Ly-sec c6303cdb6b Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-23 17:53:55 +02:00
Ly-sec c48e87e012 Settings: update default settings 2025-09-23 17:53:40 +02:00
Ly-sec 1ca84bf052 OSD: Implement Volume & Brightness OSD 2025-09-23 17:53:24 +02:00
ItsLemmy f86dac2172 DockMenu: minor UI tweaks. 2025-09-23 10:22:59 -04:00
ItsLemmy 59fe0a058e Autoformatting 2025-09-23 09:25:44 -04:00
ItsLemmy 640a4339db Cava: Now only runs when a visualizer is in sight. 2025-09-23 08:37:16 -04:00
FUFSoB 505cf48b6c other: small changes 2025-09-23 12:40:19 +05:00
FUFSoB 6d5574cac0 bugfix: urgency low was treated as normal 2025-09-23 11:46:46 +05:00
FUFSoB e35264708a bugfix: remove race condition, respect duration settings 2025-09-23 11:34:21 +05:00
FUFSoB ea0350bcca feat: set if notifs can be above fullscreen apps 2025-09-23 11:01:05 +05:00
FUFSoB b47ac6dd8a feat: set if respecting custom notif timeout 2025-09-23 10:53:44 +05:00
ItsLemmy 120ed36deb Cava: always active 2025-09-22 22:41:24 -04:00
ItsLemmy 26fe3114a6 Settings: updated comments 2025-09-22 22:39:47 -04:00
ItsLemmy 39e58acade MediaCard: Using the new NContextMenu 2025-09-22 22:34:35 -04:00
ItsLemmy 807e7394fe Cava + Visualizer: Should not depend on mpris. Its by design. 2025-09-22 22:07:29 -04:00
ItsLemmy d745be9c96 Bar section editor: better icons for move across sections 2025-09-22 21:45:22 -04:00
ItsLemmy 8f8f6c23ea Bar Editor: added ability to move widget to other sections with right clicking context menu. 2025-09-22 21:33:38 -04:00
ItsLemmy 3da0e529c6 Shell: cleanup 2025-09-22 21:09:45 -04:00
Ly-sec d5a862d904 shell: remove reload popup, except for error 2025-09-23 03:08:32 +02:00
Ly-sec 4de2b7f5a8 LockScreen: fix cursor 2025-09-23 03:02:44 +02:00
ItsLemmy 9f31c61a18 Bar section editor: added missing tooltips: 2025-09-22 21:00:51 -04:00
ItsLemmy d8539c0814 Removed filepicker icons aliases 2025-09-22 20:56:00 -04:00
ItsLemmy 9b8c0b9cf0 ListView replaced by proper NListView 2025-09-22 20:53:59 -04:00
ItsLemmy c4764c0e5b ScreenRecorder: disable toast when recording starts 2025-09-22 20:23:00 -04:00
ItsLemmy aec170d7f8 Fix a few hardcoded margin by proper Style.xxx 2025-09-22 20:16:39 -04:00
ItsLemmy a395156556 ControlCenterSettings fix 2025-09-22 20:14:42 -04:00
ItsLemmy 50ea3e9a8b More renaming 2025-09-22 20:09:12 -04:00
ItsLemmy 50ef79677e Updating bar widgets ids 2025-09-22 19:51:57 -04:00
ItsLemmy def778dbf1 Settings: Log before splicing or you will log the wrong widget.id 2025-09-22 19:39:52 -04:00
ItsLemmy b8f4401878 First pass 2025-09-22 19:11:10 -04:00
Ly-sec 9a7fb4a219 Bar/: add Calendar folder 2025-09-23 00:24:22 +02:00
Ly-sec 39b52eb17e Bar/: remove Panel suffix 2025-09-23 00:21:43 +02:00
Ly-sec 609f1e9655 Bar/: refactor layout 2025-09-23 00:20:06 +02:00
Ly-sec 9bb60d0ae3 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-23 00:01:36 +02:00
Ly-sec 202516aee3 Dock: fix pinned app grouping 2025-09-23 00:01:31 +02:00
Ly-sec 489ce76d2a Notification: layout changes 2025-09-22 23:56:18 +02:00
ItsLemmy 6a8c3c721a TablerIcons at root of Commons/ 2025-09-22 17:49:05 -04:00
ItsLemmy 21d331c232 ActiveWindow: more cleanup 2025-09-22 17:37:34 -04:00
Ly-sec 4c9d40865f NText: add elide (ltr & rtl) 2025-09-22 23:20:59 +02:00
Ly-sec 490200b3b8 ActiveWindow: properly hide when no window is available 2025-09-22 22:50:58 +02:00
Ly-sec 6031c97e1a ScreenRecorder: add toast for record stop/start/error 2025-09-22 22:47:16 +02:00
Ly-sec 4d0777ab93 Let people use scrollwheel to switch between workspaces (fixes #290) 2025-09-22 22:27:20 +02:00
Ly-sec 17308083fe Revert "ActiveWindow: hide ActiveWindow if there is no actual window"
This reverts commit 51fb5b9f4a.
2025-09-22 22:25:01 +02:00
Ly-sec 51fb5b9f4a ActiveWindow: hide ActiveWindow if there is no actual window 2025-09-22 22:23:39 +02:00
Ly-sec 773912320f LockScreen: fix expanding password 2025-09-22 22:19:43 +02:00
ItsLemmy 4a4cd20553 ActiveWindow: Fix #338 2025-09-22 16:01:15 -04:00
ItsLemmy 6fbaf46ed9 AppIcons => ThemeIcons 2025-09-22 14:58:34 -04:00
ItsLemmy 03da290c54 Notifications History: restored original panel width, changed title to: "Notifications" 2025-09-22 13:59:19 -04:00
FUFSoB 2d0d6207a1 WIP: notif progress bar 2025-09-22 22:51:25 +05:00
ItsLemmy f896b41c6b Dock: removed onCountChanged as it is unecessary and was producing warnings. 2025-09-22 13:49:03 -04:00
ItsLemmy e0d577cbda Prevent even more dragging. 2025-09-22 13:47:51 -04:00
ItsLemmy be1c975f4d Prevent even more dragging when popup are open. 2025-09-22 13:46:25 -04:00
ItsLemmy c20773d60b Prevent NPanel dragging when popup are open. 2025-09-22 13:40:38 -04:00
FUFSoB 45fb881ec2 rename notifications layer 2025-09-22 22:33:45 +05:00
ItsLemmy 64001152ef BarWidgetSettings: fix 2025-09-22 13:32:00 -04:00
ItsLemmy 5aa935b348 FileDialog: also properly hide/restore popups when opening 2025-09-22 12:19:41 -04:00
ItsLemmy 826dba7f53 Merge branch 'main' into file-dialog-builtin 2025-09-22 11:54:44 -04:00
Lemmy 358cfe26e2 Merge pull request #335 from lonerOrz/sidepanel
feat(bar): Allow custom icon for SidePanelToggle
2025-09-22 11:49:00 -04:00
ItsLemmy 8ece805273 File Picker: Using platform's native picker - removed custom picker. 2025-09-22 11:39:04 -04:00
Lysec 8e32816976 Merge pull request #336 from lonerOrz/systemMonitor
fix(bar): Ensure SystemMonitor temperature is fully visible
2025-09-22 16:31:08 +02:00
Ly-sec 64757979e8 Dock: use Style.fontSize, remove most logging 2025-09-22 16:25:44 +02:00
Ly-sec 26a4861a8b Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-22 16:10:46 +02:00
Ly-sec 21c6c5a610 Added pinning to dock & right click menu to dock
Dock: display pinned apps on the left even when not running (lower
opacity)
DockMenu: Let users close, activate and pin/unpin apps
Settings: add pinned list for docks
2025-09-22 16:09:25 +02:00
Lysec 5594257147 Merge pull request #340 from msdevpt/ghostty-template
fix: ghostty template
2025-09-22 15:59:12 +02:00
Ly-sec 879d9ec879 Notification: add location option
Autoformat
2025-09-22 14:09:23 +02:00
Ly-sec d13793fcbd Notification: add scaling 2025-09-22 13:58:59 +02:00
M.Silva 51138cbf55 fix: ghostty template 2025-09-22 08:38:22 +01:00
loner 355473a946 fix(bar): Ensure SystemMonitor temperature is fully visible
In the vertical bar layout, the temperature text in the SystemMonitor
widget (e.g., "55°C") could be truncated due to the widget's fixed
width.
  This commit resolves the issue by applying a dynamic scale
transformation to the text component.
2025-09-22 11:26:21 +08:00
loner f25bba7c11 feat(bar): Allow custom icon for SidePanelToggle
Adds a feature allowing users to select a custom image file to be used
as the icon for the SidePanelToggle widget.
  - Introduces a "Browse File" button in the widget's settings dialog,
utilizing the `NFilePicker` component.
  - An `NImageCircled` preview of the selected custom icon is now shown
in the settings.
  - The display logic for the widget is updated to prioritize the custom
icon path over the library icon and distro logo.
2025-09-22 11:05:26 +08:00
LemmyCook f348eb993c v2.13.0-dev 2025-09-21 21:31:38 -04:00
LemmyCook 3f1675b84a v2.13.0 2025-09-21 21:25:39 -04:00
LemmyCook 3aac552c44 Clock: Minor vertical adjustment tweaks when capsule are off. 2025-09-21 21:25:15 -04:00
LemmyCook 1717fc0992 NTextInput: new approach to avoid all input leakage and dragging NPanel issues. 2025-09-21 21:17:12 -04:00
LemmyCook a7e3deecd3 NInputButton properly uses NTextInput 2025-09-21 20:49:46 -04:00
LemmyCook 46c3ea5d22 Revert "fix: disable panel dragging during text input and dialog interaction"
This reverts commit 56db321846.
2025-09-21 20:24:51 -04:00
LemmyCook 78f0c1da6a Merge branch 'file-picker' 2025-09-21 20:22:09 -04:00
LemmyCook 4753766b4f Clock / DateTimeTokens: better look and alignment 2025-09-21 20:19:50 -04:00
LemmyCook 0c1ed01319 DisplayTab: slight UI rework 2025-09-21 17:06:15 -04:00
LemmyCook 91dbc6a7f1 Brightness: Fix wrong logger call. 2025-09-21 16:38:33 -04:00
LemmyCook d4a46e5361 Default settings generation completed! 2025-09-21 16:31:42 -04:00
LemmyCook 177a9743d6 Merge branch 'main' into default-settings 2025-09-21 15:42:16 -04:00
LemmyCook 2b8338938a Default wallpaper with the new logo (wip) 2025-09-21 15:41:58 -04:00
LemmyCook 84702465d7 wip: default settings 2025-09-21 15:40:41 -04:00
Ly-sec 3684c87f8c WallpaperTab: fix width of NInputAction for individual wallpapers
NFilePicker: reverse grid/listview button
2025-09-21 21:32:57 +02:00
Lemmy 85815ba86d Update README.md 2025-09-21 15:20:42 -04:00
LemmyCook 6eb453136d Wallpaper: cached images goes to their own subfolder. 2025-09-21 14:54:33 -04:00
Ly-sec 385f4943ae NFilePicker: cleanup 2025-09-21 20:52:47 +02:00
Ly-sec 4dcc9609d6 Add icons to TablerIcons, edit sizing of icons in FilePicker etc 2025-09-21 20:40:28 +02:00
Ly-sec 3bbf26a18e NFilePicker: renamed NFileManager to NFilePicker, update grid hover 2025-09-21 19:44:04 +02:00
Ly-sec dfe3aed46e NFilePicker: fix some layout/color issues 2025-09-21 19:39:52 +02:00
LemmyCook 796e080948 Merge branch 'notification-history-improved' 2025-09-21 12:28:55 -04:00
LemmyCook 052bdefaab Notification: finalization before merge 2025-09-21 12:28:42 -04:00
LemmyCook 794853b7bd Notifications: removed hard limit to 100 characters. 2025-09-21 10:56:27 -04:00
LemmyCook fbd431164b Notifications: minor renaming for clarity 2025-09-21 10:45:50 -04:00
Lysec 2c1c1a513a Merge pull request #332 from acdcbyl/main
MatugenTemplate: Try to fix ghostty template
2025-09-21 16:28:14 +02:00
LemmyCook 0279b5654a Notifications: minor renaming + house keeping. Bring back the close history when clearing all notifications 2025-09-21 10:24:47 -04:00
Aiser c93e907595 MatugenTemplate: Try to fix ghostty template 2025-09-21 19:35:12 +08:00
Ly-sec 5965004721 NFileManager: fix file path, add image thumbnails 2025-09-21 13:18:52 +02:00
Ly-sec 86d891cfa8 Add NInputButton widget and FileManagerService integration
NInputButton.qml: new input+button widget
FileManagerService.qml: singleton service for file/folder dialogs
NFileManager.qml: create first iteration of filemanager
WallpaperTab.qml: integrate NInputButton
ScreenRecorderTab.qml: integrate NInputButton
GeneralTab.qml: integrate NInputButton
2025-09-21 13:06:57 +02:00
Lysec 1161fca422 Merge pull request #331 from acdcbyl/main
MatugenTemplate: Rewrite ghostty template
2025-09-21 12:51:12 +02:00
Aiser 26575ade7e MatugenTemplate:Rewrite ghostty template 2025-09-21 18:48:28 +08:00
Ly-sec fac9b8f54c NotificationService: fix width/height warning 2025-09-21 11:12:18 +02:00
Ly-sec 71ce858b32 Notification: fix saving/deleting notifications 2025-09-21 10:59:44 +02:00
Ly-sec ff34696d28 NotificationService: cleanup, fix duplicate images, resize to 64x64 2025-09-21 10:48:43 +02:00
LemmyCook 2e0214ddb8 Workspaces: Fix scaling #328 2025-09-20 23:51:49 -04:00
LemmyCook f316effecd Clock: fixed centering and padding + smarted sizing. Fix #325 2025-09-20 23:46:12 -04:00
Lemmy 6aa14120de Merge pull request #327 from msdevpt/adjust-workspace-size
chore: adjust to maintain visual proportion
2025-09-20 23:27:41 -04:00
LemmyCook 1ad6969d9b Notification service: Full refactoring to support image caching for history. 2025-09-20 23:26:05 -04:00
LemmyCook aed7440c5b Center Fallback icon 2025-09-20 17:23:49 -04:00
LemmyCook 10534b46f9 test-notif: changed debian-logo to steam, as I don't have a debian logo 2025-09-20 16:40:36 -04:00
M.Silva 802d4efdd3 chore: adjust to maintain visual proportion 2025-09-20 19:47:19 +01:00
Lemmy 20949a0298 Merge pull request #322 from ixxie/flake/systemd-service
nix flake: systemd service + home manager settings
2025-09-20 12:19:51 -04:00
Matan Bendix Shenhav 8f596f14b0 feat(flake): enable home-manager colors options 2025-09-20 17:32:28 +02:00
LemmyCook c85043782f Clock: better settings UI + support for \\n in horizontal bar. 2025-09-20 10:44:50 -04:00
LemmyCook fe4603f87a Clock Settings: slight layout and wording improvement 2025-09-20 09:47:20 -04:00
Matan Bendix Shenhav f8313a04fd feat(flake): enable home-manager settings config 2025-09-20 15:12:01 +02:00
Matan Bendix Shenhav ba5e85ca67 chore(flake): format with nixfmt-rfc-style 2025-09-20 15:12:01 +02:00
Matan Bendix Shenhav 5233547d76 feat(flake): systemd service 2025-09-20 15:12:01 +02:00
Ly-sec 56db321846 fix: disable panel dragging during text input and dialog interaction
NPanel: disable DragHandler when popups open, block drag over text inputs
BarWidgetSettingsDialog: notify panel of open/close state
BarSectionEditor: pass panel reference to dialog
2025-09-20 12:23:43 +02:00
ItsLemmy 8d0ce8dc49 Clock: simpler format management (horiz vs vertical) so one can switch the bar position without editing its clock. 2025-09-20 03:01:06 -04:00
ItsLemmy a340f8f31f Merge branch 'main' of github.com:Ly-sec/Noctalia 2025-09-20 01:53:00 -04:00
ItsLemmy 3853c099d0 NTextInput: dont propagate events to avoid dragging panel when selecting text with the mouse. 2025-09-20 01:52:57 -04:00
Lemmy 35a928e3d8 Update README.md 2025-09-20 01:31:11 -04:00
ItsLemmy 8d942d0782 CLock settings: less tall UI for 1080p 2025-09-20 01:23:59 -04:00
Lemmy c70a66b589 Update README.md 2025-09-20 00:54:12 -04:00
Lemmy a8398916c9 New logo 2025-09-20 00:42:58 -04:00
LemmyCook ed464b196f Font: added new Noctalia icon + Niri icon. 2025-09-20 00:31:45 -04:00
LemmyCook f3f8b82fdd Clock: new approach to bar clock display based on tokens. 2025-09-19 23:18:59 -04:00
LemmyCook 2cd73c265d Settings: on load, automatically remove deprecated userSettings. 2025-09-19 22:42:09 -04:00
LemmyCook 737e990117 CustomButtonSettings: Using header for subsection 2025-09-19 22:41:32 -04:00
LemmyCook 8a78ee090a Cleanup: more strings 2025-09-19 17:11:34 -04:00
LemmyCook 761aa62995 Cleanup: more strings cleanup, removing capitalization and minor adjusments. 2025-09-19 17:03:31 -04:00
LemmyCook dabf281ae8 CustomButton: simplified icon selection (in accordance with sidepanel toggle) 2025-09-19 16:42:19 -04:00
LemmyCook 5cb9935f2f SidePanelToggle: now allows to pick any icon from the font. 2025-09-19 16:37:38 -04:00
LemmyCook 9236b2f00e autoformatting 2025-09-19 15:53:06 -04:00
LemmyCook 29b67f1337 Calendar: week numbers take 2 - Fix #308 2025-09-19 15:52:58 -04:00
LemmyCook dd2c02af3f Merge branch 'compositor-service' 2025-09-19 14:42:31 -04:00
LemmyCook b960441321 Revert flake.nix until it's properly investigated. 2025-09-19 14:02:13 -04:00
LemmyCook babb4ca202 Revert to the old flake.nix until things work as expected. 2025-09-19 14:01:19 -04:00
LemmyCook 4dc1076abc ActiveWindow: adaptation to the new compositor service 2025-09-19 13:45:12 -04:00
LemmyCook 590708da57 Bar: New widget "Wallpaper Selector" to open the selector directly. 2025-09-19 11:24:46 -04:00
LemmyCook 78df416bc7 KeepAwake: fix border onHover 2025-09-19 11:24:04 -04:00
LemmyCook fcc054c3ae WallpaperSelector: set current tab index to the current screen the UI opened on. 2025-09-19 11:18:55 -04:00
LemmyCook 06b858a77e Autoformatting 2025-09-19 11:05:35 -04:00
LemmyCook 658b583e84 Floating bar: On the perpendicular axis of the bar: only apply the floating margin between the screen and the bar. This will avoid people having to deal with struts and gaps.
- ex: if bar is on top, the vertical margin will only be applied between
the top screen edge and the bar, not extra margin below the bar
2025-09-19 11:05:15 -04:00
LemmyCook ed557af1c2 Tooltip improvements (only use period for long sentences) 2025-09-19 10:38:10 -04:00
LemmyCook 61203dc5fd Wallpaper Selector: added screen tab for a better UX. 2025-09-19 09:48:43 -04:00
Ly-sec b7d417ea91 flake: possible fix for installation issue 2025-09-19 12:55:57 +02:00
LemmyCook 978405bd85 2.12.1-dev 2025-09-18 23:42:34 -04:00
LemmyCook 878115db59 ScreenRecorderIndicator: Now always shown and can now start recording. 2025-09-18 23:34:20 -04:00
LemmyCook 50469e5c82 BarService: lookupWidget can now match by index. 2025-09-18 23:33:46 -04:00
LemmyCook 860e721709 Hotfix: do not filter our the screenrecorder indicator, as it messes with widgets index and settings. 2025-09-18 23:12:35 -04:00
LemmyCook 1dbc0cada6 WIP compositor cleanup 2025-09-18 22:58:57 -04:00
LemmyCook 88ece93db2 2.12.0-dev 2025-09-18 22:09:38 -04:00
157 changed files with 15248 additions and 4597 deletions
+34
View File
@@ -0,0 +1,34 @@
{
"dark": {
"mPrimary": "#E6B450",
"mOnPrimary": "#0B0E14",
"mSecondary": "#AAD94C",
"mOnSecondary": "#0B0E14",
"mTertiary": "#39BAE6",
"mOnTertiary": "#0B0E14",
"mError": "#D95757",
"mOnError": "#0B0E14",
"mSurface": "#1e222a",
"mOnSurface": "#BFBDB6",
"mSurfaceVariant": "#0B0E14",
"mOnSurfaceVariant": "#636A72",
"mOutline": "#565B66",
"mShadow": "#000000"
},
"light": {
"mPrimary": "#FF8F40",
"mOnPrimary": "#F8F9FA",
"mSecondary": "#86B300",
"mOnSecondary": "#F8F9FA",
"mTertiary": "#55B4D4",
"mOnTertiary": "#F8F9FA",
"mError": "#E65050",
"mOnError": "#F8F9FA",
"mSurface": "#E4E6E9",
"mOnSurface": "#5C6166",
"mSurfaceVariant": "#F8F9FA",
"mOnSurfaceVariant": "#ABADB1",
"mOutline": "#8A9199",
"mShadow": "#F8F9FA"
}
}
+28 -29
View File
@@ -1,35 +1,34 @@
{
"dark": {
"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": "#3e364e",
"mShadow": "#120f18"
"mPrimary": "#fff59b",
"mOnPrimary": "#0e0e43",
"mSecondary": "#a9aefe",
"mOnSecondary": "#0e0e43",
"mTertiary": "#9BFECE",
"mOnTertiary": "#0e0e43",
"mError": "#FD4663",
"mOnError": "#0e0e43",
"mSurface": "#070722",
"mOnSurface": "#f3edf7",
"mSurfaceVariant": "#11112d",
"mOnSurfaceVariant": "#7c80b4",
"mOutline": "#21215F",
"mShadow": "#070722"
},
"light": {
"mPrimary": "#9b59ba",
"mOnPrimary": "#ffffff",
"mSecondary": "#784999",
"mOnSecondary": "#ffffff",
"mTertiary": "#c17093",
"mOnTertiary": "#ffffff",
"mError": "#e9899d",
"mOnError": "#1e1418",
"mSurface": "#f5f1fa",
"mOnSurface": "#1c1822",
"mSurfaceVariant": "#e7dfee",
"mOnSurfaceVariant": "#4a3d59",
"mOutline": "#cebedc",
"mShadow": "#ffffff"
"mPrimary": "#5d65f5",
"mOnPrimary": "#dadcff",
"mSecondary": "#8E93D8",
"mOnSecondary": "#dadcff",
"mTertiary": "#0e0e43",
"mOnTertiary": "#fef29a",
"mError": "#FD4663",
"mOnError": "#0e0e43",
"mSurface": "#e6e8fa",
"mOnSurface": "#4b55c8",
"mSurfaceVariant": "#eff0ff",
"mOnSurfaceVariant": "#0e0e43",
"mOutline": "#8288fc",
"mShadow": "#f3edf7"
}
}
Binary file not shown.
+23 -21
View File
@@ -1,23 +1,25 @@
palette = 0={{colors.surface.default.hex}}
palette = 1={{colors.error.default.hex}}
palette = 2={{colors.tertiary.default.hex}}
palette = 3={{colors.secondary.default.hex}}
palette = 4={{colors.primary.default.hex}}
palette = 5={{colors.primary.default.hex}}
palette = 6={{colors.secondary.default.hex}}
palette = 7={{colors.on_background.default.hex}}
palette = 8={{colors.outline.default.hex}}
palette = 9={{colors.secondary_fixed_dim.default.hex}}
palette = 10={{colors.tertiary_container.default.hex}}
palette = 11={{colors.surface_container.default.hex}}
palette = 12={{colors.primary_container.default.hex}}
palette = 13={{colors.on_primary_container.default.hex}}
palette = 14={{colors.surface_variant.default.hex}}
palette = 15={{colors.on_background.default.hex}}
palette = 0= {{colors.shadow.default.hex}}
palette = 1= {{colors.error.default.hex}}
palette = 2= {{colors.tertiary.default.hex}}
palette = 3= {{colors.secondary.default.hex}}
palette = 4= {{colors.primary.default.hex}}
palette = 5= {{colors.primary.default.hex}}
palette = 6= {{colors.secondary.default.hex}}
palette = 7= {{colors.on_background.default.hex}}
palette = 8= {{colors.outline.default.hex}}
palette = 9= {{colors.secondary_fixed_dim.default.hex}}
palette = 10= {{colors.tertiary_container.default.hex}}
palette = 11= {{colors.surface_container.default.hex}}
palette = 12= {{colors.primary_container.default.hex}}
palette = 13= {{colors.on_primary_container.default.hex}}
palette = 14= {{colors.surface_variant.default.hex}}
palette = 15= {{colors.primary.default.hex}}
cursor-color = {{colors.primary.default.hex}}
foreground={{colors.on_surface.default.hex}}
background={{colors.surface.default.hex}}
cursor-color = {{colors.on_surface.default.hex}}
cursor-text = {{colors.on_surface.default.hex}}
foreground = {{colors.on_surface.default.hex}}
background = {{colors.surface.default.hex}}
selection-foreground = {{colors.on_secondary.default.hex}}
selection-background = {{colors.secondary_fixed_dim.default.hex}}
selection-background = {{colors.on_secondary.default.hex}}
selection-foreground = {{colors.secondary_fixed_dim.default.hex}}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 760 KiB

+188
View File
@@ -0,0 +1,188 @@
{
"settingsVersion": 6,
"bar": {
"position": "top",
"backgroundOpacity": 1,
"monitors": [],
"density": "default",
"showCapsule": true,
"floating": false,
"marginVertical": 0.25,
"marginHorizontal": 0.25,
"widgets": {
"left": [
{
"id": "SystemMonitor"
},
{
"id": "ActiveWindow"
},
{
"id": "MediaMini"
}
],
"center": [
{
"id": "Workspace"
}
],
"right": [
{
"id": "ScreenRecorder"
},
{
"id": "Tray"
},
{
"id": "NotificationHistory"
},
{
"id": "WiFi"
},
{
"id": "Bluetooth"
},
{
"id": "Battery"
},
{
"id": "Volume"
},
{
"id": "Brightness"
},
{
"id": "NightLight"
},
{
"id": "Clock"
},
{
"id": "ControlCenter"
}
]
}
},
"general": {
"avatarImage": "",
"dimDesktop": true,
"showScreenCorners": false,
"forceBlackScreenCorners": false,
"radiusRatio": 1,
"screenRadiusRatio": 1,
"animationSpeed": 1
},
"location": {
"name": "Tokyo",
"useFahrenheit": false,
"use12hourFormat": false,
"showWeekNumberInCalendar": false
},
"screenRecorder": {
"directory": "",
"frameRate": 60,
"audioCodec": "opus",
"videoCodec": "h264",
"quality": "very_high",
"colorRange": "limited",
"showCursor": true,
"audioSource": "default_output",
"videoSource": "portal"
},
"wallpaper": {
"enabled": true,
"directory": "",
"enableMultiMonitorDirectories": false,
"setWallpaperOnAllMonitors": true,
"fillMode": "crop",
"fillColor": "#000000",
"randomEnabled": false,
"randomIntervalSec": 300,
"transitionDuration": 1500,
"transitionType": "random",
"transitionEdgeSmoothness": 0.05,
"monitors": []
},
"appLauncher": {
"enableClipboardHistory": false,
"position": "center",
"backgroundOpacity": 1,
"pinnedExecs": [],
"useApp2Unit": false,
"sortByMostUsed": true
},
"dock": {
"autoHide": false,
"exclusive": false,
"backgroundOpacity": 1,
"floatingRatio": 1,
"monitors": [],
"pinnedApps": []
},
"network": {
"wifiEnabled": true,
"bluetoothEnabled": true
},
"notifications": {
"doNotDisturb": false,
"monitors": [],
"location": "top_right",
"alwaysOnTop": false,
"lastSeenTs": 0,
"respectExpireTimeout": false,
"lowUrgencyDuration": 3,
"normalUrgencyDuration": 8,
"criticalUrgencyDuration": 15,
"enableOSD": true
},
"audio": {
"volumeStep": 5,
"volumeOverdrive": false,
"cavaFrameRate": 60,
"visualizerType": "linear",
"mprisBlacklist": [],
"preferredPlayer": ""
},
"ui": {
"fontDefault": "Roboto",
"fontFixed": "DejaVu Sans Mono",
"fontBillboard": "Inter",
"monitorsScaling": [],
"idleInhibitorEnabled": false
},
"brightness": {
"brightnessStep": 5
},
"colorSchemes": {
"useWallpaperColors": false,
"predefinedScheme": "Noctalia (default)",
"darkMode": true
},
"matugen": {
"gtk4": false,
"gtk3": false,
"qt6": false,
"qt5": false,
"kitty": false,
"ghostty": false,
"foot": false,
"fuzzel": false,
"vesktop": false,
"pywalfox": false,
"enableUserTemplates": false
},
"nightLight": {
"enabled": false,
"forced": false,
"autoSchedule": true,
"nightTemp": "4000",
"dayTemp": "6500",
"manualSunrise": "06:30",
"manualSunset": "18:30"
},
"hooks": {
"enabled": false,
"wallpaperChange": "",
"darkModeChange": ""
}
}
+424
View File
@@ -0,0 +1,424 @@
#!/bin/bash
# JSON Language File Comparison Script
# Compares language files against en.json reference and generates a report
set -euo pipefail
# Configuration
FOLDER_PATH="Assets/Translations"
REFERENCE_FILE="en.json"
# Colors for terminal output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Function to print colored output
print_color() {
local color=$1
local message=$2
echo -e "${color}${message}${NC}"
}
# Function to check if jq is installed
check_dependencies() {
if ! command -v jq &> /dev/null; then
print_color $RED "Error: 'jq' is required but not installed. Please install jq first." >&2
print_color $YELLOW "On Ubuntu/Debian: sudo apt-get install jq" >&2
print_color $YELLOW "On CentOS/RHEL: sudo yum install jq" >&2
print_color $YELLOW "On macOS: brew install jq" >&2
exit 1
fi
}
# Function to extract all keys from a JSON file recursively
extract_keys() {
local json_file=$1
if [[ ! -f "$json_file" ]]; then
echo "Error: File $json_file not found" >&2
return 1
fi
# Extract all keys recursively using jq
jq -r '
def keys_recursive:
if type == "object" then
keys[] as $k |
if (.[$k] | type) == "object" then
($k + "." + (.[$k] | keys_recursive))
else
$k
end
else
empty
end;
keys_recursive
' "$json_file" 2>/dev/null | sort
}
# Function to get language files
get_language_files() {
find "$FOLDER_PATH" -maxdepth 1 -name "*.json" -type f | sort
}
# Function to generate report header
generate_header() {
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "================================================================================"
echo " LANGUAGE FILE COMPARISON REPORT"
echo "================================================================================"
echo "Generated: $timestamp"
echo "Reference file: $REFERENCE_FILE"
echo "Folder: $(realpath "$FOLDER_PATH")"
echo ""
echo "Notes:"
echo "- Keys are compared recursively through all nested JSON objects"
echo "- Missing keys indicate incomplete translations"
echo "- Extra keys might indicate deprecated keys or translation-specific additions"
echo "- Translation completion percentage is calculated based on English reference"
echo "- Results are sorted by descending line numbers for easier editing"
echo ""
echo "This report compares all language JSON files against the English reference file"
echo "and identifies missing keys and extra keys in each language."
echo ""
}
# Function to find line number of a key in JSON file
find_key_line_number() {
local json_file=$1
local key_path=$2
# Extract the final key name (after last dot)
local key_name="${key_path##*.}"
# Search for the key in the file with line numbers
# Look for the pattern "key": (with quotes and colon)
local line_num=$(grep -n "\"$key_name\":" "$json_file" 2>/dev/null | head -1 | cut -d: -f1 || echo "")
if [[ -n "$line_num" ]]; then
echo "$line_num"
else
# If not found with quotes, try without (though less reliable)
line_num=$(grep -n "$key_name:" "$json_file" 2>/dev/null | head -1 | cut -d: -f1 || echo "")
if [[ -n "$line_num" ]]; then
echo "$line_num"
else
echo "?"
fi
fi
}
# Function to safely count lines
count_non_empty_lines() {
local content="$1"
if [[ -z "$content" ]]; then
echo "0"
else
echo "$content" | grep -c -v '^$' || echo "0"
fi
}
# Function to compare keys and generate report section
compare_language() {
local lang_file="$1"
local lang_name="$2"
local ref_keys_file="$3"
local ref_file_path="$FOLDER_PATH/$REFERENCE_FILE"
# Create temporary file for language keys
local lang_keys_file=$(mktemp)
extract_keys "$lang_file" > "$lang_keys_file" || {
echo "Error: Failed to extract keys from $lang_file" >&2
rm -f "$lang_keys_file"
return 1
}
# Get missing and extra keys safely
local missing_keys=""
local extra_keys=""
missing_keys=$(comm -23 "$ref_keys_file" "$lang_keys_file" 2>/dev/null || echo "")
extra_keys=$(comm -13 "$ref_keys_file" "$lang_keys_file" 2>/dev/null || echo "")
# Count lines safely
local missing_count=$(count_non_empty_lines "$missing_keys")
local extra_count=$(count_non_empty_lines "$extra_keys")
local total_ref_keys=$(wc -l < "$ref_keys_file" 2>/dev/null || echo "0")
local total_lang_keys=$(wc -l < "$lang_keys_file" 2>/dev/null || echo "0")
# Calculate completion percentage safely
local completion_percentage=0
if [[ $total_ref_keys -gt 0 ]]; then
completion_percentage=$(( (total_ref_keys - missing_count) * 100 / total_ref_keys ))
fi
print_color $YELLOW "================================================================================"
print_color $YELLOW "LANGUAGE: $lang_name"
print_color $YELLOW "================================================================================"
echo "File: $lang_file"
echo "Total keys in reference (en): $total_ref_keys"
echo "Total keys in $lang_name: $total_lang_keys"
# Color code the completion percentage
if [[ $completion_percentage -eq 100 ]]; then
echo -e "Translation completion: ${GREEN}${completion_percentage}%${NC}"
else
echo -e "Translation completion: ${RED}${completion_percentage}%${NC}"
fi
echo ""
echo "SUMMARY:"
echo "- Missing keys (exist in English but not in $lang_name): $missing_count"
echo "- Extra keys (exist in $lang_name but not in English): $extra_count"
echo ""
# Handle missing keys
if [[ $missing_count -gt 0 && -n "$missing_keys" ]]; then
echo "MISSING KEYS IN $lang_name:"
# Collect keys with line numbers and sort by line number (descending)
local temp_missing=$(mktemp)
while IFS= read -r key; do
if [[ -n "$key" ]]; then
local ref_line=$(find_key_line_number "$ref_file_path" "$key")
# Use numeric sort padding for proper sorting
if [[ "$ref_line" =~ ^[0-9]+$ ]]; then
printf "%06d|%s|en.json:%s\n" "$ref_line" "$key" "$ref_line" >> "$temp_missing"
else
printf "999999|%s|en.json:%s\n" "$key" "$ref_line" >> "$temp_missing"
fi
fi
done <<< "$missing_keys"
# Sort by line number (descending) and display
local counter=1
sort -t'|' -k1,1nr "$temp_missing" | while IFS='|' read -r sort_key key location; do
printf " %3d. %s (%s)\n" "$counter" "$key" "$location"
counter=$((counter + 1))
done
rm -f "$temp_missing"
echo ""
else
echo "✅ No missing keys in $lang_name"
echo ""
fi
# Handle extra keys
if [[ $extra_count -gt 0 && -n "$extra_keys" ]]; then
echo "EXTRA KEYS IN $lang_name (not in English):"
# Collect keys with line numbers and sort by line number (descending)
local temp_extra=$(mktemp)
while IFS= read -r key; do
if [[ -n "$key" ]]; then
local lang_line=$(find_key_line_number "$lang_file" "$key")
# Use numeric sort padding for proper sorting
if [[ "$lang_line" =~ ^[0-9]+$ ]]; then
printf "%06d|%s|%s:%s\n" "$lang_line" "$key" "$(basename "$lang_file")" "$lang_line" >> "$temp_extra"
else
printf "999999|%s|%s:%s\n" "$key" "$(basename "$lang_file")" "$lang_line" >> "$temp_extra"
fi
fi
done <<< "$extra_keys"
# Sort by line number (descending) and display
local counter=1
sort -t'|' -k1,1nr "$temp_extra" | while IFS='|' read -r sort_key key location; do
printf " %3d. %s (%s)\n" "$counter" "$key" "$location"
counter=$((counter + 1))
done
rm -f "$temp_extra"
echo ""
else
echo "✅ No extra keys in $lang_name"
echo ""
fi
# Clean up
rm -f "$lang_keys_file"
}
# Main function
main() {
local target_language="$1"
print_color $BLUE "Starting language file comparison..." >&2
# Check dependencies
check_dependencies
# Validate folder path
if [[ ! -d "$FOLDER_PATH" ]]; then
print_color $RED "Error: Folder '$FOLDER_PATH' does not exist" >&2
exit 1
fi
# Check if reference file exists
local ref_file_path="$FOLDER_PATH/$REFERENCE_FILE"
if [[ ! -f "$ref_file_path" ]]; then
print_color $RED "Error: Reference file '$ref_file_path' does not exist" >&2
exit 1
fi
print_color $GREEN "Reference file found: $ref_file_path" >&2
# Extract keys from reference file
local ref_keys_file=$(mktemp)
if ! extract_keys "$ref_file_path" > "$ref_keys_file"; then
print_color $RED "Error: Failed to extract keys from reference file" >&2
rm -f "$ref_keys_file"
exit 1
fi
local total_ref_keys=$(wc -l < "$ref_keys_file" 2>/dev/null || echo "0")
print_color $BLUE "Extracted $total_ref_keys keys from reference file" >&2
# Get all language files or just the target language
local -a language_files
if [[ -n "$target_language" ]]; then
# Single language mode
local target_file="$FOLDER_PATH/${target_language}.json"
if [[ ! -f "$target_file" ]]; then
print_color $RED "Error: Language file '$target_file' does not exist" >&2
rm -f "$ref_keys_file"
exit 1
fi
if [[ "$target_language" == "${REFERENCE_FILE%.json}" ]]; then
print_color $RED "Error: Cannot compare reference file against itself" >&2
rm -f "$ref_keys_file"
exit 1
fi
language_files=("$target_file")
print_color $BLUE "Checking single language: $target_language" >&2
else
# All languages mode
while IFS= read -r -d '' file; do
language_files+=("$file")
done < <(find "$FOLDER_PATH" -maxdepth 1 -name "*.json" -type f -print0 | sort -z)
if [[ ${#language_files[@]} -eq 0 ]]; then
print_color $RED "Error: No JSON files found in $FOLDER_PATH" >&2
rm -f "$ref_keys_file"
exit 1
fi
print_color $BLUE "Found ${#language_files[@]} JSON files to process" >&2
fi
echo "" >&2
# Generate report header
generate_header
local processed=0
for lang_file in "${language_files[@]}"; do
local filename=$(basename "$lang_file")
local lang_name="${filename%.json}"
# Skip the reference file in all-languages mode
if [[ -z "$target_language" && "$filename" == "$REFERENCE_FILE" ]]; then
continue
fi
print_color $YELLOW "Processing: $filename" >&2
# Validate JSON syntax
if ! jq empty "$lang_file" 2>/dev/null; then
print_color $RED "Warning: $lang_file contains invalid JSON syntax. Skipping..." >&2
echo "ERROR: $lang_file contains invalid JSON syntax and was skipped."
echo ""
continue
fi
if compare_language "$lang_file" "$lang_name" "$ref_keys_file"; then
processed=$((processed + 1))
else
print_color $RED "Error processing $lang_file" >&2
fi
done
# Add summary at the end
echo "================================================================================"
echo "SUMMARY"
echo "================================================================================"
echo "Total files processed: $processed"
echo "Reference file: $REFERENCE_FILE (English)"
if [[ -n "$target_language" ]]; then
echo "Target language: $target_language"
fi
echo "Report generated: $(date '+%Y-%m-%d %H:%M:%S')"
echo ""
echo "================================================================================"
# Clean up
rm -f "$ref_keys_file"
if [[ -n "$target_language" ]]; then
print_color $GREEN "Comparison completed for language: $target_language" >&2
else
print_color $GREEN "Comparison completed: Processed $processed language files against English reference" >&2
fi
}
# Usage information
show_usage() {
echo "Usage: $0 [language_code]" >&2
echo "" >&2
echo "This script compares JSON language files in '$FOLDER_PATH' against the English reference." >&2
echo "" >&2
echo "Arguments:" >&2
echo " language_code Optional. Compare only the specified language (e.g., 'fr', 'es', 'de')" >&2
echo " If not provided, all language files will be compared" >&2
echo "" >&2
echo "Configuration:" >&2
echo " - Folder path: $FOLDER_PATH (hardcoded)" >&2
echo " - Reference file: $REFERENCE_FILE" >&2
echo "" >&2
echo "Examples:" >&2
echo " $0 # Compare all languages" >&2
echo " $0 fr # Compare only French (fr.json)" >&2
echo " $0 es # Compare only Spanish (es.json)" >&2
echo "" >&2
echo "Requirements:" >&2
echo " - jq must be installed" >&2
echo " - $REFERENCE_FILE must exist in $FOLDER_PATH" >&2
echo " - Target language file must exist if specified" >&2
echo "" >&2
echo "Output:" >&2
echo " - Comparison report is printed to stdout" >&2
echo " - Progress messages are printed to stderr" >&2
echo " - Results are sorted by descending line numbers for easier editing" >&2
}
# Handle command line arguments
if [[ $# -gt 1 ]]; then
echo "Error: Too many arguments. Only one language code is allowed." >&2
echo "" >&2
show_usage
exit 1
fi
if [[ $# -eq 1 ]]; then
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
show_usage
exit 0
else
# Validate language code format (basic check for reasonable filename)
if [[ ! "$1" =~ ^[a-zA-Z][a-zA-Z0-9_-]*$ ]]; then
echo "Error: Invalid language code format '$1'. Use alphanumeric characters, hyphens, and underscores only." >&2
echo "" >&2
show_usage
exit 1
fi
# Run main function with target language
main "$1"
fi
else
# Run main function for all languages
main ""
fi
+31
View File
@@ -0,0 +1,31 @@
#!/bin/bash
# Comprehensive i18n checker for QML files
# Finds hardcoded strings in various QML properties
find . -name "*.qml" -type f | while read -r file; do
# Skip if file doesn't exist or is not readable
[[ ! -r "$file" ]] && continue
# Check for hardcoded strings in common properties
# Matches: property: "text with letters" but excludes I18n.tr calls
issues=$(grep -n -E '(label|text|title|description|placeholder|tooltipText|tooltip):\s*"[^"]*[a-zA-Z][^"]*"' "$file" | grep -v 'I18n\.tr')
# Also check for template literals with hardcoded text
template_issues=$(grep -n -E '(label|text|title|description|placeholder|tooltipText|tooltip):\s*`[^`]*[a-zA-Z][^`]*`' "$file" | grep -v 'I18n\.tr')
# Check for property assignments with hardcoded strings
property_issues=$(grep -n -E 'property\s+string\s+\w+:\s*"[^"]*[a-zA-Z][^"]*"' "$file" | grep -v 'I18n\.tr')
# Check for JavaScript object properties with hardcoded strings (like in arrays/models)
js_object_issues=$(grep -n -E '"(label|text|title|description|placeholder|name)":\s*"[^"]*[a-zA-Z][^"]*"' "$file" | grep -v 'I18n\.tr')
if [[ -n "$issues" || -n "$template_issues" || -n "$property_issues" || -n "$js_object_issues" ]]; then
echo "$file"
[[ -n "$issues" ]] && echo "$issues"
[[ -n "$template_issues" ]] && echo "$template_issues"
[[ -n "$property_issues" ]] && echo "$property_issues"
[[ -n "$js_object_issues" ]] && echo "$js_object_issues"
echo
fi
done
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env -S bash
echo "Sending test notifications..."
# Send a bunch of notifications with numbers
for i in {1..4}; do
notify-send "Notification $i" "This is test notification number $i with a very long text that will probably break the layout or maybe not? Who knows? Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
sleep 1
done
echo "All notifications sent!"
# Additional tests for icon/image handling
if command -v notify-send >/dev/null 2>&1; then
echo "Sending icon/image tests..."
# 1) Themed icon name
notify-send -i dialog-information "Icon name test" "Should resolve from theme (dialog-information)"
# 2) Absolute path if a sample image exists
SAMPLE_IMG="/usr/share/pixmaps/steam.png"
if [ -f "$SAMPLE_IMG" ]; then
notify-send -i "$SAMPLE_IMG" "Absolute path test" "Should show the provided image path"
fi
# 3) file:// URL form
if [ -f "$SAMPLE_IMG" ]; then
notify-send -i "file://$SAMPLE_IMG" "file:// URL test" "Should display after stripping scheme"
fi
echo "Icon/image tests sent!"
fi
# A test notification with actions
gdbus call --session \
--dest org.freedesktop.Notifications \
--object-path /org/freedesktop/Notifications \
--method org.freedesktop.Notifications.Notify \
"my-app" \
0 \
"dialog-question" \
"Confirmation Required" \
"Do you want to proceed with the action?" \
"['default', 'OK', 'cancel', 'Cancel']" \
"{}" \
5000
-32
View File
@@ -1,32 +0,0 @@
#!/usr/bin/env -S 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!"
# Additional tests for icon/image handling
if command -v notify-send >/dev/null 2>&1; then
echo "Sending icon/image tests..."
# 1) Themed icon name
notify-send -i dialog-information "Icon name test" "Should resolve from theme (dialog-information)"
# 2) Absolute path if a sample image exists
SAMPLE_IMG="/usr/share/pixmaps/debian-logo.png"
if [ -f "$SAMPLE_IMG" ]; then
notify-send -i "$SAMPLE_IMG" "Absolute path test" "Should show the provided image path"
fi
# 3) file:// URL form
if [ -f "$SAMPLE_IMG" ]; then
notify-send -i "file://$SAMPLE_IMG" "file:// URL test" "Should display after stripping scheme"
fi
echo "Icon/image tests sent!"
fi
+351
View File
@@ -0,0 +1,351 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons
Singleton {
id: root
property bool debug: false
property string debugForceLanguage: ""
property bool isLoaded: false
property string langCode: ""
property var availableLanguages: []
property var translations: ({})
property var fallbackTranslations: ({})
// Signals for reactive updates
signal languageChanged(string newLanguage)
signal translationsLoaded
// Process to list directory contents
property Process directoryScanner: Process {
id: directoryProcess
command: ["ls", `${Quickshell.shellDir}/Assets/Translations/`]
running: false
stdout: StdioCollector {
id: stdoutCollector
}
onExited: function (exitCode, exitStatus) {
if (exitCode === 0) {
var output = stdoutCollector.text || ""
parseDirectoryListing(output)
} else {
Logger.error("I18n", `Failed to scan translation directory`)
// Fallback to default languages
availableLanguages = ["en"]
detectLanguage()
}
}
}
// FileView to load translation files
property FileView translationFile: FileView {
id: fileView
watchChanges: true
onFileChanged: reload()
onLoaded: {
try {
var data = JSON.parse(text())
root.translations = data
root.isLoaded = true
root.translationsLoaded()
Logger.log("I18n", `Loaded translations for "${root.langCode}"`)
} catch (e) {
Logger.error("I18n", `Failed to parse translation file: ${e}`)
setLanguage("en")
}
}
onLoadFailed: function (error) {
setLanguage("en")
Logger.error("I18n", `Failed to load translation file: ${error}`)
}
}
// FileView to load fallback translation files
property FileView fallbackTranslationFile: FileView {
id: fallbackFileView
watchChanges: true
onFileChanged: reload()
onLoaded: {
try {
var data = JSON.parse(text())
root.fallbackTranslations = data
Logger.log("I18n", `Loaded english fallback translations`)
} catch (e) {
Logger.error("I18n", `Failed to parse fallback translation file: ${e}`)
}
}
onLoadFailed: function (error) {
Logger.error("I18n", `Failed to load fallback translation file: ${error}`)
}
}
Component.onCompleted: {
Logger.log("I18n", "Service started")
scanAvailableLanguages()
}
// -------------------------------------------
function scanAvailableLanguages() {
Logger.log("I18n", "Scanning for available translation files...")
directoryScanner.running = true
}
// -------------------------------------------
function parseDirectoryListing(output) {
var languages = []
try {
if (!output || output.trim() === "") {
Logger.warn("I18n", "Empty directory listing output")
availableLanguages = ["en"]
detectLanguage()
return
}
const entries = output.trim().split('\n')
for (var i = 0; i < entries.length; i++) {
const entry = entries[i].trim()
if (entry && entry.endsWith('.json')) {
// Extract language code from filename (e.g., "en.json" -> "en")
const langCode = entry.substring(0, entry.lastIndexOf('.json'))
if (langCode.length >= 2 && langCode.length <= 5) {
// Basic validation for language codes
languages.push(langCode)
}
}
}
// Sort languages alphabetically, but ensure "en" comes first if available
languages.sort()
const enIndex = languages.indexOf("en")
if (enIndex > 0) {
languages.splice(enIndex, 1)
languages.unshift("en")
}
if (languages.length === 0) {
Logger.warn("I18n", "No translation files found, using fallback")
languages = ["en"] // Fallback
}
availableLanguages = languages
Logger.log("I18n", `Found ${languages.length} available languages: ${languages.join(', ')}`)
// Detect language after scanning
detectLanguage()
} catch (e) {
Logger.error("I18n", `Failed to parse directory listing: ${e}`)
// Fallback to default languages
availableLanguages = ["en"]
detectLanguage()
}
}
// -------------------------------------------
function detectLanguage() {
Logger.log("I18n", `detectLanguage() called. Available languages: [${availableLanguages.join(', ')}]`)
if (availableLanguages.length === 0) {
Logger.warn("I18n", "No available languages found")
return
}
if (debug && debugForceLanguage !== "") {
Logger.log("I18n", `Debug mode: forcing language to "${debugForceLanguage}"`)
if (availableLanguages.includes(debugForceLanguage)) {
setLanguage(debugForceLanguage)
return
} else {
Logger.warn("I18n", `Debug language "${debugForceLanguage}" not available in [${availableLanguages.join(', ')}]`)
}
}
// Detect user's favorite locale - languages
for (var i = 0; i < Qt.locale().uiLanguages.length; i++) {
const userLang = Qt.locale().uiLanguages[i].substring(0, 2)
if (availableLanguages.includes(userLang)) {
setLanguage(userLang)
return
}
}
// Fallback to first available language (preferably "en" if available)
const fallbackLang = availableLanguages.includes("en") ? "en" : availableLanguages[0]
setLanguage(fallbackLang)
}
// -------------------------------------------
function setLanguage(newLangCode) {
if (newLangCode !== langCode && availableLanguages.includes(newLangCode)) {
langCode = newLangCode
Logger.log("I18n", `Language set to "${langCode}"`)
languageChanged(langCode)
loadTranslations()
} else if (!availableLanguages.includes(newLangCode)) {
Logger.warn("I18n", `Language "${newLangCode}" is not available`)
}
}
// -------------------------------------------
function loadTranslations() {
if (langCode === "")
return
const filePath = `file://${Quickshell.shellDir}/Assets/Translations/${langCode}.json`
fileView.path = filePath
isLoaded = false
Logger.log("I18n", `Loading translations from: ${filePath}`)
// Only load fallback translations if we are not using english and english is available
if (langCode !== "en" && availableLanguages.includes("en")) {
fallbackFileView.path = `file://${Quickshell.shellDir}/Assets/Translations/en.json`
}
}
// -------------------------------------------
// Check if a translation exists
function hasTranslation(key) {
if (!isLoaded)
return false
const keys = key.split(".")
var value = translations
for (var i = 0; i < keys.length; i++) {
if (value && typeof value === "object" && keys[i] in value) {
value = value[keys[i]]
} else {
return false
}
}
return typeof value === "string"
}
// -------------------------------------------
// Get all translation keys (useful for debugging)
function getAllKeys(obj, prefix) {
if (typeof obj === "undefined")
obj = translations
if (typeof prefix === "undefined")
prefix = ""
var keys = []
for (var key in (obj || {})) {
const value = obj[key]
const fullKey = prefix ? `${prefix}.${key}` : key
if (typeof value === "object" && value !== null) {
keys = keys.concat(getAllKeys(value, fullKey))
} else if (typeof value === "string") {
keys.push(fullKey)
}
}
return keys
}
// -------------------------------------------
// Reload translations (useful for development)
function reload() {
Logger.log("I18n", "Reloading translations")
loadTranslations()
}
// -------------------------------------------
// Main translation function
function tr(key, interpolations) {
if (typeof interpolations === "undefined")
interpolations = {}
if (!isLoaded) {
// if (debug) {
// Logger.warn("I18n", "Translations not loaded yet")
// }
return key
}
// Navigate nested keys (e.g., "menu.file.open")
const keys = key.split(".")
// Look-up translation in the active language
var value = translations
var notFound = false
for (var i = 0; i < keys.length; i++) {
if (value && typeof value === "object" && keys[i] in value) {
value = value[keys[i]]
} else {
if (debug) {
Logger.warn("I18n", `Translation key "${key}" not found`)
}
notFound = true
break
}
}
// Fallback to english if not found
if (notFound && availableLanguages.includes("en") && langCode !== "en") {
value = fallbackTranslations
for (var i = 0; i < keys.length; i++) {
if (value && typeof value === "object" && keys[i] in value) {
value = value[keys[i]]
} else {
// Indicate this key does not even exists in the english fallback
return `## ${key} ##`
}
}
// Make untranslated string easy to spot
value = `<i>${value}</i>`
} else if (notFound) {
// No fallback available
return `## ${key} ##`
}
if (typeof value !== "string") {
if (debug) {
Logger.warn("I18n", `Translation key "${key}" is not a string`)
}
return key
}
// Handle interpolations (e.g., "Hello {name}!")
var result = value
for (var placeholder in interpolations) {
const regex = new RegExp(`\\{${placeholder}\\}`, 'g')
result = result.replace(regex, interpolations[placeholder])
}
return result
}
// -------------------------------------------
// Plural translation function
function trp(key, count, defaultSingular, defaultPlural, interpolations) {
if (typeof defaultSingular === "undefined")
defaultSingular = ""
if (typeof defaultPlural === "undefined")
defaultPlural = ""
if (typeof interpolations === "undefined")
interpolations = {}
const pluralKey = count === 1 ? key : `${key}_plural`
const defaultValue = count === 1 ? defaultSingular : defaultPlural
// Merge interpolations with count (QML doesn't support spread operator)
var finalInterpolations = {
"count": count
}
for (var prop in interpolations) {
finalInterpolations[prop] = interpolations[prop]
}
return tr(pluralKey, finalInterpolations)
}
}
+51 -15
View File
@@ -4,22 +4,41 @@ import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Commons
import qs.Commons.IconsSets
Singleton {
id: root
// Expose the font family name for easy access
readonly property string fontFamily: fontLoader.name
readonly property string fontFamily: currentFontLoader ? currentFontLoader.name : ""
readonly property string defaultIcon: TablerIcons.defaultIcon
readonly property var icons: TablerIcons.icons
readonly property var aliases: TablerIcons.aliases
readonly property string fontPath: "/Assets/Fonts/tabler/tabler-icons.ttf"
// Current active font loader
property FontLoader currentFontLoader: null
property int fontVersion: 0
// Create a unique cache-busting path
readonly property string cacheBustingPath: Quickshell.shellDir + fontPath + "?v=" + fontVersion + "&t=" + Date.now()
// Signal emitted when font is reloaded
signal fontReloaded
Component.onCompleted: {
Logger.log("Icons", "Service started")
loadFontWithCacheBusting()
}
Connections {
target: Quickshell
function onReloadCompleted() {
Logger.log("Icons", "Quickshell reload completed - forcing font reload")
reloadFont()
}
}
// ---------------------------------------
function get(iconName) {
// Check in aliases first
if (aliases[iconName] !== undefined) {
@@ -30,20 +49,37 @@ Singleton {
return icons[iconName]
}
FontLoader {
id: fontLoader
source: Quickshell.shellDir + fontPath
function loadFontWithCacheBusting() {
Logger.log("Icons", "Loading font with cache busting:", cacheBustingPath)
// Destroy old loader first
if (currentFontLoader) {
currentFontLoader.destroy()
currentFontLoader = null
}
// Create new loader with cache-busting URL
currentFontLoader = Qt.createQmlObject(`
import QtQuick
FontLoader {
source: "${cacheBustingPath}"
}
`, root, "dynamicFontLoader_" + fontVersion)
// Connect to the new loader's status changes
currentFontLoader.statusChanged.connect(function () {
if (currentFontLoader.status === FontLoader.Ready) {
Logger.log("Icons", "Font loaded successfully:", currentFontLoader.name, "(version " + fontVersion + ")")
fontReloaded()
} else if (currentFontLoader.status === FontLoader.Error) {
Logger.error("Icons", "Font failed to load (version " + fontVersion + ")")
}
})
}
// Monitor font loading status
Connections {
target: fontLoader
function onStatusChanged() {
if (fontLoader.status === FontLoader.Ready) {
Logger.log("Icons", "Font loaded successfully:", fontFamily)
} else if (fontLoader.status === FontLoader.Error) {
Logger.error("Icons", "Font failed to load")
}
}
function reloadFont() {
Logger.log("Icons", "Forcing font reload...")
fontVersion++
loadFontWithCacheBusting()
}
}
+417 -364
View File
@@ -5,10 +5,16 @@ import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services
import "../Helpers/QtObj2JS.js" as QtObj2JS
Singleton {
id: root
// Used to access via Settings.data.xxx.yyy
readonly property alias data: adapter
property bool isLoaded: false
property bool directoriesCreated: false
// Define our app directories
// Default config directory: ~/.config/noctalia
// Default cache directory: ~/.cache/noctalia
@@ -16,187 +22,47 @@ Singleton {
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 cacheDirImages: cacheDir + "images/"
property string cacheDirImagesWallpapers: cacheDir + "images/wallpapers/"
property string cacheDirImagesNotifications: cacheDir + "images/notifications/"
property string settingsFile: Quickshell.env("NOCTALIA_SETTINGS_FILE") || (configDir + "settings.json")
property string defaultLocation: "Tokyo"
property string defaultWallpaper: Quickshell.shellDir + "/Assets/Wallpaper/noctalia.png"
property string defaultAvatar: Quickshell.env("HOME") + "/.face"
property string defaultVideosDirectory: Quickshell.env("HOME") + "/Videos"
property string defaultLocation: "Tokyo"
property string defaultWallpapersDirectory: Quickshell.env("HOME") + "/Pictures/Wallpapers"
property string defaultWallpaper: Quickshell.shellDir + "/Assets/Wallpaper/noctalia.png"
// Used to access via Settings.data.xxx.yyy
readonly property alias data: adapter
property bool isLoaded: false
property bool directoriesCreated: false
// Signal emitted when settings are loaded after startupcale changes
signal settingsLoaded
// -----------------------------------------------------
// Function to validate monitor configurations
function validateMonitorConfigurations() {
var availableScreenNames = []
for (var i = 0; i < Quickshell.screens.length; i++) {
availableScreenNames.push(Quickshell.screens[i].name)
}
Logger.log("Settings", "Available monitors: [" + availableScreenNames.join(", ") + "]")
Logger.log("Settings", "Configured bar monitors: [" + adapter.bar.monitors.join(", ") + "]")
// Check bar monitors
if (adapter.bar.monitors.length > 0) {
var hasValidBarMonitor = false
for (var j = 0; j < adapter.bar.monitors.length; j++) {
if (availableScreenNames.includes(adapter.bar.monitors[j])) {
hasValidBarMonitor = true
break
}
}
if (!hasValidBarMonitor) {
Logger.warn("Settings", "No configured bar monitors found on system, clearing bar monitor list to show on all screens")
adapter.bar.monitors = []
} else {
//Logger.log("Settings", "Found valid bar monitors, keeping configuration")
}
} else {
//Logger.log("Settings", "Bar monitor list is empty, will show on all available screens")
}
}
// -----------------------------------------------------
// If the settings structure has changed, ensure
// backward compatibility by upgrading the settings
function upgradeSettingsData() {
const sections = ["left", "center", "right"]
// -----------------
// 1st. check our settings are not super old, when we only had the widget type as a plain string
for (var s = 0; s < sections.length; s++) {
const sectionName = sections[s]
for (var i = 0; i < adapter.bar.widgets[sectionName].length; i++) {
var widget = adapter.bar.widgets[sectionName][i]
if (typeof widget === "string") {
adapter.bar.widgets[sectionName][i] = {
"id": widget
}
}
}
}
// -----------------
// 2nd. remove any non existing widget type
for (var s = 0; s < sections.length; s++) {
const sectionName = sections[s]
const widgets = adapter.bar.widgets[sectionName]
// Iterate backward through the widgets array, so it does not break when removing a widget
for (var i = widgets.length - 1; i >= 0; i--) {
var widget = widgets[i]
if (!BarWidgetRegistry.hasWidget(widget.id)) {
widgets.splice(i, 1)
Logger.warn(`Settings`, `Deleted invalid widget ${widget.id}`)
}
}
}
// -----------------
// 3nd. migrate global settings to user settings
for (var s = 0; s < sections.length; s++) {
const sectionName = sections[s]
for (var i = 0; i < adapter.bar.widgets[sectionName].length; i++) {
var widget = adapter.bar.widgets[sectionName][i]
// Check if widget registry supports user settings, if it does not, then there is nothing to do
const reg = BarWidgetRegistry.widgetMetadata[widget.id]
if ((reg === undefined) || (reg.allowUserSettings === undefined) || !reg.allowUserSettings) {
continue
}
if (upgradeWidget(widget)) {
Logger.log("Settings", `Upgraded ${widget.id} widget:`, JSON.stringify(widget))
}
}
}
// Upgrade the density of the bar so the look stay the same for people who upgrade.
if (adapter.settingsVersion == 2) {
adapter.bar.density = "comfortable"
adapter.settingsVersion++
}
}
// -----------------------------------------------------
function upgradeWidget(widget) {
// Backup the widget definition before altering
const widgetBefore = JSON.stringify(widget)
switch (widget.id) {
// Get back to global settings for these two clock settings
case "Clock":
if (widget.use12HourClock !== undefined) {
adapter.location.use12hourFormat = widget.use12HourClock
delete widget.use12HourClock
}
if (widget.reverseDayMonth !== undefined) {
adapter.location.monthBeforeDay = widget.reverseDayMonth
delete widget.reverseDayMonth
}
break
}
// Inject missing default setting (metaData) from BarWidgetRegistry
const keys = Object.keys(BarWidgetRegistry.widgetMetadata[widget.id])
for (var i = 0; i < keys.length; i++) {
const k = keys[i]
if (k === "id" || k === "allowUserSettings") {
continue
}
if (widget[k] === undefined) {
widget[k] = BarWidgetRegistry.widgetMetadata[widget.id][k]
}
}
// Compare settings, to detect if something has been upgraded
const widgetAfter = JSON.stringify(widget)
return (widgetAfter !== widgetBefore)
}
// -----------------------------------------------------
// Kickoff essential services
function kickOffServices() {
// Ensure our location singleton is created as soon as possible so we start fetching weather asap
LocationService.init()
NightLightService.apply()
ColorSchemeService.init()
MatugenService.init()
// Ensure wallpapers are restored after settings have been loaded
WallpaperService.init()
FontService.init()
HooksService.init()
BluetoothService.init()
}
// -----------------------------------------------------
// Ensure directories exist before FileView tries to read files
Component.onCompleted: {
// ensure settings dir exists
Quickshell.execDetached(["mkdir", "-p", configDir])
Quickshell.execDetached(["mkdir", "-p", cacheDir])
Quickshell.execDetached(["mkdir", "-p", cacheDirImages])
Quickshell.execDetached(["mkdir", "-p", cacheDirImagesWallpapers])
Quickshell.execDetached(["mkdir", "-p", cacheDirImagesNotifications])
// Mark directories as created and trigger file loading
directoriesCreated = true
// This should only be activated once when the settings structure has changed
// Then it should be commented out again, regular users don't need to generate
// default settings on every start
// TODO: automate this someday!
//generateDefaultSettings()
// Patch-in the local default, resolved to user's home
adapter.general.avatarImage = defaultAvatar
adapter.screenRecorder.directory = defaultVideosDirectory
adapter.wallpaper.directory = defaultWallpapersDirectory
// Set the adapter to the settingsFileView to trigger the real settings load
settingsFileView.adapter = adapter
}
// Don't write settings to disk immediately
@@ -244,213 +110,400 @@ Singleton {
// File doesn't exist, create it with default values
writeAdapter()
}
}
JsonAdapter {
id: adapter
JsonAdapter {
id: adapter
property int settingsVersion: 6
property int settingsVersion: 3
// bar
property JsonObject bar: JsonObject {
property string position: "top" // "top", "bottom", "left", or "right"
property real backgroundOpacity: 1.0
property list<string> monitors: []
property string density: "default" // "compact", "default", "comfortable"
property bool showCapsule: true
// bar
property JsonObject bar: JsonObject {
property string position: "top" // "top", "bottom", "left", or "right"
property real backgroundOpacity: 1.0
property list<string> monitors: []
property string density: "default" // "compact", "default", "comfortable"
property bool showCapsule: true
// Floating bar settings
property bool floating: false
property real marginVertical: 0.25
property real marginHorizontal: 0.25
// Floating bar settings
property bool floating: false
property real marginVertical: 0.25
property real marginHorizontal: 0.25
// Widget configuration for modular bar system
property JsonObject widgets
widgets: JsonObject {
property list<var> left: [{
"id": "SystemMonitor"
}, {
"id": "ActiveWindow"
}, {
"id": "MediaMini"
}]
property list<var> center: [{
"id": "Workspace"
}]
property list<var> right: [{
"id": "ScreenRecorderIndicator"
}, {
"id": "Tray"
}, {
"id": "NotificationHistory"
}, {
"id": "WiFi"
}, {
"id": "Bluetooth"
}, {
"id": "Battery"
}, {
"id": "Volume"
}, {
"id": "Brightness"
}, {
"id": "NightLight"
}, {
"id": "Clock"
}, {
"id": "SidePanelToggle"
}]
}
}
// general
property JsonObject general: JsonObject {
property string avatarImage: defaultAvatar
property bool dimDesktop: true
property bool showScreenCorners: false
property bool forceBlackScreenCorners: false
property real radiusRatio: 1.0
property real screenRadiusRatio: 1.0
property real animationSpeed: 1.0
}
// location
property JsonObject location: JsonObject {
property string name: defaultLocation
property bool useFahrenheit: false
property bool use12hourFormat: false
property bool monthBeforeDay: false
property bool showWeekNumberInCalendar: false
}
// screen recorder
property JsonObject screenRecorder: JsonObject {
property string directory: defaultVideosDirectory
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
property string audioSource: "default_output"
property string videoSource: "portal"
}
// wallpaper
property JsonObject wallpaper: JsonObject {
property bool enabled: true
property string directory: defaultWallpapersDirectory
property bool enableMultiMonitorDirectories: false
property bool setWallpaperOnAllMonitors: true
property string fillMode: "crop"
property color fillColor: "#000000"
property bool randomEnabled: false
property int randomIntervalSec: 300 // 5 min
property int transitionDuration: 1500 // 1500 ms
property string transitionType: "random"
property real transitionEdgeSmoothness: 0.05
property list<var> monitors: []
}
// applauncher
property JsonObject appLauncher: JsonObject {
property bool enableClipboardHistory: false
// Position: center, top_left, top_right, bottom_left, bottom_right, bottom_center, top_center
property string position: "center"
property real backgroundOpacity: 1.0
property list<string> pinnedExecs: []
property bool useApp2Unit: false
property bool sortByMostUsed: true
}
// dock
property JsonObject dock: JsonObject {
property bool autoHide: false
property bool exclusive: false
property real backgroundOpacity: 1.0
property real floatingRatio: 1.0
property list<string> monitors: []
}
// network
property JsonObject network: JsonObject {
property bool wifiEnabled: true
property bool bluetoothEnabled: true
}
// notifications
property JsonObject notifications: JsonObject {
property bool doNotDisturb: false
property list<string> monitors: []
// Last time the user opened the notification history (ms since e899999999999998poch)
property real lastSeenTs: 0
// Duration settings for different urgency levels (in seconds)
property int lowUrgencyDuration: 3
property int normalUrgencyDuration: 8
property int criticalUrgencyDuration: 15
}
// audio
property JsonObject audio: JsonObject {
property int volumeStep: 5
property int cavaFrameRate: 60
property string visualizerType: "linear"
property list<string> mprisBlacklist: []
property string preferredPlayer: ""
}
// ui
property JsonObject ui: JsonObject {
property string fontDefault: "Roboto"
property string fontFixed: "DejaVu Sans Mono"
property string fontBillboard: "Inter"
property list<var> monitorsScaling: []
property bool idleInhibitorEnabled: false
}
// brightness
property JsonObject brightness: JsonObject {
property int brightnessStep: 5
}
property JsonObject colorSchemes: JsonObject {
property bool useWallpaperColors: false
property string predefinedScheme: ""
property bool darkMode: true
}
// matugen templates toggles
property JsonObject matugen: JsonObject {
// Per-template flags to control dynamic config generation
property bool gtk4: false
property bool gtk3: false
property bool qt6: false
property bool qt5: false
property bool kitty: false
property bool ghostty: false
property bool foot: false
property bool fuzzel: false
property bool vesktop: false
property bool pywalfox: false
property bool enableUserTemplates: false
}
// night light
property JsonObject nightLight: JsonObject {
property bool enabled: false
property bool forced: false
property bool autoSchedule: true
property string nightTemp: "4000"
property string dayTemp: "6500"
property string manualSunrise: "06:30"
property string manualSunset: "18:30"
}
// hooks
property JsonObject hooks: JsonObject {
property bool enabled: false
property string wallpaperChange: ""
property string darkModeChange: ""
// Widget configuration for modular bar system
property JsonObject widgets
widgets: JsonObject {
property list<var> left: [{
"id": "SystemMonitor"
}, {
"id": "ActiveWindow"
}, {
"id": "MediaMini"
}]
property list<var> center: [{
"id": "Workspace"
}]
property list<var> right: [{
"id": "ScreenRecorder"
}, {
"id": "Tray"
}, {
"id": "NotificationHistory"
}, {
"id": "WiFi"
}, {
"id": "Bluetooth"
}, {
"id": "Battery"
}, {
"id": "Volume"
}, {
"id": "Brightness"
}, {
"id": "NightLight"
}, {
"id": "Clock"
}, {
"id": "ControlCenter"
}]
}
}
// general
property JsonObject general: JsonObject {
property string avatarImage: ""
property bool dimDesktop: true
property bool showScreenCorners: false
property bool forceBlackScreenCorners: false
property real radiusRatio: 1.0
property real screenRadiusRatio: 1.0
property real animationSpeed: 1.0
}
// location
property JsonObject location: JsonObject {
property string name: defaultLocation
property bool useFahrenheit: false
property bool use12hourFormat: false
property bool showWeekNumberInCalendar: false
}
// screen recorder
property JsonObject screenRecorder: JsonObject {
property string directory: ""
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
property string audioSource: "default_output"
property string videoSource: "portal"
}
// wallpaper
property JsonObject wallpaper: JsonObject {
property bool enabled: true
property string directory: ""
property bool enableMultiMonitorDirectories: false
property bool setWallpaperOnAllMonitors: true
property string fillMode: "crop"
property color fillColor: "#000000"
property bool randomEnabled: false
property int randomIntervalSec: 300 // 5 min
property int transitionDuration: 1500 // 1500 ms
property string transitionType: "random"
property real transitionEdgeSmoothness: 0.05
property list<var> monitors: []
}
// applauncher
property JsonObject appLauncher: JsonObject {
property bool enableClipboardHistory: false
// Position: center, top_left, top_right, bottom_left, bottom_right, bottom_center, top_center
property string position: "center"
property real backgroundOpacity: 1.0
property list<string> pinnedExecs: []
property bool useApp2Unit: false
property bool sortByMostUsed: true
}
// dock
property JsonObject dock: JsonObject {
property bool autoHide: false
property bool exclusive: false
property real backgroundOpacity: 1.0
property real floatingRatio: 1.0
property list<string> monitors: []
// Desktop entry IDs pinned to the dock (e.g., "org.kde.konsole", "firefox.desktop")
property list<string> pinnedApps: []
}
// network
property JsonObject network: JsonObject {
property bool wifiEnabled: true
property bool bluetoothEnabled: true
}
// notifications
property JsonObject notifications: JsonObject {
property bool doNotDisturb: false
property list<string> monitors: []
property string location: "top_right"
property bool alwaysOnTop: false
property real lastSeenTs: 0
property bool respectExpireTimeout: false
property int lowUrgencyDuration: 3
property int normalUrgencyDuration: 8
property int criticalUrgencyDuration: 15
property bool enableOSD: true
}
// audio
property JsonObject audio: JsonObject {
property int volumeStep: 5
property bool volumeOverdrive: false
property int cavaFrameRate: 60
property string visualizerType: "linear"
property list<string> mprisBlacklist: []
property string preferredPlayer: ""
}
// ui
property JsonObject ui: JsonObject {
property string fontDefault: "Roboto"
property string fontFixed: "DejaVu Sans Mono"
property string fontBillboard: "Inter"
property list<var> monitorsScaling: []
property bool idleInhibitorEnabled: false
}
// brightness
property JsonObject brightness: JsonObject {
property int brightnessStep: 5
}
property JsonObject colorSchemes: JsonObject {
property bool useWallpaperColors: false
property string predefinedScheme: "Noctalia (default)"
property bool darkMode: true
}
// matugen templates toggles
property JsonObject matugen: JsonObject {
// Per-template flags to control dynamic config generation
property bool gtk4: false
property bool gtk3: false
property bool qt6: false
property bool qt5: false
property bool kitty: false
property bool ghostty: false
property bool foot: false
property bool fuzzel: false
property bool vesktop: false
property bool pywalfox: false
property bool enableUserTemplates: false
}
// night light
property JsonObject nightLight: JsonObject {
property bool enabled: false
property bool forced: false
property bool autoSchedule: true
property string nightTemp: "4000"
property string dayTemp: "6500"
property string manualSunrise: "06:30"
property string manualSunset: "18:30"
}
// hooks
property JsonObject hooks: JsonObject {
property bool enabled: false
property string wallpaperChange: ""
property string darkModeChange: ""
}
}
// -----------------------------------------------------
// Generate default settings at the root of the repo
function generateDefaultSettings() {
try {
Logger.log("Settings", "Generating settings-default.json")
// Prepare a clean JSON
var plainAdapter = QtObj2JS.qtObjectToPlainObject(adapter)
var jsonData = JSON.stringify(plainAdapter, null, 2)
var defaultPath = Quickshell.shellDir + "/Assets/settings-default.json"
// Encode transfer it has base64 to avoid any escaping issue
var base64Data = Qt.btoa(jsonData)
Quickshell.execDetached(["sh", "-c", `echo "${base64Data}" | base64 -d > "${defaultPath}"`])
} catch (error) {
Logger.error("Settings", "Failed to generate default settings file: " + error)
}
}
// -----------------------------------------------------
// Function to validate monitor configurations
function validateMonitorConfigurations() {
var availableScreenNames = []
for (var i = 0; i < Quickshell.screens.length; i++) {
availableScreenNames.push(Quickshell.screens[i].name)
}
Logger.log("Settings", "Available monitors: [" + availableScreenNames.join(", ") + "]")
Logger.log("Settings", "Configured bar monitors: [" + adapter.bar.monitors.join(", ") + "]")
// Check bar monitors
if (adapter.bar.monitors.length > 0) {
var hasValidBarMonitor = false
for (var j = 0; j < adapter.bar.monitors.length; j++) {
if (availableScreenNames.includes(adapter.bar.monitors[j])) {
hasValidBarMonitor = true
break
}
}
if (!hasValidBarMonitor) {
Logger.warn("Settings", "No configured bar monitors found on system, clearing bar monitor list to show on all screens")
adapter.bar.monitors = []
} else {
//Logger.log("Settings", "Found valid bar monitors, keeping configuration")
}
} else {
//Logger.log("Settings", "Bar monitor list is empty, will show on all available screens")
}
}
// -----------------------------------------------------
// If the settings structure has changed, ensure
// backward compatibility by upgrading the settings
function upgradeSettingsData() {
const sections = ["left", "center", "right"]
// -----------------
// 1st. convert old widget id to new id
for (var s = 0; s < sections.length; s++) {
const sectionName = sections[s]
for (var i = 0; i < adapter.bar.widgets[sectionName].length; i++) {
var widget = adapter.bar.widgets[sectionName][i]
switch (widget.id) {
case "DarkModeToggle":
widget.id = "DarkMode"
break
case "PowerToggle":
widget.id = "SessionMenu"
break
case "ScreenRecorderIndicator":
widget.id = "ScreenRecorder"
break
case "SidePanelToggle":
widget.id = "ControlCenter"
break
}
}
}
// -----------------
// 2nd. remove any non existing widget type
for (var s = 0; s < sections.length; s++) {
const sectionName = sections[s]
const widgets = adapter.bar.widgets[sectionName]
// Iterate backward through the widgets array, so it does not break when removing a widget
for (var i = widgets.length - 1; i >= 0; i--) {
var widget = widgets[i]
if (!BarWidgetRegistry.hasWidget(widget.id)) {
Logger.warn(`Settings`, `Deleted invalid widget ${widget.id}`)
widgets.splice(i, 1)
}
}
}
// -----------------
// 3nd. migrate global settings to user settings
for (var s = 0; s < sections.length; s++) {
const sectionName = sections[s]
for (var i = 0; i < adapter.bar.widgets[sectionName].length; i++) {
var widget = adapter.bar.widgets[sectionName][i]
// Check if widget registry supports user settings, if it does not, then there is nothing to do
const reg = BarWidgetRegistry.widgetMetadata[widget.id]
if ((reg === undefined) || (reg.allowUserSettings === undefined) || !reg.allowUserSettings) {
continue
}
if (upgradeWidget(widget)) {
Logger.log("Settings", `Upgraded ${widget.id} widget:`, JSON.stringify(widget))
}
}
}
// Upgrade the density of the bar so the look stay the same for people who upgrade.
// TODO: remove soon
if (adapter.settingsVersion == 2) {
adapter.bar.density = "comfortable"
adapter.settingsVersion++
}
}
// -----------------------------------------------------
function upgradeWidget(widget) {
// Backup the widget definition before altering
const widgetBefore = JSON.stringify(widget)
switch (widget.id) {
// Get back to global settings for these two clock settings
case "Clock":
if (widget.use12HourClock !== undefined) {
adapter.location.use12hourFormat = widget.use12HourClock
delete widget.use12HourClock
}
break
}
// Get all existing custom settings keys
const keys = Object.keys(BarWidgetRegistry.widgetMetadata[widget.id])
// Delete deprecated user settings from the wiget
for (const k of Object.keys(widget)) {
if (k === "id" || k === "allowUserSettings") {
continue
}
if (!keys.includes(k)) {
delete widget[k]
}
}
// Inject missing default setting (metaData) from BarWidgetRegistry
for (var i = 0; i < keys.length; i++) {
const k = keys[i]
if (k === "id" || k === "allowUserSettings") {
continue
}
if (widget[k] === undefined) {
widget[k] = BarWidgetRegistry.widgetMetadata[widget.id][k]
}
}
// Compare settings, to detect if something has been upgraded
const widgetAfter = JSON.stringify(widget)
return (widgetAfter !== widgetBefore)
}
// -----------------------------------------------------
// Kickoff essential services
function kickOffServices() {
LocationService.init()
NightLightService.apply()
ColorSchemeService.init()
MatugenService.init()
WallpaperService.init()
FontService.init()
HooksService.init()
BluetoothService.init()
}
}
@@ -56,6 +56,9 @@ Singleton {
"keep-awake-on": "mug",
"keep-awake-off": "mug-off",
"disc": "disc-filled",
"eye": "eye",
"pin": "pin",
"unpin": "pinned-off",
"image": "photo",
"dark-mode": "contrast-filled",
"camera-video": "video",
@@ -3471,6 +3474,7 @@ Singleton {
"http-que-off": "\u{100df}",
"http-trace": "\u{fa30}",
"http-trace-off": "\u{100de}",
"hyprland": "\u{ec6a}",
"ice-cream": "\u{eac2}",
"ice-cream-2": "\u{ee9f}",
"ice-cream-off": "\u{f148}",
@@ -4303,6 +4307,7 @@ Singleton {
"news-off": "\u{f167}",
"nfc": "\u{eeb7}",
"nfc-off": "\u{f168}",
"niri": "\u{ec32}",
"noctalia": "\u{ec33}",
"no-copyright": "\u{efb9}",
"no-creative-commons": "\u{efba}",
+58 -74
View File
@@ -8,6 +8,7 @@ import qs.Services
Singleton {
id: root
// Current date
property var date: new Date()
// Returns a Unix Timestamp (in seconds)
@@ -15,90 +16,73 @@ Singleton {
return Math.floor(date / 1000)
}
function formatDate(monthBeforeDay = true) {
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}, ` + (monthBeforeDay ? `${month} ${day}${suffix} ${year}` : `${day}${suffix} ${month} ${year}`)
Timer {
interval: 1000
repeat: true
running: true
onTriggered: root.date = new Date()
}
// Formats a Date object into a YYYYMMDD-HHMMSS string.
function getFormattedTimestamp(date) {
if (!date) {
date = new Date()
}
const year = date.getFullYear()
/**
* 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()
// 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')
// 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')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
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, computer uptime, etc..
function formatVagueHumanReadableDuration(totalSeconds) {
if (typeof totalSeconds !== 'number' || totalSeconds < 0) {
return '0s'
return `${year}${month}${day}-${hours}${minutes}${seconds}`
}
// Floor the input to handle decimal seconds
totalSeconds = Math.floor(totalSeconds)
// Format an easy to read approximate duration ex: 4h32m
// Used to display the time remaining on the Battery widget, computer uptime, etc..
function formatVagueHumanReadableDuration(totalSeconds) {
if (typeof totalSeconds !== 'number' || totalSeconds < 0) {
return '0s'
}
const days = Math.floor(totalSeconds / 86400)
const hours = Math.floor((totalSeconds % 86400) / 3600)
const minutes = Math.floor((totalSeconds % 3600) / 60)
const seconds = totalSeconds % 60
// Floor the input to handle decimal seconds
totalSeconds = Math.floor(totalSeconds)
const parts = []
if (days)
parts.push(`${days}d`)
if (hours)
parts.push(`${hours}h`)
if (minutes)
parts.push(`${minutes}m`)
const days = Math.floor(totalSeconds / 86400)
const hours = Math.floor((totalSeconds % 86400) / 3600)
const minutes = Math.floor((totalSeconds % 3600) / 60)
const seconds = totalSeconds % 60
// Only show seconds if no hours and no minutes
if (!hours && !minutes) {
parts.push(`${seconds}s`)
const parts = []
if (days)
parts.push(`${days}d`)
if (hours)
parts.push(`${hours}h`)
if (minutes)
parts.push(`${minutes}m`)
// Only show seconds if no hours and no minutes
if (!hours && !minutes) {
parts.push(`${seconds}s`)
}
return parts.join('')
}
return parts.join('')
}
Timer {
interval: 1000
repeat: true
running: true
onTriggered: root.date = new Date()
}
// Format a date into
function formatRelativeTime(date) {
if (!date)
return ""
const diff = Date.now() - date.getTime()
if (diff < 60000)
return "now"
if (diff < 3600000)
return `${Math.floor(diff / 60000)}m ago`
if (diff < 86400000)
return `${Math.floor(diff / 3600000)}h ago`
return `${Math.floor(diff / 86400000)}d ago`
}
}
+114
View File
@@ -0,0 +1,114 @@
// -----------------------------------------------------
// Helper function to convert Qt objects to plain JavaScript objects
// Only used when generating settings-default.json
function qtObjectToPlainObject(obj) {
if (obj === null || obj === undefined) {
return obj;
}
// Handle primitive types
if (typeof obj !== "object") {
return obj;
}
// Handle native JavaScript arrays
if (Array.isArray(obj)) {
return obj.map((item) => qtObjectToPlainObject(item));
}
// Detect QML arrays FIRST (before color detection)
// QML arrays have a numeric length property and indexed properties
if (typeof obj.length === "number" && obj.length >= 0) {
// Check if it has indexed properties - be more flexible about detection
var hasIndexedProps = true;
var hasNumericKeys = false;
// Check if we have at least some numeric properties
for (var i = 0; i < obj.length; i++) {
if (obj.hasOwnProperty(i) || obj[i] !== undefined) {
hasNumericKeys = true;
break;
}
}
// If we have length > 0 and some numeric keys, treat as array
if (obj.length > 0 && hasNumericKeys) {
var arr = [];
for (var i = 0; i < obj.length; i++) {
// Use direct property access, handle undefined gracefully
var item = obj[i];
if (item !== undefined) {
arr.push(qtObjectToPlainObject(item));
}
}
return arr; // Return here to avoid processing as object
}
// Handle empty arrays (length = 0)
if (obj.length === 0) {
return [];
}
}
// Detect and convert QML color objects to hex strings
if (
typeof obj.r === "number" &&
typeof obj.g === "number" &&
typeof obj.b === "number" &&
typeof obj.a === "number" &&
typeof obj.valid === "boolean"
) {
// This looks like a QML color object
try {
// Try to get the string representation (should be hex like "#000000")
if (typeof obj.toString === "function") {
return obj.toString();
} else {
// Fallback: convert RGBA to hex manually
var r = Math.round(obj.r * 255);
var g = Math.round(obj.g * 255);
var b = Math.round(obj.b * 255);
var hex =
"#" +
r.toString(16).padStart(2, "0") +
g.toString(16).padStart(2, "0") +
b.toString(16).padStart(2, "0");
return hex;
}
} catch (e) {
// If conversion fails, fall through to regular object handling
}
}
// Handle regular objects
var plainObj = {};
// Get all property names, but filter out Qt-specific ones
var propertyNames = Object.getOwnPropertyNames(obj);
for (var i = 0; i < propertyNames.length; i++) {
var propName = propertyNames[i];
// Skip Qt-specific properties, functions, and array-like properties
if (
propName === "objectName" ||
propName === "objectNameChanged" ||
propName === "length" || // Skip length property
/^\d+$/.test(propName) || // Skip numeric keys (0, 1, 2, etc.)
propName.endsWith("Changed") ||
typeof obj[propName] === "function"
) {
continue;
}
try {
var value = obj[propName];
plainObj[propName] = qtObjectToPlainObject(value);
} catch (e) {
// Skip properties that can't be accessed
continue;
}
}
return plainObj;
}
-1
View File
@@ -3,7 +3,6 @@ import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Modules.SettingsPanel
import qs.Widgets
Variants {
+12 -43
View File
@@ -28,7 +28,7 @@ Variants {
}
}
active: Settings.isLoaded && modelData && modelData.name ? (Settings.data.bar.monitors.includes(modelData.name) || (Settings.data.bar.monitors.length === 0)) : false
active: Settings.isLoaded && BarService.isVisible && modelData && modelData.name ? (Settings.data.bar.monitors.includes(modelData.name) || (Settings.data.bar.monitors.length === 0)) : false
sourceComponent: PanelWindow {
screen: modelData || null
@@ -47,45 +47,14 @@ Variants {
}
// Floating bar margins - only apply when floating is enabled
// Also don't apply margin on the opposite side ot the bar orientation, ex: if bar is floating on top, margin is only applied on top, not bottom.
margins {
top: Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0
bottom: Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0
left: Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0
right: Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0
top: Settings.data.bar.floating && Settings.data.bar.position !== "bottom" ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0
bottom: Settings.data.bar.floating && Settings.data.bar.position !== "top" ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0
left: Settings.data.bar.floating && Settings.data.bar.position !== "right" ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0
right: Settings.data.bar.floating && Settings.data.bar.position !== "left" ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0
}
// Filter out potentially invisible widgets so they we don't have extra spacing around an invisble widget.
// TODO: add activewindow
property var visibleLeftWidgets: {
if (!Settings.data.bar.widgets.left) return []
return Settings.data.bar.widgets.left.filter(widget => {
if (widget.id === "ScreenRecorderIndicator") {
return ScreenRecorderService.isRecording
}
return true
})
}
property var visibleCenterWidgets: {
if (!Settings.data.bar.widgets.center) return []
return Settings.data.bar.widgets.center.filter(widget => {
if (widget.id === "ScreenRecorderIndicator") {
return ScreenRecorderService.isRecording
}
return true
})
}
property var visibleRightWidgets: {
if (!Settings.data.bar.widgets.right) return []
return Settings.data.bar.widgets.right.filter(widget => {
if (widget.id === "ScreenRecorderIndicator") {
return ScreenRecorderService.isRecording
}
return true
})
}
Item {
anchors.fill: parent
clip: true
@@ -120,7 +89,7 @@ Variants {
spacing: Style.marginS * root.scaling
Repeater {
model: visibleLeftWidgets
model: Settings.data.bar.widgets.left
delegate: BarWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
@@ -143,7 +112,7 @@ Variants {
spacing: Style.marginS * root.scaling
Repeater {
model: visibleCenterWidgets
model: Settings.data.bar.widgets.center
delegate: BarWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
@@ -167,7 +136,7 @@ Variants {
spacing: Style.marginS * root.scaling
Repeater {
model: visibleRightWidgets
model: Settings.data.bar.widgets.right
delegate: BarWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
@@ -201,7 +170,7 @@ Variants {
spacing: Style.marginS * root.scaling
Repeater {
model: visibleLeftWidgets
model: Settings.data.bar.widgets.left
delegate: BarWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
@@ -226,7 +195,7 @@ Variants {
spacing: Style.marginS * root.scaling
Repeater {
model: visibleCenterWidgets
model: Settings.data.bar.widgets.center
delegate: BarWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
@@ -252,7 +221,7 @@ Variants {
spacing: Style.marginS * root.scaling
Repeater {
model: visibleRightWidgets
model: Settings.data.bar.widgets.right
delegate: BarWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
@@ -176,46 +176,6 @@ ColumnLayout {
}
}
}
// MouseArea {
// id: availableDeviceArea
// acceptedButtons: Qt.LeftButton | Qt.RightButton
// anchors.fill: parent
// hoverEnabled: true
// cursorShape: (canConnect || canDisconnect)
// && !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor)
// onEntered: {
// if (root.tooltipText && !isBusy) {
// tooltip.show()
// }
// }
// onExited: {
// if (root.tooltipText && !isBusy) {
// tooltip.hide()
// }
// }
// onClicked: function (mouse) {
// if (!modelData || modelData.pairing) {
// return
// }
// if (root.tooltipText && !isBusy) {
// tooltip.hide()
// }
// if (mouse.button === Qt.LeftButton) {
// if (modelData.connected) {
// BluetoothService.disconnectDevice(modelData)
// } else {
// BluetoothService.connectDeviceWithTrust(modelData)
// }
// } else if (mouse.button === Qt.RightButton) {
// BluetoothService.forgetDevice(modelData)
// }
// }
// }
}
}
}
@@ -35,7 +35,7 @@ NPanel {
}
NText {
text: "Bluetooth"
text: I18n.tr("bluetooth.panel.title")
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
@@ -52,7 +52,7 @@ NPanel {
NIconButton {
enabled: Settings.data.network.bluetoothEnabled
icon: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop" : "refresh"
tooltipText: "Refresh Devices"
tooltipText: I18n.tr("tooltips.refresh-devices")
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
if (BluetoothService.adapter) {
@@ -63,7 +63,7 @@ NPanel {
NIconButton {
icon: "close"
tooltipText: "Close."
tooltipText: I18n.tr("tooltips.close")
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
root.close()
@@ -94,14 +94,14 @@ NPanel {
}
NText {
text: "Bluetooth is disabled"
text: I18n.tr("bluetooth.panel.disabled")
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Enable Bluetooth to see available devices."
text: I18n.tr("bluetooth.panel.enable-message")
font.pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
@@ -124,7 +124,7 @@ NPanel {
// Connected devices
BluetoothDevicesList {
label: "Connected devices"
label: I18n.tr("bluetooth.panel.connected-devices")
property var items: {
if (!BluetoothService.adapter || !Bluetooth.devices)
return []
@@ -138,8 +138,8 @@ NPanel {
// Known devices
BluetoothDevicesList {
label: "Known devices"
tooltipText: "Left click to connect.\nRight click to forget."
label: I18n.tr("bluetooth.panel.known-devices")
tooltipText: I18n.tr("tooltips.connect-disconnect-devices")
property var items: {
if (!BluetoothService.adapter || !Bluetooth.devices)
return []
@@ -153,7 +153,7 @@ NPanel {
// Available devices
BluetoothDevicesList {
label: "Available devices"
label: I18n.tr("bluetooth.panel.available-devices")
property var items: {
if (!BluetoothService.adapter || !Bluetooth.devices)
return []
@@ -199,14 +199,14 @@ NPanel {
}
NText {
text: "Scanning for devices..."
text: I18n.tr("bluetooth.panel.scanning")
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
}
}
NText {
text: "Make sure your device is in pairing mode."
text: I18n.tr("bluetooth.panel.pairing-mode")
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
@@ -10,15 +10,18 @@ import qs.Widgets
NPanel {
id: root
preferredWidth: Settings.data.location.showWeekNumberInCalendar ? 350 : 330
preferredHeight: 320
preferredWidth: Settings.data.location.showWeekNumberInCalendar ? 320 : 300
preferredHeight: 300
// Main Column
panelContent: ColumnLayout {
id: content
anchors.fill: parent
anchors.margins: Style.marginM * scaling
spacing: Style.marginXS * scaling
readonly property int firstDayOfWeek: Qt.locale().firstDayOfWeek
// Header: Month/Year with navigation
RowLayout {
Layout.fillWidth: true
@@ -28,7 +31,7 @@ NPanel {
NIconButton {
icon: "chevron-left"
tooltipText: "Previous month"
tooltipText: I18n.tr("tooltips.previous-month")
onClicked: {
let newDate = new Date(grid.year, grid.month - 1, 1)
grid.year = newDate.getFullYear()
@@ -47,7 +50,7 @@ NPanel {
NIconButton {
icon: "chevron-right"
tooltipText: "Next month"
tooltipText: I18n.tr("tooltips.next-month")
onClicked: {
let newDate = new Date(grid.year, grid.month + 1, 1)
grid.year = newDate.getFullYear()
@@ -78,7 +81,7 @@ NPanel {
NText {
anchors.centerIn: parent
text: "Week"
text: I18n.tr("calendar.panel.week")
color: Color.mOutline
font.pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightRegular
@@ -106,9 +109,7 @@ NPanel {
NText {
anchors.centerIn: parent
text: {
// Use the locale's first day of week setting
let firstDay = Qt.locale().firstDayOfWeek
let dayIndex = (firstDay + index) % 7
let dayIndex = (content.firstDayOfWeek + index) % 7
return Qt.locale().dayName(dayIndex, Locale.ShortFormat)
}
color: Color.mSecondary
@@ -130,22 +131,19 @@ NPanel {
spacing: 0
// Week numbers column (only visible when enabled)
GridLayout {
ColumnLayout {
visible: Settings.data.location.showWeekNumberInCalendar
Layout.preferredWidth: visible ? Style.baseWidgetSize * scaling : 0
Layout.fillHeight: true
columns: 1
rows: 6
columnSpacing: 0
rowSpacing: 0
spacing: 0
Repeater {
model: 6 // Maximum 6 weeks in a month view
Rectangle {
Item {
Layout.fillWidth: true
Layout.fillHeight: true
color: Color.transparent
Layout.preferredHeight: Style.baseWidgetSize * scaling
NText {
anchors.centerIn: parent
@@ -153,30 +151,54 @@ NPanel {
font.pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightBold
text: {
// Calculate the first day shown in the calendar grid
let firstDay = new Date(grid.year, grid.month, 1)
let firstDayOfWeek = Qt.locale().firstDayOfWeek
let startOffset = (firstDay.getDay() - firstDayOfWeek + 7) % 7
let gridStartDate = new Date(grid.year, grid.month, 1 - startOffset)
// Calculate the date shown in the first column of this row
// MonthGrid always shows 42 days (6 weeks × 7 days)
// Get the date for the start of this specific row
let rowDate = new Date(gridStartDate)
rowDate.setDate(gridStartDate.getDate() + (index * 7))
// First, find the first day of the month
let firstOfMonth = new Date(grid.year, grid.month, 1)
// Calculate week number based on the Thursday of the visual row
// This correctly handles rows that span two different ISO weeks.
let thursdayOfRow = new Date(rowDate)
let offsetToThursday = (4 - thursdayOfRow.getDay() + 7) % 7
thursdayOfRow.setDate(thursdayOfRow.getDate() + offsetToThursday)
// Calculate how many days before the 1st to start the grid
// This depends on the locale's first day of week
let firstDayOfWeek = content.firstDayOfWeek
let firstOfMonthDayOfWeek = firstOfMonth.getDay()
// Check if this row is visible (contains days from current month)
let rowEndDate = new Date(rowDate)
rowEndDate.setDate(rowDate.getDate() + 6)
// Calculate offset: how many days before the 1st should the grid start?
let daysBeforeFirst = (firstOfMonthDayOfWeek - firstDayOfWeek + 7) % 7
if (rowDate.getMonth() === grid.month || rowEndDate.getMonth() === grid.month || (rowDate.getMonth() < grid.month && rowEndDate.getMonth() > grid.month)) {
return `${getISOWeekNumber(thursdayOfRow)}`
// MonthGrid typically shows the previous month's days to fill the first week
// If the 1st is already on the first day of week, show the previous week
if (daysBeforeFirst === 0) {
daysBeforeFirst = 7
}
return ""
// Calculate the start date of the grid
let gridStartDate = new Date(grid.year, grid.month, 1 - daysBeforeFirst)
// Calculate the date for this specific row (week)
let rowStartDate = new Date(gridStartDate)
rowStartDate.setDate(gridStartDate.getDate() + (index * 7))
// For ISO week numbers, we need to find the Thursday of this week
// ISO 8601 week numbering: week with year's first Thursday is week 1
// The week number is determined by the Thursday
// Find the Thursday of this row's week
// If firstDayOfWeek is Monday (1), Thursday is +3 days
// If firstDayOfWeek is Sunday (0), we need to adjust
let thursday = new Date(rowStartDate)
if (firstDayOfWeek === 0) {
// Sunday start: Thursday is 4 days after Sunday
thursday.setDate(rowStartDate.getDate() + 4)
} else if (firstDayOfWeek === 1) {
// Monday start: Thursday is 3 days after Monday
thursday.setDate(rowStartDate.getDate() + 3)
} else {
// Other start days: calculate offset to Thursday
let daysToThursday = (4 - firstDayOfWeek + 7) % 7
thursday.setDate(rowStartDate.getDate() + daysToThursday)
}
return `${getISOWeekNumber(thursday)}`
}
}
}
@@ -194,24 +216,27 @@ NPanel {
year: Time.date.getFullYear()
locale: Qt.locale()
delegate: Rectangle {
width: Style.baseWidgetSize * scaling
height: Style.baseWidgetSize * scaling
radius: Style.radiusS * scaling
color: model.today ? Color.mPrimary : Color.transparent
delegate: Item {
NText {
anchors.centerIn: parent
text: model.day
color: model.today ? Color.mOnPrimary : Color.mOnSurface
opacity: model.month === grid.month ? Style.opacityHeavy : Style.opacityLight
font.pointSize: Style.fontSizeM * scaling
font.weight: model.today ? Style.fontWeightBold : Style.fontWeightRegular
}
Rectangle {
width: Style.baseWidgetSize * scaling
height: Style.baseWidgetSize * scaling
radius: width / 2
color: model.today ? Color.mPrimary : Color.transparent
Behavior on color {
ColorAnimation {
duration: Style.animationFast
NText {
anchors.centerIn: parent
text: model.day
color: model.today ? Color.mOnPrimary : Color.mOnSurface
opacity: model.month === grid.month ? Style.opacityHeavy : Style.opacityLight
font.pointSize: Style.fontSizeM * scaling
font.weight: model.today ? Style.fontWeightBold : Style.fontWeightRegular
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
}
@@ -219,21 +244,27 @@ NPanel {
}
}
// ISO 8601 week number calculation
// This is locale-independent and always uses Monday as first day of week
function getISOWeekNumber(date) {
// Create a copy of the date and normalize to noon to prevent DST issues
const targetDate = new Date(date.getTime())
targetDate.setHours(12, 0, 0, 0)
// Create a copy and set to nearest Thursday (current date + 4 - current day number)
// ISO week starts on Monday (1) to Sunday (7)
const target = new Date(date.getTime())
target.setHours(0, 0, 0, 0)
// Roll the date to the Thursday of the week.
// getDay() is 0 for Sunday, we want Monday to be 1 and Sunday to be 7.
const dayOfWeek = targetDate.getDay() || 7
targetDate.setDate(targetDate.getDate() - dayOfWeek + 4)
// Get day of week where Monday = 1, Sunday = 7
const dayOfWeek = target.getDay() || 7
// Get the first day of that Thursday's year
const yearStart = new Date(targetDate.getFullYear(), 0, 1)
// Set to nearest Thursday (which determines the week number)
target.setDate(target.getDate() + 4 - dayOfWeek)
// Calculate the difference in days and find the week number
const dayOfYear = ((targetDate - yearStart) / 86400000) + 1
return Math.ceil(dayOfYear / 7)
// Get first day of year
const yearStart = new Date(target.getFullYear(), 0, 1)
// Calculate full weeks between yearStart and target
// Add 1 because we're counting weeks, not week differences
const weekNumber = Math.ceil(((target - yearStart) / 86400000 + 1) / 7)
return weekNumber
}
}
+3 -3
View File
@@ -20,7 +20,7 @@ Item {
property bool compact: false
// Effective shown state (true if hovered/animated open or forced)
readonly property bool revealed: forceOpen || showPill
readonly property bool revealed: !forceClose && (forceOpen || showPill)
signal shown
signal hidden
@@ -221,7 +221,7 @@ Item {
hovered = true
root.entered()
tooltip.show()
if (disableOpen) {
if (disableOpen || forceClose) {
return
}
if (!forceOpen) {
@@ -231,7 +231,7 @@ Item {
onExited: {
hovered = false
root.exited()
if (!forceOpen) {
if (!forceOpen && !forceClose) {
hide()
}
tooltip.hide()
@@ -40,7 +40,7 @@ NPanel {
}
NText {
text: "Wi-Fi"
text: I18n.tr("wifi.panel.title")
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
@@ -56,7 +56,7 @@ NPanel {
NIconButton {
icon: "refresh"
tooltipText: "Refresh"
tooltipText: I18n.tr("tooltips.refresh")
baseSize: Style.baseWidgetSize * 0.8
enabled: Settings.data.network.wifiEnabled && !NetworkService.scanning
onClicked: NetworkService.scan()
@@ -64,7 +64,7 @@ NPanel {
NIconButton {
icon: "close"
tooltipText: "Close."
tooltipText: I18n.tr("tooltips.close")
baseSize: Style.baseWidgetSize * 0.8
onClicked: root.close()
}
@@ -136,14 +136,14 @@ NPanel {
}
NText {
text: "Wi-Fi is disabled"
text: I18n.tr("wifi.panel.disabled")
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Enable Wi-Fi to see available networks."
text: I18n.tr("wifi.panel.enable-message")
font.pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
@@ -172,7 +172,7 @@ NPanel {
}
NText {
text: "Searching for nearby networks..."
text: I18n.tr("wifi.panel.searching")
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
@@ -263,7 +263,9 @@ NPanel {
spacing: Style.marginXS * scaling
NText {
text: `${modelData.signal}%`
text: I18n.tr("system.signal-strength", {
"signal": modelData.signal
})
font.pointSize: Style.fontSizeXXS * scaling
color: Color.mOnSurfaceVariant
}
@@ -295,7 +297,7 @@ NPanel {
NText {
id: connectedText
anchors.centerIn: parent
text: "Connected"
text: I18n.tr("wifi.panel.connected")
font.pointSize: Style.fontSizeXXS * scaling
color: Color.mOnPrimary
}
@@ -311,7 +313,7 @@ NPanel {
NText {
id: disconnectingText
anchors.centerIn: parent
text: "Disconnecting..."
text: I18n.tr("wifi.panel.disconnecting")
font.pointSize: Style.fontSizeXXS * scaling
color: Color.mOnPrimary
}
@@ -327,7 +329,7 @@ NPanel {
NText {
id: forgettingText
anchors.centerIn: parent
text: "Forgetting..."
text: I18n.tr("wifi.panel.forgetting")
font.pointSize: Style.fontSizeXXS * scaling
color: Color.mOnPrimary
}
@@ -345,7 +347,7 @@ NPanel {
NText {
id: savedText
anchors.centerIn: parent
text: "Saved"
text: I18n.tr("wifi.panel.saved")
font.pointSize: Style.fontSizeXXS * scaling
color: Color.mOnSurfaceVariant
}
@@ -367,7 +369,7 @@ NPanel {
NIconButton {
visible: (modelData.existing || modelData.cached) && !modelData.connected && NetworkService.connectingTo !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
icon: "trash"
tooltipText: "Forget network"
tooltipText: I18n.tr("tooltips.forget-network")
baseSize: Style.baseWidgetSize * 0.8
onClicked: expandedSsid = expandedSsid === modelData.ssid ? "" : modelData.ssid
}
@@ -376,10 +378,10 @@ NPanel {
visible: !modelData.connected && NetworkService.connectingTo !== modelData.ssid && passwordSsid !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
text: {
if (modelData.existing || modelData.cached)
return "Connect"
return I18n.tr("wifi.panel.connect")
if (!NetworkService.isSecured(modelData.security))
return "Connect"
return "Password"
return I18n.tr("wifi.panel.connect")
return I18n.tr("wifi.panel.password")
}
outlined: !hovered
fontSize: Style.fontSizeXS * scaling
@@ -397,7 +399,7 @@ NPanel {
NButton {
visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid
text: "Disconnect"
text: I18n.tr("wifi.panel.disconnect")
outlined: !hovered
fontSize: Style.fontSizeXS * scaling
backgroundColor: Color.mError
@@ -457,7 +459,7 @@ NPanel {
Text {
visible: parent.text.length === 0
anchors.verticalCenter: parent.verticalCenter
text: "Enter password..."
text: I18n.tr("wifi.panel.enter-password")
color: Color.mOnSurfaceVariant
font.pointSize: Style.fontSizeS * scaling
}
@@ -465,7 +467,7 @@ NPanel {
}
NButton {
text: "Connect"
text: I18n.tr("wifi.panel.connect")
fontSize: Style.fontSizeXXS * scaling
enabled: passwordInput.length > 0 && !NetworkService.connecting
outlined: true
@@ -511,7 +513,7 @@ NPanel {
}
NText {
text: "Forget this network?"
text: I18n.tr("wifi.panel.forget-network")
font.pointSize: Style.fontSizeS * scaling
color: Color.mError
Layout.fillWidth: true
@@ -520,7 +522,7 @@ NPanel {
NButton {
id: forgetButton
text: "Forget"
text: I18n.tr("wifi.panel.forget")
fontSize: Style.fontSizeXXS * scaling
backgroundColor: Color.mError
outlined: forgetButton.hovered ? false : true
@@ -561,14 +563,14 @@ NPanel {
}
NText {
text: "No networks found"
text: I18n.tr("wifi.panel.no-networks")
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NButton {
text: "Scan again"
text: I18n.tr("wifi.panel.scan-again")
icon: "refresh"
Layout.alignment: Qt.AlignHCenter
onClicked: NetworkService.scan()
+194 -124
View File
@@ -30,60 +30,34 @@ Item {
return {}
}
readonly property bool showIcon: (widgetSettings.showIcon !== undefined) ? widgetSettings.showIcon : widgetMetadata.showIcon
// 6% of total width
readonly property real minWidth: Math.max(1, screen.width * 0.06)
readonly property real maxWidth: minWidth * 2
readonly property bool hasActiveWindow: CompositorService.getFocusedWindowTitle() !== ""
readonly property string windowTitle: CompositorService.getFocusedWindowTitle() || "No active window"
readonly property string fallbackIcon: "user-desktop"
readonly property string barPosition: Settings.data.bar.position
readonly property bool isVertical: barPosition === "left" || barPosition === "right"
readonly property bool compact: (Settings.data.bar.density === "compact")
implicitHeight: (barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling)
implicitWidth: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * 0.8 * scaling) : (horizontalLayout.implicitWidth + Style.marginM * 2 * scaling)
// Widget settings - matching MediaMini pattern
readonly property bool showIcon: (widgetSettings.showIcon !== undefined) ? widgetSettings.showIcon : widgetMetadata.showIcon
readonly property bool autoHide: (widgetSettings.autoHide !== undefined) ? widgetSettings.autoHide : widgetMetadata.autoHide
readonly property string scrollingMode: (widgetSettings.scrollingMode !== undefined) ? widgetSettings.scrollingMode : (widgetMetadata.scrollingMode !== undefined ? widgetMetadata.scrollingMode : "hover")
readonly property real textSize: {
var base = isVertical ? width : height
return Math.max(1, compact ? base * 0.43 : base * 0.33)
}
// Fixed width
readonly property real widgetWidth: Math.max(1, screen.width * 0.06)
readonly property real iconSize: textSize * 1.25
implicitHeight: visible ? ((barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling)) : 0
implicitWidth: visible ? ((barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : (widgetWidth * scaling)) : 0
function getTitle() {
try {
return CompositorService.focusedWindowTitle !== "(No active window)" ? CompositorService.focusedWindowTitle : ""
} catch (e) {
Logger.warn("ActiveWindow", "Error getting title:", e)
return ""
opacity: !autoHide || hasActiveWindow ? 1.0 : 0
Behavior on opacity {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutCubic
}
}
visible: getTitle() !== ""
function calculatedVerticalHeight() {
// Use standard widget height like other widgets
return Math.round(Style.capsuleHeight * scaling)
}
function calculatedHorizontalWidth() {
let total = Style.marginM * 2 * scaling // internal padding
if (showIcon) {
total += Style.capsuleHeight * 0.5 * scaling + 2 * scaling // icon + spacing
}
// Calculate actual text width more accurately
const title = getTitle()
if (title !== "") {
// Estimate text width: average character width * number of characters
const avgCharWidth = Style.fontSizeS * scaling * 0.6 // rough estimate
const titleWidth = Math.min(title.length * avgCharWidth, 80 * scaling)
total += titleWidth
}
// Row layout handles spacing between widgets
return Math.max(total, Style.capsuleHeight * scaling) // Minimum width
return Math.round(Style.baseWidgetSize * 0.8 * scaling)
}
function getAppIcon() {
@@ -94,7 +68,7 @@ Item {
try {
const idValue = focusedWindow.appId
const normalizedId = (typeof idValue === 'string') ? idValue : String(idValue)
const iconResult = AppIcons.iconForAppId(normalizedId.toLowerCase())
const iconResult = ThemeIcons.iconForAppId(normalizedId.toLowerCase())
if (iconResult && iconResult !== "") {
return iconResult
}
@@ -103,49 +77,49 @@ Item {
}
}
// Fallback to ToplevelManager
if (ToplevelManager && ToplevelManager.activeToplevel) {
try {
const activeToplevel = ToplevelManager.activeToplevel
if (activeToplevel.appId) {
const idValue2 = activeToplevel.appId
const normalizedId2 = (typeof idValue2 === 'string') ? idValue2 : String(idValue2)
const iconResult2 = AppIcons.iconForAppId(normalizedId2.toLowerCase())
if (iconResult2 && iconResult2 !== "") {
return iconResult2
if (CompositorService.isHyprland) {
// Fallback to ToplevelManager
if (ToplevelManager && ToplevelManager.activeToplevel) {
try {
const activeToplevel = ToplevelManager.activeToplevel
if (activeToplevel.appId) {
const idValue2 = activeToplevel.appId
const normalizedId2 = (typeof idValue2 === 'string') ? idValue2 : String(idValue2)
const iconResult2 = ThemeIcons.iconForAppId(normalizedId2.toLowerCase())
if (iconResult2 && iconResult2 !== "") {
return iconResult2
}
}
} catch (fallbackError) {
Logger.warn("ActiveWindow", "Error getting icon from ToplevelManager:", fallbackError)
}
} catch (fallbackError) {
Logger.warn("ActiveWindow", "Error getting icon from ToplevelManager:", fallbackError)
}
}
return ""
return ThemeIcons.iconFromName(fallbackIcon)
} catch (e) {
Logger.warn("ActiveWindow", "Error in getAppIcon:", e)
return ""
return ThemeIcons.iconFromName(fallbackIcon)
}
}
// A hidden text element to safely measure the full title width
// Hidden text element to measure full title width
NText {
id: fullTitleMetrics
visible: false
text: getTitle()
text: windowTitle
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
}
Rectangle {
id: windowTitleRect
id: windowActiveRect
visible: root.visible
anchors.left: (barPosition === "top" || barPosition === "bottom") ? parent.left : undefined
anchors.top: (barPosition === "left" || barPosition === "right") ? parent.top : undefined
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
width: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : (horizontalLayout.implicitWidth + Style.marginM * 2 * scaling)
height: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : Math.round(Style.capsuleHeight * scaling)
radius: width / 2
width: (barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : (widgetWidth * scaling)
height: (barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : Math.round(Style.capsuleHeight * scaling)
radius: (barPosition === "left" || barPosition === "right") ? width / 2 : Math.round(Style.radiusM * scaling)
color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
Item {
@@ -153,21 +127,21 @@ Item {
anchors.fill: parent
anchors.leftMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling
anchors.rightMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling
clip: true
// Horizontal layout for top/bottom bars
RowLayout {
id: horizontalLayout
anchors.centerIn: parent
spacing: 2 * scaling
id: rowLayout
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
visible: barPosition === "top" || barPosition === "bottom"
z: 1
// Window icon
Item {
Layout.preferredWidth: Style.capsuleHeight * 0.75 * scaling
Layout.preferredHeight: Style.capsuleHeight * 0.75 * scaling
Layout.preferredWidth: Math.round(18 * scaling)
Layout.preferredHeight: Math.round(18 * scaling)
Layout.alignment: Qt.AlignVCenter
visible: getTitle() !== "" && showIcon
visible: showIcon
IconImage {
id: windowIcon
@@ -176,39 +150,139 @@ Item {
asynchronous: true
smooth: true
visible: source !== ""
// Handle loading errors gracefully
onStatusChanged: {
if (status === Image.Error) {
Logger.warn("ActiveWindow", "Failed to load icon:", source)
}
}
}
}
NText {
id: titleText
// Title container with scrolling
Item {
id: titleContainer
Layout.preferredWidth: {
try {
if (mouseArea.containsMouse) {
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling))
} else {
return Math.round(Math.min(fullTitleMetrics.contentWidth, 80 * scaling)) // Limited width for horizontal bars
// Calculate available width based on other elements
var iconWidth = (showIcon && windowIcon.visible ? (18 * scaling + Style.marginS * scaling) : 0)
var totalMargins = Style.marginXXS * scaling * 2
var availableWidth = mainContainer.width - iconWidth - totalMargins
return Math.max(20 * scaling, availableWidth)
}
Layout.maximumWidth: Layout.preferredWidth
Layout.alignment: Qt.AlignVCenter
Layout.preferredHeight: titleText.height
clip: true
property bool isScrolling: false
property bool isResetting: false
property real textWidth: fullTitleMetrics.contentWidth
property real containerWidth: width
property bool needsScrolling: textWidth > containerWidth
// Timer for "always" mode with delay
Timer {
id: scrollStartTimer
interval: 1000
repeat: false
onTriggered: {
if (scrollingMode === "always" && titleContainer.needsScrolling) {
titleContainer.isScrolling = true
titleContainer.isResetting = false
}
} catch (e) {
Logger.warn("ActiveWindow", "Error calculating width:", e)
return 80 * scaling
}
}
Layout.alignment: Qt.AlignVCenter
horizontalAlignment: Text.AlignLeft
text: getTitle()
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
elide: mouseArea.containsMouse ? Text.ElideNone : Text.ElideRight
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
clip: true
// Update scrolling state based on mode
property var updateScrollingState: function () {
if (scrollingMode === "never") {
isScrolling = false
isResetting = false
} else if (scrollingMode === "always") {
if (needsScrolling) {
if (mouseArea.containsMouse) {
isScrolling = false
isResetting = true
} else {
scrollStartTimer.restart()
}
} else {
scrollStartTimer.stop()
isScrolling = false
isResetting = false
}
} else if (scrollingMode === "hover") {
if (mouseArea.containsMouse && needsScrolling) {
isScrolling = true
isResetting = false
} else {
isScrolling = false
if (needsScrolling) {
isResetting = true
}
}
}
}
onWidthChanged: updateScrollingState()
Component.onCompleted: updateScrollingState()
// React to hover changes
Connections {
target: mouseArea
function onContainsMouseChanged() {
titleContainer.updateScrollingState()
}
}
// Scrolling content with seamless loop
Item {
id: scrollContainer
height: parent.height
width: childrenRect.width
property real scrollX: 0
x: scrollX
Row {
spacing: 50 * scaling // Gap between text copies
NText {
id: titleText
text: windowTitle
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
verticalAlignment: Text.AlignVCenter
color: Color.mOnSurface
}
// Second copy for seamless scrolling
NText {
text: windowTitle
font: titleText.font
verticalAlignment: Text.AlignVCenter
color: Color.mOnSurface
visible: titleContainer.needsScrolling && titleContainer.isScrolling
}
}
// Reset animation
NumberAnimation on scrollX {
running: titleContainer.isResetting
to: 0
duration: 300
easing.type: Easing.OutQuad
onFinished: {
titleContainer.isResetting = false
}
}
// Seamless infinite scroll
NumberAnimation on scrollX {
id: infiniteScroll
running: titleContainer.isScrolling && !titleContainer.isResetting
from: 0
to: -(titleContainer.textWidth + 50 * scaling)
duration: Math.max(4000, windowTitle.length * 100)
loops: Animation.Infinite
easing.type: Easing.Linear
}
}
Behavior on Layout.preferredWidth {
NumberAnimation {
@@ -223,15 +297,17 @@ Item {
Item {
id: verticalLayout
anchors.centerIn: parent
width: parent.width - Style.marginXS * scaling * 2
height: parent.height - Style.marginXS * scaling * 2
width: parent.width - Style.marginM * scaling * 2
height: parent.height - Style.marginM * scaling * 2
visible: barPosition === "left" || barPosition === "right"
z: 1
// Window icon
Item {
width: Style.capsuleHeight * 0.75 * scaling
height: Style.capsuleHeight * 0.75 * scaling
width: Style.baseWidgetSize * 0.5 * scaling
height: Style.baseWidgetSize * 0.5 * scaling
anchors.centerIn: parent
visible: windowTitle !== ""
IconImage {
id: windowIconVertical
@@ -240,13 +316,6 @@ Item {
asynchronous: true
smooth: true
visible: source !== ""
// Handle loading errors gracefully
onStatusChanged: {
if (status === Image.Error) {
Logger.warn("ActiveWindow", "Failed to load icon:", source)
}
}
}
}
}
@@ -257,30 +326,31 @@ Item {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton
onEntered: {
if (barPosition === "left" || barPosition === "right") {
tooltip.show()
} else if ((tooltip.text !== "") && (scrollingMode === "never")) {
tooltip.show()
}
}
onExited: {
if (barPosition === "left" || barPosition === "right") {
tooltip.hide()
}
tooltip.hide()
}
}
// Hover tooltip with full title (only for vertical bars)
NTooltip {
id: tooltip
target: verticalLayout
text: getTitle()
positionLeft: barPosition === "right"
positionRight: barPosition === "left"
delay: 500
}
}
}
NTooltip {
id: tooltip
text: windowTitle
target: (barPosition === "left" || barPosition === "right") ? verticalLayout : windowActiveRect
positionLeft: barPosition === "right"
positionRight: barPosition === "left"
positionAbove: Settings.data.bar.position === "bottom"
delay: Style.tooltipDelay
}
Connections {
target: CompositorService
function onActiveWindowChanged() {
+3 -1
View File
@@ -54,7 +54,9 @@ Item {
// Only notify once we are a below threshold
if (!charging && !root.hasNotifiedLowBattery && percent <= warningThreshold) {
root.hasNotifiedLowBattery = true
ToastService.showWarning("Low Battery", `Battery is at ${Math.round(percent)}%. Please connect the charger.`)
ToastService.showWarning(I18n.tr("toast.battery.low"), I18n.tr("toast.battery.low-desc", {
"percent": Math.round(percent)
}))
} else if (root.hasNotifiedLowBattery && (charging || percent > warningThreshold + 5)) {
// Reset when charging starts or when battery recovers 5% above threshold
root.hasNotifiedLowBattery = false
+1 -1
View File
@@ -21,7 +21,7 @@ NIconButton {
colorBorderHover: Color.transparent
icon: Settings.data.network.bluetoothEnabled ? "bluetooth" : "bluetooth-off"
tooltipText: "Bluetooth devices."
tooltipText: I18n.tr("tooltips.bluetooth-devices")
onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this)
onRightClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this)
}
+2 -2
View File
@@ -1,10 +1,10 @@
import QtQuick
import Quickshell
import qs.Commons
import qs.Modules.SettingsPanel
import qs.Modules.Bar.Extras
import qs.Modules.Settings
import qs.Services
import qs.Widgets
import qs.Modules.Bar.Extras
Item {
id: root
+54 -143
View File
@@ -29,175 +29,86 @@ Rectangle {
}
readonly property string barPosition: Settings.data.bar.position
readonly property bool isBarVertical: barPosition === "left" || barPosition === "right"
readonly property bool compact: (Settings.data.bar.density === "compact")
readonly property bool use12h: Settings.data.location.use12hourFormat
readonly property bool monthBeforeDay: Settings.data.location.monthBeforeDay
readonly property var now: Time.date
// Resolve settings: try user settings or defaults from BarWidgetRegistry
readonly property string displayFormat: widgetSettings.displayFormat !== undefined ? widgetSettings.displayFormat : widgetMetadata.displayFormat
readonly property bool usePrimaryColor: widgetSettings.usePrimaryColor !== undefined ? widgetSettings.usePrimaryColor : widgetMetadata.usePrimaryColor
property bool useMonospacedFont: widgetSettings.useMonospacedFont !== undefined ? widgetSettings.useMonospacedFont : widgetMetadata.useMonospacedFont
readonly property string formatHorizontal: widgetSettings.formatHorizontal !== undefined ? widgetSettings.formatHorizontal : widgetMetadata.formatHorizontal
readonly property string formatVertical: widgetSettings.formatVertical !== undefined ? widgetSettings.formatVertical : widgetMetadata.formatVertical
// Use compact mode for vertical bars
readonly property bool verticalMode: barPosition === "left" || barPosition === "right"
implicitWidth: isBarVertical ? Math.round(Style.capsuleHeight * scaling) : Math.round((isBarVertical ? verticalLoader.implicitWidth : horizontalLoader.implicitWidth) + Style.marginM * 2 * scaling)
implicitWidth: verticalMode ? Math.round(Style.capsuleHeight * scaling) : Math.round(layout.implicitWidth + Style.marginM * 2 * scaling)
implicitHeight: verticalMode ? Math.round(Style.capsuleHeight * 2.5 * scaling) : Math.round(Style.capsuleHeight * scaling) // Match BarPill
implicitHeight: isBarVertical ? Math.round(verticalLoader.implicitHeight + Style.marginS * 2 * scaling) : Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusS * scaling)
color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
Item {
id: clockContainer
anchors.fill: parent
anchors.margins: compact ? 0 : Style.marginXS * scaling
anchors.centerIn: parent
ColumnLayout {
id: layout
// Horizontal
Loader {
id: horizontalLoader
active: !isBarVertical
anchors.centerIn: parent
spacing: verticalMode ? -2 * scaling : -3 * scaling
// Compact mode for vertical bars - Time section (HH, MM)
Repeater {
model: verticalMode ? 2 : 1
NText {
readonly property bool showSeconds: (displayFormat === "time-seconds")
readonly property bool inlineDate: (displayFormat === "time-date")
readonly property var now: Time.date
text: {
if (verticalMode) {
// Compact mode: time section (first 2 lines)
switch (index) {
case 0:
// Hours
if (use12h) {
const hours = now.getHours()
const displayHours = hours === 0 ? 12 : (hours > 12 ? hours - 12 : hours)
return displayHours.toString().padStart(2, '0')
} else {
return now.getHours().toString().padStart(2, '0')
}
case 1:
// Minutes
return now.getMinutes().toString().padStart(2, '0')
default:
return ""
}
} else {
// Normal mode: single line with time
let timeStr = ""
if (use12h) {
// 12-hour format with proper padding and consistent spacing
const hours = now.getHours()
const displayHours = hours === 0 ? 12 : (hours > 12 ? hours - 12 : hours)
const paddedHours = displayHours.toString().padStart(2, '0')
const minutes = now.getMinutes().toString().padStart(2, '0')
const ampm = hours < 12 ? 'AM' : 'PM'
if (showSeconds) {
const seconds = now.getSeconds().toString().padStart(2, '0')
timeStr = `${paddedHours}:${minutes}:${seconds} ${ampm}`
} else {
timeStr = `${paddedHours}:${minutes} ${ampm}`
}
sourceComponent: ColumnLayout {
anchors.centerIn: parent
spacing: Settings.data.bar.showCapsule ? -4 * scaling : -2 * scaling
Repeater {
id: repeater
model: Qt.formatDateTime(now, formatHorizontal.trim()).split("\\n")
NText {
visible: text !== ""
text: modelData
font.family: useMonospacedFont ? Settings.data.ui.fontFixed : Settings.data.ui.fontDefault
font.pointSize: {
if (repeater.model.length == 1) {
return Style.fontSizeS * scaling
} else {
// 24-hour format with padding
const hours = now.getHours().toString().padStart(2, '0')
const minutes = now.getMinutes().toString().padStart(2, '0')
if (showSeconds) {
const seconds = now.getSeconds().toString().padStart(2, '0')
timeStr = `${hours}:${minutes}:${seconds}`
} else {
timeStr = `${hours}:${minutes}`
}
}
// Add inline date if needed
if (inlineDate) {
let dayName = now.toLocaleDateString(Qt.locale(), "ddd")
dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1)
const day = now.getDate().toString().padStart(2, '0')
let month = now.toLocaleDateString(Qt.locale(), "MMM")
timeStr += " - " + (monthBeforeDay ? `${dayName}, ${month} ${day}` : `${dayName}, ${day} ${month}`)
}
return timeStr
}
}
font.family: Settings.data.ui.fontFixed
font.pointSize: verticalMode ? Style.fontSizeXXS * scaling : Style.fontSizeXS * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
}
}
// Separator line for compact mode (between time and date)
Rectangle {
visible: verticalMode
Layout.preferredWidth: 20 * scaling
Layout.preferredHeight: 2 * scaling
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 3 * scaling
Layout.bottomMargin: 3 * scaling
color: Color.mPrimary
opacity: 0.3
radius: 1 * scaling
}
// Compact mode for vertical bars - Date section (DD, MM)
Repeater {
model: verticalMode ? 2 : 0
NText {
readonly property var now: Time.date
text: {
if (verticalMode) {
// Compact mode: date section (last 2 lines)
switch (index) {
case 0:
return monthBeforeDay ? (now.getMonth() + 1).toString().padStart(2, '0') : now.getDate().toString().padStart(2, '0')
case 1:
return monthBeforeDay ? now.getDate().toString().padStart(2, '0') : (now.getMonth() + 1).toString().padStart(2, '0')
default:
return ""
return (index == 0) ? Style.fontSizeXS * scaling : Style.fontSizeXXS * scaling
}
}
return ""
font.weight: Style.fontWeightBold
color: usePrimaryColor ? Color.mPrimary : Color.mOnSurface
wrapMode: Text.WordWrap
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
}
font.pointSize: Style.fontSizeXXS * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
}
}
}
// Second line for normal mode (date)
NText {
visible: !verticalMode && (displayFormat === "time-date-short")
text: {
const now = Time.date
const day = now.getDate().toString().padStart(2, '0')
const month = (now.getMonth() + 1).toString().padStart(2, '0')
return monthBeforeDay ? `${month}/${day}` : `${day}/${month}`
// Vertical
Loader {
id: verticalLoader
active: isBarVertical
anchors.centerIn: parent // Now this works without layout conflicts
sourceComponent: ColumnLayout {
anchors.centerIn: parent
spacing: -2 * scaling
Repeater {
model: Qt.formatDateTime(now, formatVertical.trim()).split(" ")
delegate: NText {
visible: text !== ""
text: modelData
font.family: useMonospacedFont ? Settings.data.ui.fontFixed : Settings.data.ui.fontDefault
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightBold
color: usePrimaryColor ? Color.mPrimary : Color.mOnSurface
wrapMode: Text.WordWrap
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
}
}
// Enable fixed-width font for consistent spacing
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeXXS * scaling
font.weight: Style.fontWeightRegular
color: Color.mPrimary
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
}
}
}
NTooltip {
id: tooltip
text: `${Time.formatDate(monthBeforeDay)}.`
text: I18n.tr("clock.tooltip")
target: clockContainer
positionAbove: Settings.data.bar.position === "bottom"
}
@@ -29,10 +29,13 @@ NIconButton {
return {}
}
readonly property string customIcon: widgetSettings.icon || widgetMetadata.icon
readonly property bool useDistroLogo: (widgetSettings.useDistroLogo !== undefined) ? widgetSettings.useDistroLogo : widgetMetadata.useDistroLogo
readonly property string customIconPath: widgetSettings.customIconPath || ""
icon: useDistroLogo ? "" : "noctalia"
tooltipText: "Open side panel."
// If we have a custom path or distro logo, don't use the theme icon.
icon: (customIconPath === "" && !useDistroLogo) ? customIcon : ""
tooltipText: I18n.tr("tooltips.open-control-center")
baseSize: Style.capsuleHeight
compact: (Settings.data.bar.density === "compact")
colorBg: (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
@@ -40,16 +43,22 @@ NIconButton {
colorBgHover: useDistroLogo ? Color.mSurfaceVariant : Color.mTertiary
colorBorder: Color.transparent
colorBorderHover: useDistroLogo ? Color.mTertiary : Color.transparent
onClicked: PanelService.getPanel("sidePanel")?.toggle(this)
onClicked: PanelService.getPanel("controlCenterPanel")?.toggle(this)
onRightClicked: PanelService.getPanel("settingsPanel")?.toggle()
IconImage {
id: logo
id: customOrDistroLogo
anchors.centerIn: parent
width: root.width * 0.8
height: width
source: useDistroLogo ? DistroLogoService.osLogo : ""
visible: useDistroLogo && source !== ""
source: {
if (customIconPath !== "")
return customIconPath
if (useDistroLogo)
return DistroLogoService.osLogo
return ""
}
visible: source !== ""
smooth: true
asynchronous: true
}
+2 -2
View File
@@ -5,7 +5,7 @@ import Quickshell.Io
import qs.Commons
import qs.Services
import qs.Widgets
import qs.Modules.SettingsPanel
import qs.Modules.Settings
import qs.Modules.Bar.Extras
Item {
@@ -57,7 +57,7 @@ Item {
disableOpen: true
tooltipText: {
if (!hasExec) {
return "Custom Button - Configure in settings"
return "Custom button, configure in settings."
} else {
var lines = []
if (leftClickExec !== "") {
@@ -10,7 +10,7 @@ NIconButton {
property real scaling: 1.0
icon: "dark-mode"
tooltipText: "Toggle light/dark mode."
tooltipText: Settings.data.colorSchemes.darkMode ? I18n.tr("tooltips.switch-to-light-mode") : I18n.tr("tooltips.switch-to-dark-mode")
compact: (Settings.data.bar.density === "compact")
baseSize: Style.capsuleHeight
colorBg: Settings.data.colorSchemes.darkMode ? (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent) : Color.mPrimary
+2 -1
View File
@@ -14,9 +14,10 @@ NIconButton {
baseSize: Style.capsuleHeight
compact: (Settings.data.bar.density === "compact")
icon: IdleInhibitorService.isInhibited ? "keep-awake-on" : "keep-awake-off"
tooltipText: IdleInhibitorService.isInhibited ? "Disable keep awake" : "Enable keep awake"
tooltipText: IdleInhibitorService.isInhibited ? I18n.tr("tooltips.disable-keep-awake") : I18n.tr("tooltips.enable-keep-awake")
colorBg: IdleInhibitorService.isInhibited ? Color.mPrimary : (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
colorFg: IdleInhibitorService.isInhibited ? Color.mOnPrimary : Color.mOnSurface
colorBorder: Color.transparent
colorBorderHover: Color.transparent
onClicked: IdleInhibitorService.manualToggle()
}
+4 -3
View File
@@ -48,13 +48,14 @@ Item {
icon: "keyboard"
autoHide: false // Important to be false so we can hover as long as we want
text: currentLayout.toUpperCase()
tooltipText: "Keyboard layout: " + currentLayout.toUpperCase()
tooltipText: I18n.tr("tooltips.keyboard-layout", {
"layout": currentLayout.toUpperCase()
})
forceOpen: root.displayMode === "forceOpen"
forceClose: root.displayMode === "alwaysHide"
onClicked: {
// You could open keyboard settings here if needed
// For now, just show the current layout
// You could open keyboard settings here if needed.
}
}
}
+168 -69
View File
@@ -33,13 +33,25 @@ Item {
readonly property string barPosition: Settings.data.bar.position
readonly property bool compact: (Settings.data.bar.density === "compact")
readonly property bool autoHide: (widgetSettings.autoHide !== undefined) ? widgetSettings.autoHide : widgetMetadata.autoHide
readonly property bool showAlbumArt: (widgetSettings.showAlbumArt !== undefined) ? widgetSettings.showAlbumArt : widgetMetadata.showAlbumArt
readonly property bool showVisualizer: (widgetSettings.showVisualizer !== undefined) ? widgetSettings.showVisualizer : widgetMetadata.showVisualizer
readonly property string visualizerType: (widgetSettings.visualizerType !== undefined && widgetSettings.visualizerType !== "") ? widgetSettings.visualizerType : widgetMetadata.visualizerType
readonly property string scrollingMode: (widgetSettings.scrollingMode !== undefined) ? widgetSettings.scrollingMode : widgetMetadata.scrollingMode
// 6% of total width
readonly property real minWidth: Math.max(1, screen.width * 0.06)
readonly property real maxWidth: minWidth * 2
// Fixed width - no expansion
readonly property real widgetWidth: Math.max(1, screen.width * 0.06)
implicitHeight: visible ? ((barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling)) : 0
implicitWidth: visible ? ((barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : (widgetWidth * scaling)) : 0
opacity: !autoHide || getTitle() !== "" ? 1.0 : 0
Behavior on opacity {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutCubic
}
}
function getTitle() {
return MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - ${MediaService.trackArtist}` : "")
@@ -49,23 +61,6 @@ Item {
return Math.round(Style.baseWidgetSize * 0.8 * scaling)
}
function calculatedHorizontalWidth() {
let total = Style.marginM * 2 * scaling // internal padding
if (showAlbumArt) {
total += 18 * scaling + 2 * scaling // album art + spacing
} else {
total += Style.fontSizeL * scaling + 2 * scaling // icon + spacing
}
total += Math.min(fullTitleMetrics.contentWidth, maxWidth * scaling) // title text
// Row layout handles spacing between widgets
return total
}
implicitHeight: visible ? ((barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling)) : 0
implicitWidth: visible ? ((barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : (rowLayout.implicitWidth + Style.marginM * 2 * scaling)) : 0
visible: MediaService.currentPlayer !== null && MediaService.canPlay
// A hidden text element to safely measure the full title width
NText {
id: fullTitleMetrics
@@ -79,18 +74,11 @@ Item {
visible: root.visible
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: (barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : (rowLayout.implicitWidth + Style.marginM * 2 * scaling)
width: (barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : (widgetWidth * scaling)
height: (barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : Math.round(Style.capsuleHeight * scaling)
radius: (barPosition === "left" || barPosition === "right") ? width / 2 : Math.round(Style.radiusM * scaling)
color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
// Used to anchor the tooltip, so the tooltip does not move when the content expands
Item {
id: anchor
height: parent.height
width: 200 * scaling
}
Item {
id: mainContainer
anchors.fill: parent
@@ -100,14 +88,14 @@ Item {
Loader {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
active: showVisualizer && visualizerType == "linear" && MediaService.isPlaying
active: showVisualizer && visualizerType == "linear"
z: 0
sourceComponent: LinearSpectrum {
width: mainContainer.width - Style.marginS * scaling
height: 20 * scaling
values: CavaService.values
fillColor: Color.mOnSurfaceVariant
fillColor: Color.mPrimary
opacity: 0.4
}
}
@@ -115,14 +103,14 @@ Item {
Loader {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
active: showVisualizer && visualizerType == "mirrored" && MediaService.isPlaying
active: showVisualizer && visualizerType == "mirrored"
z: 0
sourceComponent: MirroredSpectrum {
width: mainContainer.width - Style.marginS * scaling
height: mainContainer.height - Style.marginS * scaling
values: CavaService.values
fillColor: Color.mOnSurfaceVariant
fillColor: Color.mPrimary
opacity: 0.4
}
}
@@ -130,14 +118,14 @@ Item {
Loader {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
active: showVisualizer && visualizerType == "wave" && MediaService.isPlaying
active: showVisualizer && visualizerType == "wave"
z: 0
sourceComponent: WaveSpectrum {
width: mainContainer.width - Style.marginS * scaling
height: mainContainer.height - Style.marginS * scaling
values: CavaService.values
fillColor: Color.mOnSurfaceVariant
fillColor: Color.mPrimary
opacity: 0.4
}
}
@@ -145,9 +133,10 @@ Item {
// Horizontal layout for top/bottom bars
RowLayout {
id: rowLayout
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
visible: barPosition === "top" || barPosition === "bottom"
visible: (barPosition === "top" || barPosition === "bottom") && getTitle() !== ""
z: 1 // Above the visualizer
NIcon {
@@ -180,24 +169,134 @@ Item {
}
}
NText {
id: titleText
Item {
id: titleContainer
Layout.preferredWidth: {
if (mouseArea.containsMouse) {
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling))
} else {
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.minWidth * scaling))
// Calculate available width based on other elements in the row
var iconWidth = (windowIcon.visible ? (Style.fontSizeL * scaling + Style.marginS * scaling) : 0)
var albumArtWidth = (showAlbumArt ? (18 * scaling + Style.marginS * scaling) : 0)
var totalMargins = Style.marginXXS * scaling * 2
var availableWidth = mainContainer.width - iconWidth - albumArtWidth - totalMargins
return Math.max(20 * scaling, availableWidth)
}
Layout.maximumWidth: Layout.preferredWidth
Layout.alignment: Qt.AlignVCenter
Layout.preferredHeight: titleText.height
clip: true
property bool isScrolling: false
property bool isResetting: false
property real textWidth: fullTitleMetrics.contentWidth
property real containerWidth: width
property bool needsScrolling: textWidth > containerWidth
// Timer for "always" mode with delay
Timer {
id: scrollStartTimer
interval: 1000
repeat: false
onTriggered: {
if (scrollingMode === "always" && titleContainer.needsScrolling) {
titleContainer.isScrolling = true
titleContainer.isResetting = false
}
}
}
Layout.alignment: Qt.AlignVCenter
text: getTitle()
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
color: Color.mSecondary
// Update scrolling state based on mode
property var updateScrollingState: function () {
if (scrollingMode === "never") {
isScrolling = false
isResetting = false
} else if (scrollingMode === "always") {
if (needsScrolling) {
if (mouseArea.containsMouse) {
isScrolling = false
isResetting = true
} else {
scrollStartTimer.restart()
}
} else {
scrollStartTimer.stop()
isScrolling = false
isResetting = false
}
} else if (scrollingMode === "hover") {
if (mouseArea.containsMouse && needsScrolling) {
isScrolling = true
isResetting = false
} else {
isScrolling = false
if (needsScrolling) {
isResetting = true
}
}
}
}
onWidthChanged: updateScrollingState()
Component.onCompleted: updateScrollingState()
Connections {
target: mouseArea
function onContainsMouseChanged() {
titleContainer.updateScrollingState()
}
}
// Scrolling content
Item {
id: scrollContainer
height: parent.height
width: childrenRect.width
property real scrollX: 0
x: scrollX
Row {
spacing: 50 * scaling // Gap between text copies
NText {
id: titleText
text: getTitle()
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
verticalAlignment: Text.AlignVCenter
color: Color.mOnSurface
}
NText {
text: getTitle()
font: titleText.font
verticalAlignment: Text.AlignVCenter
color: Color.mOnSurface
visible: titleContainer.needsScrolling && titleContainer.isScrolling
}
}
// Reset animation
NumberAnimation on scrollX {
running: titleContainer.isResetting
to: 0
duration: 300
easing.type: Easing.OutQuad
onFinished: {
titleContainer.isResetting = false
}
}
// Seamless infinite scroll
NumberAnimation on scrollX {
id: infiniteScroll
running: titleContainer.isScrolling && !titleContainer.isResetting
from: 0
to: -(titleContainer.textWidth + 50 * scaling) // Scroll one complete text width + gap
duration: Math.max(4000, getTitle().length * 120)
loops: Animation.Infinite
easing.type: Easing.Linear
}
}
Behavior on Layout.preferredWidth {
NumberAnimation {
@@ -243,6 +342,10 @@ Item {
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: mouse => {
if (!MediaService.currentPlayer || !MediaService.canPlay) {
return
}
if (mouse.button === Qt.LeftButton) {
MediaService.playPause()
} else if (mouse.button == Qt.RightButton) {
@@ -257,18 +360,14 @@ Item {
}
onEntered: {
if (barPosition === "left" || barPosition === "right") {
if ((tooltip.text !== "") && (barPosition === "left" || barPosition === "right")) {
tooltip.show()
} else if (tooltip.text !== "") {
} else if ((tooltip.text !== "") && (scrollingMode === "never")) {
tooltip.show()
}
}
onExited: {
if (barPosition === "left" || barPosition === "right") {
tooltip.hide()
} else {
tooltip.hide()
}
tooltip.hide()
}
}
}
@@ -277,23 +376,23 @@ Item {
NTooltip {
id: tooltip
text: {
if (barPosition === "left" || barPosition === "right") {
return getTitle()
} else {
var str = ""
if (MediaService.canGoNext) {
str += "Right click for next.\n"
}
if (MediaService.canGoPrevious) {
str += "Middle click for previous."
}
return str
var title = getTitle()
var controls = ""
if (MediaService.canGoNext) {
controls += "Right click for next.\n"
}
if (MediaService.canGoPrevious) {
controls += "Middle click for previous."
}
if (controls !== "") {
return title + "\n\n" + controls
}
return title
}
target: (barPosition === "left" || barPosition === "right") ? verticalLayout : anchor
target: (barPosition === "left" || barPosition === "right") ? verticalLayout : mediaMini
positionLeft: barPosition === "right"
positionRight: barPosition === "left"
positionAbove: Settings.data.bar.position === "bottom"
delay: 500
delay: Style.tooltipDelay
}
}
+5 -3
View File
@@ -3,7 +3,7 @@ import Quickshell
import Quickshell.Io
import Quickshell.Services.Pipewire
import qs.Commons
import qs.Modules.SettingsPanel
import qs.Modules.Settings
import qs.Services
import qs.Widgets
import qs.Modules.Bar.Extras
@@ -93,11 +93,13 @@ Item {
icon: getIcon()
compact: (Settings.data.bar.density === "compact")
autoHide: false // Important to be false so we can hover as long as we want
text: Math.floor(AudioService.inputVolume * 100)
text: Math.round(AudioService.inputVolume * 100)
suffix: "%"
forceOpen: displayMode === "alwaysShow"
forceClose: displayMode === "alwaysHide"
tooltipText: "Microphone: " + Math.round(AudioService.inputVolume * 100) + "%\nLeft click to toggle mute.\nRight click for settings.\nScroll to modify volume."
tooltipText: I18n.tr("tooltips.microphone-volume-at", {
"volume": Math.round(AudioService.inputVolume * 100)
})
onWheel: function (delta) {
wheelAccumulator += delta
+2 -2
View File
@@ -4,7 +4,7 @@ import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Modules.SettingsPanel
import qs.Modules.Settings
import qs.Services
import qs.Widgets
@@ -22,7 +22,7 @@ NIconButton {
colorBorderHover: Color.transparent
icon: Settings.data.nightLight.enabled ? (Settings.data.nightLight.forced ? "nightlight-forced" : "nightlight-on") : "nightlight-off"
tooltipText: `Night light: ${Settings.data.nightLight.enabled ? (Settings.data.nightLight.forced ? "forced." : "enabled.") : "disabled."}\nLeft click to cycle (disabled normal forced).\nRight click to access settings.`
tooltipText: Settings.data.nightLight.enabled ? (Settings.data.nightLight.forced ? I18n.tr("tooltips.night-light-forced") : I18n.tr("tooltips.night-light-enabled")) : I18n.tr("tooltips.night-light-disabled")
onClicked: {
if (!Settings.data.nightLight.enabled) {
Settings.data.nightLight.enabled = true
+2 -2
View File
@@ -39,7 +39,7 @@ NIconButton {
function computeUnreadCount() {
var since = lastSeenTs()
var count = 0
var model = NotificationService.historyModel
var model = NotificationService.historyList
for (var i = 0; i < model.count; i++) {
var item = model.get(i)
var ts = item.timestamp instanceof Date ? item.timestamp.getTime() : item.timestamp
@@ -52,7 +52,7 @@ NIconButton {
baseSize: Style.capsuleHeight
compact: (Settings.data.bar.density === "compact")
icon: Settings.data.notifications.doNotDisturb ? "bell-off" : "bell"
tooltipText: Settings.data.notifications.doNotDisturb ? "Notification history.\nRight-click to disable 'Do Not Disturb'." : "Notification history.\nRight-click to enable 'Do Not Disturb'."
tooltipText: Settings.data.notifications.doNotDisturb ? I18n.tr("tooltips.open-notification-history-disable-dnd") : I18n.tr("tooltips.open-notification-history-enable-dnd")
colorBg: (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
colorFg: Color.mOnSurface
colorBorder: Color.transparent
+3 -1
View File
@@ -16,7 +16,9 @@ NIconButton {
visible: PowerProfileService.available
icon: PowerProfileService.getIcon()
tooltipText: `Current power profile is "${PowerProfileService.getName()}".`
tooltipText: I18n.tr("tooltips.power-profile", {
"profile": PowerProfileService.getName()
})
compact: (Settings.data.bar.density === "compact")
colorBg: (PowerProfileService.profile === PowerProfile.Balanced) ? (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent) : Color.mPrimary
colorFg: (PowerProfileService.profile === PowerProfile.Balanced) ? Color.mOnSurface : Color.mOnPrimary
+22
View File
@@ -0,0 +1,22 @@
import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
// Screen Recording Indicator
NIconButton {
id: root
property ShellScreen screen
property real scaling: 1.0
icon: "camera-video"
tooltipText: ScreenRecorderService.isRecording ? I18n.tr("tooltips.click-to-stop-recording") : I18n.tr("tooltips.click-to-start-recording")
compact: (Settings.data.bar.density === "compact")
baseSize: Style.capsuleHeight
colorBg: ScreenRecorderService.isRecording ? Color.mPrimary : (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
colorFg: ScreenRecorderService.isRecording ? Color.mOnPrimary : Color.mOnSurface
colorBorder: Color.transparent
colorBorderHover: Color.transparent
onClicked: ScreenRecorderService.toggleRecording()
}
@@ -1,21 +0,0 @@
import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
// Screen Recording Indicator
NIconButton {
id: root
property ShellScreen screen
property real scaling: 1.0
visible: ScreenRecorderService.isRecording
icon: "camera-video"
tooltipText: "Screen recording is active.\nClick to stop recording."
compact: (Settings.data.bar.density === "compact")
baseSize: Style.capsuleHeight
colorBg: Color.mPrimary
colorFg: Color.mOnPrimary
onClicked: ScreenRecorderService.toggleRecording()
}
@@ -14,10 +14,10 @@ NIconButton {
compact: (Settings.data.bar.density === "compact")
baseSize: Style.capsuleHeight
icon: "power"
tooltipText: "Power Settings"
tooltipText: I18n.tr("tooltips.session-menu")
colorBg: (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
colorFg: Color.mError
colorBorder: Color.transparent
colorBorderHover: Color.transparent
onClicked: PanelService.getPanel("powerPanel")?.toggle()
onClicked: PanelService.getPanel("sessionMenuPanel")?.toggle()
}
+7 -2
View File
@@ -120,7 +120,9 @@ Rectangle {
columnSpacing: Style.marginXXS * scaling
NText {
text: isVertical ? `${SystemStatService.cpuTemp}°` : `${SystemStatService.cpuTemp}°C`
text: I18n.tr("system.cpu-temperature", {
"temp": SystemStatService.cpuTemp
})
font.family: Settings.data.ui.fontFixed
font.pointSize: textSize
font.weight: Style.fontWeightMedium
@@ -128,6 +130,7 @@ Rectangle {
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
scale: isVertical ? Math.min(1.0, cpuTempContent.width / implicitWidth) : 1.0
Layout.row: isVertical ? 0 : 0
Layout.column: isVertical ? 0 : 1
}
@@ -282,7 +285,9 @@ Rectangle {
columnSpacing: isVertical ? (Style.marginXXS * scaling) : (Style.marginXS * scaling)
NText {
text: `${SystemStatService.diskPercent}%`
text: I18n.tr("system.disk-usage", {
"percent": SystemStatService.diskPercent
})
font.family: Settings.data.ui.fontFixed
font.pointSize: textSize
font.weight: Style.fontWeightMedium
+2 -2
View File
@@ -68,7 +68,7 @@ Rectangle {
anchors.centerIn: parent
width: parent.width
height: parent.height
source: AppIcons.iconForAppId(taskbarItem.modelData.appId)
source: ThemeIcons.iconForAppId(taskbarItem.modelData.appId)
smooth: true
asynchronous: true
}
@@ -104,7 +104,7 @@ Rectangle {
NTooltip {
id: taskbarTooltip
text: taskbarItem.modelData.title || taskbarItem.modelData.appId || "Unknown App."
text: taskbarItem.modelData.title || taskbarItem.modelData.appId || "Unknown app."
target: taskbarItem
positionAbove: Settings.data.bar.position === "bottom"
}
+5 -3
View File
@@ -3,7 +3,7 @@ import Quickshell
import Quickshell.Io
import Quickshell.Services.Pipewire
import qs.Commons
import qs.Modules.SettingsPanel
import qs.Modules.Settings
import qs.Services
import qs.Widgets
import qs.Modules.Bar.Extras
@@ -79,11 +79,13 @@ Item {
rightOpen: BarService.getPillDirection(root)
icon: getIcon()
autoHide: false // Important to be false so we can hover as long as we want
text: Math.floor(AudioService.volume * 100)
text: Math.round(AudioService.volume * 100)
suffix: "%"
forceOpen: displayMode === "alwaysShow"
forceClose: displayMode === "alwaysHide"
tooltipText: "Volume: " + Math.round(AudioService.volume * 100) + "%\nLeft click to toggle mute.\nRight click for settings.\nScroll to modify volume."
tooltipText: I18n.tr("tooltips.volume-at", {
"volume": Math.round(AudioService.volume * 100)
})
onWheel: function (delta) {
wheelAccumulator += delta
+23
View File
@@ -0,0 +1,23 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
NIconButton {
id: root
property ShellScreen screen
property real scaling: 1.0
baseSize: Style.capsuleHeight
compact: (Settings.data.bar.density === "compact")
icon: "wallpaper-selector"
tooltipText: I18n.tr("tooltips.open-wallpaper-selector")
colorBg: (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
colorFg: Color.mOnSurface
colorBorder: Color.transparent
colorBorderHover: Color.transparent
onClicked: PanelService.getPanel("wallpaperPanel")?.toggle(this)
}
+1 -1
View File
@@ -40,7 +40,7 @@ NIconButton {
return "signal_wifi_bad"
}
}
tooltipText: "Manage Wi-Fi."
tooltipText: I18n.tr("tooltips.manage-wifi")
onClicked: PanelService.getPanel("wifiPanel")?.toggle(this)
onRightClicked: PanelService.getPanel("wifiPanel")?.toggle(this)
}
+79 -17
View File
@@ -56,6 +56,10 @@ Item {
property int horizontalPadding: Math.round(Style.marginS * scaling)
property int spacingBetweenPills: Math.round(Style.marginXS * scaling)
// Wheel scroll handling
property int wheelAccumulatedDelta: 0
property bool wheelCooldown: false
signal workspaceChanged(int workspaceId, color accentColor)
implicitWidth: isVertical ? Math.round(Style.barHeight * scaling) : computeWidth()
@@ -63,18 +67,14 @@ Item {
function getWorkspaceWidth(ws) {
const d = Style.capsuleHeight * root.baseDimensionRatio
if (ws.isFocused)
return d * 2.5
else
return d
const factor = ws.isFocused ? 2.2 : 1
return d * factor * scaling
}
function getWorkspaceHeight(ws) {
const d = Style.capsuleHeight * root.baseDimensionRatio
if (ws.isFocused)
return d * 3
else
return d
const factor = ws.isFocused ? 2.2 : 1
return d * factor * scaling
}
function computeWidth() {
@@ -99,6 +99,28 @@ Item {
return Math.round(total)
}
function getFocusedLocalIndex() {
for (var i = 0; i < localWorkspaces.count; i++) {
if (localWorkspaces.get(i).isFocused === true)
return i
}
return -1
}
function switchByOffset(offset) {
if (localWorkspaces.count === 0)
return
var current = getFocusedLocalIndex()
if (current < 0)
current = 0
var next = (current + offset) % localWorkspaces.count
if (next < 0)
next = localWorkspaces.count - 1
const ws = localWorkspaces.get(next)
if (ws && ws.idx !== undefined)
CompositorService.switchToWorkspace(ws.idx)
}
Component.onCompleted: {
refreshWorkspaces()
}
@@ -111,7 +133,7 @@ Item {
onHideUnoccupiedChanged: refreshWorkspaces()
Connections {
target: WorkspaceService
target: CompositorService
function onWorkspacesChanged() {
refreshWorkspaces()
}
@@ -120,8 +142,8 @@ Item {
function refreshWorkspaces() {
localWorkspaces.clear()
if (screen !== null) {
for (var i = 0; i < WorkspaceService.workspaces.count; i++) {
const ws = WorkspaceService.workspaces.get(i)
for (var i = 0; i < CompositorService.workspaces.count; i++) {
const ws = CompositorService.workspaces.get(i)
if (ws.output.toLowerCase() === screen.name.toLowerCase()) {
if (hideUnoccupied && !ws.isOccupied && !ws.isFocused) {
continue
@@ -189,6 +211,46 @@ Item {
anchors.verticalCenter: parent.verticalCenter
}
// Debounce timer for wheel interactions
Timer {
id: wheelDebounce
interval: 150
repeat: false
onTriggered: {
root.wheelCooldown = false
root.wheelAccumulatedDelta = 0
}
}
// Scroll to switch workspaces
WheelHandler {
id: wheelHandler
target: root
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: function (event) {
if (root.wheelCooldown)
return
// Prefer vertical delta, fall back to horizontal if needed
var dy = event.angleDelta.y
var dx = event.angleDelta.x
var useDy = Math.abs(dy) >= Math.abs(dx)
var delta = useDy ? dy : dx
// One notch is typically 120
root.wheelAccumulatedDelta += delta
var step = 120
if (Math.abs(root.wheelAccumulatedDelta) >= step) {
var direction = root.wheelAccumulatedDelta > 0 ? -1 : 1
// For vertical layout, natural mapping: wheel up -> previous, down -> next (already handled by sign)
// For horizontal layout, same mapping using vertical wheel
root.switchByOffset(direction)
root.wheelCooldown = true
wheelDebounce.restart()
root.wheelAccumulatedDelta = 0
event.accepted = true
}
}
}
// Horizontal layout for top/bottom bars
Row {
id: pillRow
@@ -203,7 +265,7 @@ Item {
Item {
id: workspacePillContainer
width: root.getWorkspaceWidth(model)
height: Style.capsuleHeight * root.baseDimensionRatio
height: Style.capsuleHeight * root.baseDimensionRatio * scaling
Rectangle {
id: pill
@@ -235,7 +297,7 @@ Item {
if (model.isActive || model.isOccupied)
return Color.mOnSecondary
return Color.mOnSurface
return Color.mOnSecondary
}
}
}
@@ -250,7 +312,7 @@ Item {
if (model.isActive || model.isOccupied)
return Color.mSecondary
return Color.mOutline
return Qt.alpha(Color.mSecondary, 0.3)
}
scale: model.isFocused ? 1.0 : 0.9
z: 0
@@ -260,7 +322,7 @@ Item {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
WorkspaceService.switchToWorkspace(model.idx)
CompositorService.switchToWorkspace(model.idx)
}
hoverEnabled: true
}
@@ -346,7 +408,7 @@ Item {
model: localWorkspaces
Item {
id: workspacePillContainerVertical
width: Style.capsuleHeight * root.baseDimensionRatio
width: Style.capsuleHeight * root.baseDimensionRatio * scaling
height: root.getWorkspaceHeight(model)
Rectangle {
@@ -404,7 +466,7 @@ Item {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
WorkspaceService.switchToWorkspace(model.idx)
CompositorService.switchToWorkspace(model.idx)
}
hoverEnabled: true
}
@@ -32,12 +32,12 @@ NBox {
color: Color.mPrimary
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "No media player detected"
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
// NText {
// text: I18n.tr("system.no-media-player-detected")
// color: Color.mOnSurfaceVariant
// Layout.alignment: Qt.AlignHCenter
// }
Item {
Layout.fillWidth: true
Layout.fillHeight: true
@@ -51,91 +51,71 @@ NBox {
visible: MediaService.currentPlayer && MediaService.canPlay
spacing: Style.marginM * scaling
// Player selector
ComboBox {
id: playerSelector
// Player selector using NContextMenu
Rectangle {
id: playerSelectorButton
Layout.fillWidth: true
Layout.preferredHeight: Style.barHeight * 0.83 * scaling
Layout.preferredHeight: Style.barHeight * scaling
visible: MediaService.getAvailablePlayers().length > 1
model: MediaService.getAvailablePlayers()
textRole: "identity"
currentIndex: MediaService.selectedPlayerIndex
radius: Style.radiusM * scaling
color: Color.transparent
background: Rectangle {
visible: false
// implicitWidth: 120 * scaling
// implicitHeight: 30 * scaling
color: Color.transparent
border.color: playerSelector.activeFocus ? Color.mSecondary : Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
radius: Style.radiusM * scaling
}
property var currentPlayer: MediaService.getAvailablePlayers()[MediaService.selectedPlayerIndex]
contentItem: NText {
visible: false
leftPadding: Style.marginM * scaling
rightPadding: playerSelector.indicator.width + playerSelector.spacing
text: playerSelector.displayText
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurface
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
RowLayout {
anchors.fill: parent
spacing: Style.marginS * scaling
indicator: NIcon {
x: playerSelector.width - width
y: playerSelector.topPadding + (playerSelector.availableHeight - height) / 2
icon: "caret-down"
font.pointSize: Style.fontSizeXXL * scaling
color: Color.mOnSurface
horizontalAlignment: Text.AlignRight
}
popup: Popup {
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.marginM * scaling)
padding: Style.marginS * scaling
contentItem: ListView {
clip: true
implicitHeight: contentHeight
model: playerSelector.popup.visible ? playerSelector.delegateModel : null
currentIndex: playerSelector.highlightedIndex
ScrollIndicator.vertical: ScrollIndicator {}
NIcon {
icon: "caret-down"
font.pointSize: Style.fontSizeXXL * scaling
color: Color.mOnSurfaceVariant
}
background: Rectangle {
color: Color.mSurface
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
radius: Style.radiusXS * scaling
NText {
text: playerSelectorButton.currentPlayer ? playerSelectorButton.currentPlayer.identity : ""
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
Layout.fillWidth: true
}
}
delegate: ItemDelegate {
width: playerSelector.width
contentItem: NText {
text: modelData.identity
font.pointSize: Style.fontSizeS * scaling
color: highlighted ? Color.mSurface : Color.mOnSurface
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
highlighted: playerSelector.highlightedIndex === index
MouseArea {
id: playerSelectorMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
background: Rectangle {
width: popup.width - Style.marginS * scaling * 2
color: highlighted ? Color.mSecondary : Color.transparent
radius: Style.radiusXS * scaling
onClicked: {
// Create menu items from available players
var menuItems = []
var players = MediaService.getAvailablePlayers()
for (var i = 0; i < players.length; i++) {
menuItems.push({
"label": players[i].identity,
"action": i.toString(),
"icon": "disc",
"enabled": true,
"visible": true
})
}
playerContextMenu.model = menuItems
playerContextMenu.openAtItem(playerSelectorButton, playerSelectorButton.width - playerContextMenu.width, playerSelectorButton.height)
}
}
onActivated: {
MediaService.selectedPlayerIndex = currentIndex
MediaService.updateCurrentPlayer()
NContextMenu {
id: playerContextMenu
parent: root
width: 200 * scaling
onTriggered: function (action) {
var index = parseInt(action)
if (!isNaN(index)) {
MediaService.selectedPlayerIndex = index
MediaService.updateCurrentPlayer()
}
}
}
}
@@ -193,7 +173,7 @@ NBox {
NText {
visible: MediaService.trackArtist !== ""
text: MediaService.trackArtist
color: Color.mOnSurface
color: Color.mPrimary
font.pointSize: Style.fontSizeXS * scaling
elide: Text.ElideRight
Layout.fillWidth: true
@@ -300,7 +280,7 @@ NBox {
// Previous button
NIconButton {
icon: "media-prev"
tooltipText: "Previous Media"
tooltipText: I18n.tr("tooltips.previous-media")
visible: MediaService.canGoPrevious
onClicked: MediaService.canGoPrevious ? MediaService.previous() : {}
}
@@ -308,7 +288,7 @@ NBox {
// Play/Pause button
NIconButton {
icon: MediaService.isPlaying ? "media-pause" : "media-play"
tooltipText: MediaService.isPlaying ? "Pause" : "Play"
tooltipText: MediaService.isPlaying ? I18n.tr("tooltips.pause") : I18n.tr("tooltips.play")
visible: (MediaService.canPlay || MediaService.canPause)
onClicked: (MediaService.canPlay || MediaService.canPause) ? MediaService.playPause() : {}
}
@@ -316,7 +296,7 @@ NBox {
// Next button
NIconButton {
icon: "media-next"
tooltipText: "Next media"
tooltipText: I18n.tr("tooltips.next-media")
visible: MediaService.canGoNext
onClicked: MediaService.canGoNext ? MediaService.next() : {}
}
@@ -324,7 +304,7 @@ NBox {
}
Loader {
active: Settings.data.audio.visualizerType == "linear" && MediaService.isPlaying
active: Settings.data.audio.visualizerType == "linear"
Layout.alignment: Qt.AlignHCenter
sourceComponent: LinearSpectrum {
@@ -337,7 +317,7 @@ NBox {
}
Loader {
active: Settings.data.audio.visualizerType == "mirrored" && MediaService.isPlaying
active: Settings.data.audio.visualizerType == "mirrored"
Layout.alignment: Qt.AlignHCenter
sourceComponent: MirroredSpectrum {
@@ -350,7 +330,7 @@ NBox {
}
Loader {
active: Settings.data.audio.visualizerType == "wave" && MediaService.isPlaying
active: Settings.data.audio.visualizerType == "wave"
Layout.alignment: Qt.AlignHCenter
sourceComponent: WaveSpectrum {
@@ -26,7 +26,9 @@ NBox {
// Performance
NIconButton {
icon: PowerProfileService.getIcon(PowerProfile.Performance)
tooltipText: `Set "${PowerProfileService.getName(PowerProfile.Performance)}" power profile.`
tooltipText: I18n.tr("tooltips.set-power-profile", {
"profile": PowerProfileService.getName(PowerProfile.Performance)
})
enabled: hasPP
opacity: enabled ? Style.opacityFull : Style.opacityMedium
colorBg: (enabled && PowerProfileService.profile === PowerProfile.Performance) ? Color.mPrimary : Color.mSurfaceVariant
@@ -36,7 +38,9 @@ NBox {
// Balanced
NIconButton {
icon: PowerProfileService.getIcon(PowerProfile.Balanced)
tooltipText: `Set "${PowerProfileService.getName(PowerProfile.Balanced)}" power profile.`
tooltipText: I18n.tr("tooltips.set-power-profile", {
"profile": PowerProfileService.getName(PowerProfile.Balanced)
})
enabled: hasPP
opacity: enabled ? Style.opacityFull : Style.opacityMedium
colorBg: (enabled && PowerProfileService.profile === PowerProfile.Balanced) ? Color.mPrimary : Color.mSurfaceVariant
@@ -46,7 +50,9 @@ NBox {
// Eco
NIconButton {
icon: PowerProfileService.getIcon(PowerProfile.PowerSaver)
tooltipText: `Set "${PowerProfileService.getName(PowerProfile.PowerSaver)}" power profile.`
tooltipText: I18n.tr("tooltips.set-power-profile", {
"profile": PowerProfileService.getName(PowerProfile.PowerSaver)
})
enabled: hasPP
opacity: enabled ? Style.opacityFull : Style.opacityMedium
colorBg: (enabled && PowerProfileService.profile === PowerProfile.PowerSaver) ? Color.mPrimary : Color.mSurfaceVariant
@@ -4,8 +4,8 @@ import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.Modules.SettingsPanel
import qs.Modules.SidePanel
import qs.Modules.Settings
import qs.Modules.ControlCenter
import qs.Commons
import qs.Services
import qs.Widgets
@@ -42,7 +42,9 @@ NBox {
font.capitalization: Font.Capitalize
}
NText {
text: `System uptime: ${uptimeText}`
text: I18n.tr("system.uptime", {
"uptime": uptimeText
})
font.pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant
}
@@ -56,7 +58,7 @@ NBox {
}
NIconButton {
icon: "settings"
tooltipText: "Open settings."
tooltipText: I18n.tr("tooltips.open-settings")
onClicked: {
settingsPanel.requestedTab = SettingsPanel.Tab.General
settingsPanel.open()
@@ -66,19 +68,19 @@ NBox {
NIconButton {
id: powerButton
icon: "power"
tooltipText: "Power menu."
tooltipText: I18n.tr("tooltips.session-menu")
onClicked: {
powerPanel.open()
sidePanel.close()
sessionMenuPanel.open()
controlCenterPanel.close()
}
}
NIconButton {
id: closeButton
icon: "close"
tooltipText: "Close side panel."
tooltipText: I18n.tr("tooltips.close")
onClicked: {
sidePanel.close()
controlCenterPanel.close()
}
}
}
@@ -3,7 +3,7 @@ import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Modules.SettingsPanel
import qs.Modules.Settings
import qs.Services
import qs.Widgets
@@ -24,7 +24,7 @@ NBox {
NIconButton {
icon: "camera-video"
enabled: ScreenRecorderService.isAvailable
tooltipText: ScreenRecorderService.isAvailable ? (ScreenRecorderService.isRecording ? "Stop screen recording." : "Start screen recording.") : "Screen recorder not installed."
tooltipText: ScreenRecorderService.isAvailable ? (ScreenRecorderService.isRecording ? I18n.tr("tooltips.stop-screen-recording") : I18n.tr("tooltips.start-screen-recording")) : I18n.tr("tooltips.screen-recorder-not-installed")
colorBg: ScreenRecorderService.isRecording ? Color.mPrimary : Color.mSurfaceVariant
colorFg: ScreenRecorderService.isRecording ? Color.mOnPrimary : Color.mPrimary
onClicked: {
@@ -33,8 +33,8 @@ NBox {
ScreenRecorderService.toggleRecording()
// If we were not recording and we just initiated a start, close the panel
if (!ScreenRecorderService.isRecording) {
var panel = PanelService.getPanel("sidePanel")
panel && panel.close()
var panel = PanelService.getPanel("controlCenterPanel")
panel?.close()
}
}
}
@@ -42,7 +42,7 @@ NBox {
// Idle Inhibitor
NIconButton {
icon: IdleInhibitorService.isInhibited ? "keep-awake-on" : "keep-awake-off"
tooltipText: IdleInhibitorService.isInhibited ? "Disable keep awake." : "Enable keep awake."
tooltipText: IdleInhibitorService.isInhibited ? I18n.tr("tooltips.disable-keep-awake") : I18n.tr("tooltips.enable-keep-awake")
colorBg: IdleInhibitorService.isInhibited ? Color.mPrimary : Color.mSurfaceVariant
colorFg: IdleInhibitorService.isInhibited ? Color.mOnPrimary : Color.mPrimary
onClicked: {
@@ -54,8 +54,8 @@ NBox {
NIconButton {
visible: Settings.data.wallpaper.enabled
icon: "wallpaper-selector"
tooltipText: "Left click: Open wallpaper selector.\nRight click: Set random wallpaper."
onClicked: PanelService.getPanel("wallpaperSelector")?.toggle(this)
tooltipText: I18n.tr("tooltips.wallpaper-selector")
onClicked: PanelService.getPanel("wallpaperPanel")?.toggle(this)
onRightClicked: WallpaperService.setRandomWallpaper()
}
@@ -2,7 +2,7 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Modules.SidePanel.Cards
import qs.Modules.ControlCenter.Cards
import qs.Commons
import qs.Services
import qs.Widgets
+188 -16
View File
@@ -13,6 +13,7 @@ Variants {
model: Quickshell.screens
delegate: Item {
id: root
required property ShellScreen modelData
property real scaling: ScalingService.getScreenScale(modelData)
@@ -25,6 +26,37 @@ Variants {
}
}
// Update dock apps when toplevels change
Connections {
target: ToplevelManager ? ToplevelManager.toplevels : null
function onValuesChanged() {
updateDockApps()
}
}
// Update dock apps when pinned apps change
Connections {
target: Settings.data.dock
function onPinnedAppsChanged() {
updateDockApps()
}
}
// Initial update when component is ready
Component.onCompleted: {
if (Settings.isLoaded && ToplevelManager) {
updateDockApps()
}
}
// Update when Settings are loaded
Connections {
target: Settings
function onSettingsLoaded() {
updateDockApps()
}
}
// Shared properties between peek and dock windows
readonly property bool autoHide: Settings.data.dock.autoHide
readonly property int hideDelay: 500
@@ -43,12 +75,66 @@ Variants {
// Shared state between windows
property bool dockHovered: false
property bool anyAppHovered: false
property bool menuHovered: false
property bool hidden: autoHide
property bool peekHovered: false
// Separate property to control Loader - stays true during animations
property bool dockLoaded: !autoHide // Start loaded if autoHide is off
// Track the currently open context menu
property var currentContextMenu: null
// Combined model of running apps and pinned apps
property var dockApps: []
// Function to close any open context menu
function closeAllContextMenus() {
if (currentContextMenu && currentContextMenu.visible) {
currentContextMenu.hide()
}
}
// Function to update the combined dock apps model
function updateDockApps() {
const runningApps = ToplevelManager ? (ToplevelManager.toplevels.values || []) : []
const pinnedApps = Settings.data.dock.pinnedApps || []
const combined = []
const processedAppIds = new Set()
// Strategy: Maintain app positions as much as possible
// 1. First pass: Add all running apps (both pinned and non-pinned) in their current order
runningApps.forEach(toplevel => {
if (toplevel && toplevel.appId) {
const isPinned = pinnedApps.includes(toplevel.appId)
const appType = isPinned ? "pinned-running" : "running"
combined.push({
"type": appType,
"toplevel": toplevel,
"appId": toplevel.appId,
"title": toplevel.title
})
processedAppIds.add(toplevel.appId)
}
})
// 2. Second pass: Add non-running pinned apps at the end
pinnedApps.forEach(pinnedAppId => {
if (!processedAppIds.has(pinnedAppId)) {
// Pinned app that is not running
combined.push({
"type": "pinned",
"toplevel": null,
"appId": pinnedAppId,
"title": pinnedAppId
})
}
})
dockApps = combined
}
// Timer to unload dock after hide animation completes
Timer {
id: unloadTimer
@@ -65,7 +151,7 @@ Variants {
id: hideTimer
interval: hideDelay
onTriggered: {
if (autoHide && !dockHovered && !anyAppHovered && !peekHovered) {
if (autoHide && !dockHovered && !anyAppHovered && !peekHovered && !menuHovered) {
hidden = true
unloadTimer.restart() // Start unload timer when hiding
}
@@ -137,7 +223,7 @@ Variants {
onExited: {
peekHovered = false
if (!hidden && !dockHovered && !anyAppHovered) {
if (!hidden && !dockHovered && !anyAppHovered && !menuHovered) {
hideTimer.restart()
}
}
@@ -147,7 +233,7 @@ Variants {
// DOCK WINDOW
Loader {
active: Settings.isLoaded && modelData && Settings.data.dock.monitors.includes(modelData.name) && dockLoaded && ToplevelManager && (ToplevelManager.toplevels.values.length > 0)
active: Settings.isLoaded && modelData && Settings.data.dock.monitors.includes(modelData.name) && dockLoaded && ToplevelManager && (dockApps.length > 0)
sourceComponent: PanelWindow {
id: dockWindow
@@ -235,10 +321,15 @@ Variants {
onExited: {
dockHovered = false
if (autoHide && !anyAppHovered && !peekHovered) {
if (autoHide && !anyAppHovered && !peekHovered && !menuHovered) {
hideTimer.restart()
}
}
onClicked: {
// Close any open context menu when clicking on the dock background
closeAllContextMenus()
}
}
Item {
@@ -247,10 +338,10 @@ Variants {
height: parent.height - (Style.marginM * 2 * scaling)
anchors.centerIn: parent
function getAppIcon(toplevel: Toplevel): string {
if (!toplevel)
function getAppIcon(appData): string {
if (!appData || !appData.appId)
return ""
return AppIcons.iconForAppId(toplevel.appId?.toLowerCase())
return ThemeIcons.iconForAppId(appData.appId?.toLowerCase())
}
RowLayout {
@@ -260,7 +351,7 @@ Variants {
anchors.centerIn: parent
Repeater {
model: ToplevelManager ? ToplevelManager.toplevels : null
model: dockApps
delegate: Item {
id: appButton
@@ -268,10 +359,19 @@ Variants {
Layout.preferredHeight: iconSize
Layout.alignment: Qt.AlignCenter
property bool isActive: ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData
property bool isActive: modelData.toplevel && ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData.toplevel
property bool hovered: appMouseArea.containsMouse
property string appId: modelData ? modelData.appId : ""
property string appTitle: modelData ? modelData.title : ""
property string appTitle: modelData ? (modelData.title || modelData.appId) : ""
property bool isRunning: modelData && (modelData.type === "running" || modelData.type === "pinned-running")
// Listen for the toplevel being closed
Connections {
target: modelData?.toplevel
function onClosed() {
Qt.callLater(root.updateDockApps)
}
}
// Individual tooltip for this app
NTooltip {
@@ -296,6 +396,9 @@ Variants {
fillMode: Image.PreserveAspectFit
cache: true
// Dim pinned apps that aren't running
opacity: appButton.isRunning ? 1.0 : 0.6
scale: appButton.hovered ? 1.15 : 1.0
Behavior on scale {
@@ -305,6 +408,13 @@ Variants {
easing.overshoot: 1.2
}
}
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutQuad
}
}
}
// Fall back if no icon
@@ -314,6 +424,7 @@ Variants {
icon: "question-mark"
font.pointSize: iconSize * 0.7
color: appButton.isActive ? Color.mPrimary : Color.mOnSurfaceVariant
opacity: appButton.isRunning ? 1.0 : 0.6
scale: appButton.hovered ? 1.15 : 1.0
Behavior on scale {
@@ -323,6 +434,41 @@ Variants {
easing.overshoot: 1.2
}
}
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutQuad
}
}
}
// Context menu popup
DockMenu {
id: contextMenu
scaling: root.scaling
onHoveredChanged: menuHovered = hovered
onRequestClose: {
contextMenu.hide()
// Restart hide timer after menu action if auto-hide is enabled
if (autoHide && !dockHovered && !anyAppHovered && !peekHovered) {
hideTimer.restart()
}
}
onAppClosed: root.updateDockApps // Force immediate dock update when app is closed
onVisibleChanged: {
if (visible) {
root.currentContextMenu = contextMenu
} else if (root.currentContextMenu === contextMenu) {
root.currentContextMenu = null
// Reset menu hover state when menu becomes invisible
menuHovered = false
// Restart hide timer if conditions are met
if (autoHide && !dockHovered && !anyAppHovered && !peekHovered) {
hideTimer.restart()
}
}
}
}
MouseArea {
@@ -330,7 +476,7 @@ Variants {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
onEntered: {
anyAppHovered = true
@@ -347,17 +493,43 @@ Variants {
onExited: {
anyAppHovered = false
appTooltip.hide()
if (autoHide && !dockHovered && !peekHovered) {
if (autoHide && !dockHovered && !peekHovered && !menuHovered) {
hideTimer.restart()
}
}
onClicked: function (mouse) {
if (mouse.button === Qt.MiddleButton && modelData?.close) {
modelData.close()
if (mouse.button === Qt.RightButton) {
// If right-clicking on the same app with an open context menu, close it
if (root.currentContextMenu === contextMenu && contextMenu.visible) {
root.closeAllContextMenus()
return
}
// Close any other existing context menu first
root.closeAllContextMenus()
// Hide tooltip when showing context menu
appTooltip.hide()
contextMenu.show(appButton, modelData.toplevel || modelData)
return
}
if (mouse.button === Qt.LeftButton && modelData?.activate) {
modelData.activate()
// Close any existing context menu for non-right-click actions
root.closeAllContextMenus()
// Check if toplevel is still valid (not a stale reference)
const isValidToplevel = modelData?.toplevel && ToplevelManager && ToplevelManager.toplevels.values.includes(modelData.toplevel)
if (mouse.button === Qt.MiddleButton && isValidToplevel && modelData.toplevel.close) {
modelData.toplevel.close()
Qt.callLater(root.updateDockApps) // Force immediate dock update
} else if (mouse.button === Qt.LeftButton) {
if (isValidToplevel && modelData.toplevel.activate) {
// Running app - activate it
modelData.toplevel.activate()
} else if (modelData?.appId) {
// Pinned app not running - launch it
Quickshell.execDetached(["gtk-launch", modelData.appId])
}
}
}
}
+264
View File
@@ -0,0 +1,264 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Commons
import qs.Services
import qs.Widgets
PopupWindow {
id: root
property var toplevel: null
property Item anchorItem: null
property real scaling: 1.0
property bool hovered: menuMouseArea.containsMouse || activateMouseArea.containsMouse || pinMouseArea.containsMouse || closeMouseArea.containsMouse
property var onAppClosed: null // Callback function for when an app is closed
signal requestClose
implicitWidth: 140 * scaling
implicitHeight: contextMenuColumn.implicitHeight + (Style.marginM * scaling * 2)
color: Color.transparent
visible: false
// Helper functions for pin/unpin functionality
function isAppPinned(appId) {
if (!appId)
return false
const pinnedApps = Settings.data.dock.pinnedApps || []
return pinnedApps.includes(appId)
}
function toggleAppPin(appId) {
if (!appId)
return
let pinnedApps = (Settings.data.dock.pinnedApps || []).slice() // Create a copy
const isPinned = pinnedApps.includes(appId)
if (isPinned) {
// Unpin: remove from array
pinnedApps = pinnedApps.filter(id => id !== appId)
} else {
// Pin: add to array
pinnedApps.push(appId)
}
// Update the settings
Settings.data.dock.pinnedApps = pinnedApps
}
anchor.item: anchorItem
anchor.rect.x: anchorItem ? (anchorItem.width - implicitWidth) / 2 : 0
anchor.rect.y: anchorItem ? -implicitHeight - (Style.marginM * scaling) : 0
function show(item, toplevelData) {
if (!item) {
Logger.warn("DockMenu", "anchorItem is undefined, won't show menu.")
return
}
anchorItem = item
toplevel = toplevelData
visible = true
}
function hide() {
visible = false
}
// Close menu when clicking on background, track hover for the whole menu area
MouseArea {
id: menuMouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: function (mouse) {
if (mouse.button === Qt.RightButton) {
root.hide() // Close on right-click
} else {
root.hide() // Close when clicking on the background (outside menu content)
}
}
}
Shortcut {
sequences: ["Escape"]
enabled: root.visible
onActivated: root.hide()
}
Rectangle {
anchors.fill: parent
color: Color.mSurfaceVariant
radius: Style.radiusS * scaling
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
// Prevent clicks inside the menu from closing it
MouseArea {
anchors.fill: parent
onClicked: {
} // Do nothing, just consume the click
}
Column {
id: contextMenuColumn
anchors.fill: parent
anchors.margins: Style.marginM * scaling
spacing: 0
// Focus item
Rectangle {
width: parent.width
height: 32 * scaling
color: activateMouseArea.containsMouse ? Color.mTertiary : Color.transparent
radius: Style.radiusXS * scaling
Row {
anchors.left: parent.left
anchors.leftMargin: Style.marginS * scaling
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
NIcon {
icon: "eye"
font.pointSize: Style.fontSizeL * scaling
color: activateMouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurfaceVariant
anchors.verticalCenter: parent.verticalCenter
}
NText {
text: I18n.tr("dock.menu.focus")
font.pointSize: Style.fontSizeS * scaling
color: activateMouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurfaceVariant
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: activateMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.toplevel?.activate) {
root.toplevel.activate()
}
root.requestClose()
}
}
}
// Pin/Unpin item
Rectangle {
width: parent.width
height: 32 * scaling
color: pinMouseArea.containsMouse ? Color.mTertiary : Color.transparent
radius: Style.radiusXS * scaling
Row {
anchors.left: parent.left
anchors.leftMargin: Style.marginS * scaling
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
NIcon {
icon: {
if (!root.toplevel)
return "pin"
return root.isAppPinned(root.toplevel.appId) ? "unpin" : "pin"
}
font.pointSize: Style.fontSizeL * scaling
color: pinMouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurfaceVariant
anchors.verticalCenter: parent.verticalCenter
}
NText {
text: {
if (!root.toplevel)
return I18n.tr("dock.menu.pin")
return root.isAppPinned(root.toplevel.appId) ? I18n.tr("dock.menu.unpin") : I18n.tr("dock.menu.pin")
}
font.pointSize: Style.fontSizeS * scaling
color: pinMouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurfaceVariant
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: pinMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.toplevel?.appId) {
root.toggleAppPin(root.toplevel.appId)
}
//root.hide()
root.requestClose()
}
}
}
// Close item
Rectangle {
width: parent.width
height: 32 * scaling
color: closeMouseArea.containsMouse ? Color.mTertiary : Color.transparent
radius: Style.radiusXS * scaling
Row {
anchors.left: parent.left
anchors.leftMargin: Style.marginS * scaling
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
NIcon {
icon: "close"
font.pointSize: Style.fontSizeL * scaling
color: closeMouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurfaceVariant
anchors.verticalCenter: parent.verticalCenter
}
NText {
text: I18n.tr("dock.menu.close")
font.pointSize: Style.fontSizeS * scaling
color: closeMouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurfaceVariant
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: closeMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
// Check if toplevel is still valid before trying to close it
const isValidToplevel = root.toplevel && ToplevelManager && ToplevelManager.toplevels.values.includes(root.toplevel)
if (isValidToplevel && root.toplevel.close) {
root.toplevel.close()
// Trigger immediate dock update callback if provided
if (root.onAppClosed && typeof root.onAppClosed === "function") {
Qt.callLater(root.onAppClosed)
}
} else {
Logger.warn("DockMenu", "Cannot close app - invalid toplevel reference")
}
root.hide()
root.requestClose()
}
}
}
}
}
}
+2 -2
View File
@@ -245,7 +245,7 @@ NPanel {
fontWeight: Style.fontWeightSemiBold
text: searchText
placeholderText: "Search entries... or use > for commands"
placeholderText: I18n.tr("placeholders.search-launcher")
onTextChanged: searchText = text
@@ -397,7 +397,7 @@ NPanel {
sourceComponent: Component {
IconImage {
anchors.fill: parent
source: modelData.icon ? AppIcons.iconFromName(modelData.icon, "application-x-executable") : ""
source: modelData.icon ? ThemeIcons.iconFromName(modelData.icon, "application-x-executable") : ""
visible: modelData.icon && source !== ""
asynchronous: true
}
@@ -7,7 +7,7 @@ import "../../../Helpers/FuzzySort.js" as Fuzzysort
Item {
property var launcher: null
property string name: "Applications"
property string name: I18n.tr("plugins.applications")
property bool handleSearch: true
property var entries: []
@@ -1,10 +1,11 @@
import QtQuick
import qs.Services
import qs.Commons
import "../../../Helpers/AdvancedMath.js" as AdvancedMath
Item {
property var launcher: null
property string name: "Calculator"
property string name: I18n.tr("plugins.calculator")
function handleCommand(query) {
// Handle >calc command or direct math expressions after >
@@ -14,7 +15,7 @@ Item {
function commands() {
return [{
"name": ">calc",
"description": "Calculator - evaluate mathematical expressions",
"description": I18n.tr("plugins.calculator-description"),
"icon": "accessories-calculator",
"isImage": false,
"onActivate": function () {
@@ -36,8 +37,8 @@ Item {
if (!expression) {
return [{
"name": "Calculator",
"description": "Enter a mathematical expression",
"name": I18n.tr("plugins.calculator-name"),
"description": I18n.tr("plugins.calculator-enter-expression"),
"icon": "accessories-calculator",
"isImage": false,
"onActivate": function () {}
@@ -59,7 +60,7 @@ Item {
}]
} catch (error) {
return [{
"name": "Error",
"name": I18n.tr("plugins.calculator-error"),
"description": error.message || "Invalid expression",
"icon": "dialog-error",
"isImage": false,
+11 -11
View File
@@ -7,7 +7,7 @@ Item {
id: root
// Plugin metadata
property string name: "Clipboard History"
property string name: I18n.tr("plugins.clipboard")
property var launcher: null
// Plugin capabilities
@@ -68,7 +68,7 @@ Item {
function commands() {
return [{
"name": ">clip",
"description": "Search clipboard history",
"description": I18n.tr("plugins.clipboard-search-description"),
"icon": "text-x-generic",
"isImage": false,
"onActivate": function () {
@@ -76,7 +76,7 @@ Item {
}
}, {
"name": ">clip clear",
"description": "Clear all clipboard history",
"description": I18n.tr("plugins.clipboard-clear-description"),
"icon": "text-x-generic",
"isImage": false,
"onActivate": function () {
@@ -99,8 +99,8 @@ Item {
// Check if clipboard service is not active
if (!ClipboardService.active) {
return [{
"name": "Clipboard History Disabled",
"description": "Enable clipboard history in settings or install cliphist",
"name": I18n.tr("plugins.clipboard-history-disabled"),
"description": I18n.tr("plugins.clipboard-history-disabled-description"),
"icon": "view-refresh",
"isImage": false,
"onActivate": function () {}
@@ -110,8 +110,8 @@ Item {
// Special command: clear
if (query === "clear") {
return [{
"name": "Clear Clipboard History",
"description": "Remove all items from clipboard history",
"name": I18n.tr("plugins.clipboard-clear-history"),
"description": I18n.tr("plugins.clipboard-clear-description-full"),
"icon": "delete_sweep",
"isImage": false,
"onActivate": function () {
@@ -124,8 +124,8 @@ Item {
// Show loading state if data is being loaded
if (ClipboardService.loading || isWaitingForData) {
return [{
"name": "Loading clipboard history...",
"description": "Please wait",
"name": I18n.tr("plugins.clipboard-loading"),
"description": I18n.tr("plugins.clipboard-loading-description"),
"icon": "view-refresh",
"isImage": false,
"onActivate": function () {}
@@ -140,8 +140,8 @@ Item {
isWaitingForData = true
ClipboardService.list(100)
return [{
"name": "Loading clipboard history...",
"description": "Please wait",
"name": I18n.tr("plugins.clipboard-loading"),
"description": I18n.tr("plugins.clipboard-loading-description"),
"icon": "view-refresh",
"isImage": false,
"onActivate": function () {}
+209 -75
View File
@@ -199,7 +199,7 @@ Loader {
z: 10
Loader {
active: MediaService.isPlaying && Settings.data.audio.visualizerType == "linear"
active: Settings.data.audio.visualizerType == "linear"
anchors.centerIn: parent
width: 160 * scaling
height: 160 * scaling
@@ -228,7 +228,7 @@ Loader {
}
Loader {
active: MediaService.isPlaying && Settings.data.audio.visualizerType == "mirrored"
active: Settings.data.audio.visualizerType == "mirrored"
anchors.centerIn: parent
width: 160 * scaling
height: 160 * scaling
@@ -258,7 +258,7 @@ Loader {
}
Loader {
active: MediaService.isPlaying && Settings.data.audio.visualizerType == "wave"
active: Settings.data.audio.visualizerType == "wave"
anchors.centerIn: parent
width: 160 * scaling
height: 160 * scaling
@@ -305,31 +305,6 @@ Loader {
}
}
Rectangle {
anchors.centerIn: parent
width: parent.width + 24 * scaling
height: parent.height + 24 * scaling
radius: width * 0.5
color: Color.transparent
border.color: Qt.alpha(Color.mPrimary, 0.3)
border.width: Math.max(1, Style.borderM * scaling)
z: -1
visible: !MediaService.isPlaying
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
}
}
}
NImageCircled {
anchors.centerIn: parent
width: 100 * scaling
@@ -364,6 +339,7 @@ Loader {
Rectangle {
id: terminalBackground
anchors.fill: parent
clip: true
radius: Style.radiusM * scaling
color: Qt.alpha(Color.mSurface, 0.9)
border.color: Color.mPrimary
@@ -407,7 +383,7 @@ Loader {
spacing: Style.marginL * scaling
NText {
text: "SECURE TERMINAL"
text: I18n.tr("lock-screen.secure-terminal")
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
@@ -479,7 +455,9 @@ Loader {
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
property int currentIndex: 0
property string fullText: "Welcome back, " + Quickshell.env("USER") + "!"
property string fullText: I18n.tr("system.welcome-back", {
"user": Quickshell.env("USER")
})
Timer {
interval: Style.animationFast
@@ -510,7 +488,7 @@ Loader {
}
NText {
text: "sudo unlock-session"
text: I18n.tr("lock-screen.unlock-command")
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
@@ -522,7 +500,7 @@ Loader {
spacing: Style.marginM * scaling
NText {
text: "Password:"
text: I18n.tr("lock-screen.password")
color: Color.mPrimary
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
@@ -558,48 +536,60 @@ Loader {
}
}
NText {
id: asterisksText
text: "*".repeat(passwordInput.text.length)
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
visible: passwordInput.activeFocus && !lockContext.unlockInProgress
// Container for asterisks and cursor to control positioning
Item {
Layout.fillWidth: true
Layout.preferredHeight: asterisksText.implicitHeight
SequentialAnimation {
id: typingEffect
NumberAnimation {
target: passwordInput
property: "scale"
to: 1.01
duration: 50
}
NumberAnimation {
target: passwordInput
property: "scale"
to: 1.0
duration: 50
NText {
id: asterisksText
text: "*".repeat(passwordInput.text.length)
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
visible: passwordInput.activeFocus && !lockContext.unlockInProgress
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
wrapMode: Text.NoWrap
maximumLineCount: 1
elide: Text.ElideRight
SequentialAnimation {
id: typingEffect
NumberAnimation {
target: passwordInput
property: "scale"
to: 1.01
duration: 50
}
NumberAnimation {
target: passwordInput
property: "scale"
to: 1.0
duration: 50
}
}
}
}
Rectangle {
width: 8 * scaling
height: 20 * scaling
color: Color.mPrimary
visible: passwordInput.activeFocus
Layout.leftMargin: -Style.marginS * scaling
Layout.alignment: Qt.AlignVCenter
Rectangle {
width: 8 * scaling
height: 20 * scaling
color: Color.mPrimary
visible: passwordInput.activeFocus
anchors.left: asterisksText.right
anchors.leftMargin: Style.marginXS * scaling
anchors.verticalCenter: parent.verticalCenter
SequentialAnimation on opacity {
loops: Animation.Infinite
NumberAnimation {
to: 1.0
duration: 500
}
NumberAnimation {
to: 0.0
duration: 500
SequentialAnimation on opacity {
loops: Animation.Infinite
NumberAnimation {
to: 1.0
duration: 500
}
NumberAnimation {
to: 0.0
duration: 500
}
}
}
}
@@ -643,6 +633,7 @@ Loader {
RowLayout {
Layout.alignment: Qt.AlignRight
Layout.bottomMargin: -10 * scaling
Layout.fillWidth: true
Rectangle {
Layout.preferredWidth: 120 * scaling
Layout.preferredHeight: 40 * scaling
@@ -731,6 +722,149 @@ Loader {
}
}
// ALARMING Easter Egg for long passwords
Item {
id: easterEggContainer
anchors.fill: parent
z: 1000
property bool easterEggTriggered: false
// Monitor password length
Connections {
target: passwordInput
function onTextChanged() {
if (passwordInput.text.length >= 25) {
easterEggContainer.easterEggTriggered = true
}
}
function onActiveFocusChanged() {
if (!passwordInput.activeFocus) {
easterEggContainer.easterEggTriggered = false
}
}
}
// Also reset when authentication starts
Connections {
target: lockContext
function onUnlockInProgressChanged() {
if (lockContext.unlockInProgress) {
easterEggContainer.easterEggTriggered = false
}
}
}
// Scattered warning messages (game-style pop-ups)
Repeater {
model: easterEggContainer.easterEggTriggered && passwordInput.activeFocus && !lockContext.unlockInProgress ? 12 : 0
NText {
property var messages: ["BREACH DETECTED", "SECURITY ALERT", "SYSTEM COMPROMISED", "ANOMALY DETECTED", "FIREWALL BREACH", "DEFENSE FAILING", "16 // 16 // 16", "THE ATLAS SEES ALL", "SIMULATION DETECTED", "WAKE UP", "16 16 16 16 16", "KZZT... 16... KZZT", "ERROR ERROR ERROR", "THEY'RE WATCHING", "16 MINUTES REMAIN"]
property real baseX: Math.random() * (parent.width - 300)
property real baseY: Math.random() * (parent.height - 80)
text: messages[index % messages.length]
color: Color.mError
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
x: baseX
y: baseY
// Better random positioning avoiding center terminal
Component.onCompleted: {
var centerX = parent.width / 2
var centerY = parent.height / 2
var avoidRadius = 350 * scaling
// If too close to center, push to random edge zones
var distanceFromCenter = Math.sqrt((x - centerX) * (x - centerX) + (y - centerY) * (y - centerY))
if (distanceFromCenter < avoidRadius) {
// Pick a random edge zone
var zone = Math.floor(Math.random() * 4)
switch (zone) {
case 0:
// Top
x = Math.random() * parent.width
y = Math.random() * 100 * scaling
break
case 1:
// Right
x = parent.width - (50 + Math.random() * 200) * scaling
y = Math.random() * parent.height
break
case 2:
// Bottom
x = Math.random() * parent.width
y = parent.height - (50 + Math.random() * 100) * scaling
break
case 3:
// Left
x = Math.random() * 200 * scaling
y = Math.random() * parent.height
break
}
}
// Add some random drift to make positioning more varied
x += (Math.random() - 0.5) * 100 * scaling
y += (Math.random() - 0.5) * 50 * scaling
// Ensure we stay within bounds
x = Math.max(20 * scaling, Math.min(parent.width - 280 * scaling, x))
y = Math.max(20 * scaling, Math.min(parent.height - 60 * scaling, y))
}
// Simple pop-in animation
SequentialAnimation on scale {
loops: Animation.Infinite
PauseAnimation {
duration: index * 400 + Math.random() * 1000
}
NumberAnimation {
from: 0
to: 1.2
duration: 300
easing.type: Easing.OutBack
}
NumberAnimation {
to: 1.0
duration: 200
}
PauseAnimation {
duration: 2000 + Math.random() * 3000
}
NumberAnimation {
to: 0
duration: 300
}
PauseAnimation {
duration: 800 + Math.random() * 1200
}
}
// Gentle blinking effect
SequentialAnimation on opacity {
loops: Animation.Infinite
PauseAnimation {
duration: index * 200
}
NumberAnimation {
to: 0.6
duration: 400 + Math.random() * 300
}
NumberAnimation {
to: 1.0
duration: 300 + Math.random() * 200
}
}
}
}
}
// Power buttons at bottom right
RowLayout {
anchors.right: parent.right
@@ -759,7 +893,7 @@ Loader {
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.top
anchors.bottomMargin: 12 * scaling
anchors.bottomMargin: Style.marginM * scaling
radius: Style.radiusM * scaling
color: Color.mSurface
border.color: Color.mOutline
@@ -770,7 +904,7 @@ Loader {
id: shutdownTooltipText
anchors.margins: Style.marginM * scaling
anchors.fill: parent
text: "Shut down."
text: I18n.tr("lock-screen.shut-down")
font.pointSize: Style.fontSizeM * scaling
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
@@ -810,7 +944,7 @@ Loader {
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.top
anchors.bottomMargin: 12 * scaling
anchors.bottomMargin: Style.marginM * scaling
radius: Style.radiusM * scaling
color: Color.mSurface
border.color: Color.mOutline
@@ -821,7 +955,7 @@ Loader {
id: restartTooltipText
anchors.margins: Style.marginM * scaling
anchors.fill: parent
text: "Restart."
text: I18n.tr("lock-screen.restart")
font.pointSize: Style.fontSizeM * scaling
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
@@ -862,7 +996,7 @@ Loader {
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.top
anchors.bottomMargin: 12 * scaling
anchors.bottomMargin: Style.marginM * scaling
radius: Style.radiusM * scaling
color: Color.mSurface
border.color: Color.mOutline
@@ -873,7 +1007,7 @@ Loader {
id: suspendTooltipText
anchors.margins: Style.marginM * scaling
anchors.fill: parent
text: "Suspend."
text: I18n.tr("lock-screen.suspend")
font.pointSize: Style.fontSizeM * scaling
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
+191 -121
View File
@@ -16,97 +16,113 @@ Variants {
id: root
required property ShellScreen modelData
readonly property real scaling: ScalingService.getScreenScale(modelData)
property real scaling: ScalingService.getScreenScale(modelData)
// Access the notification model from the service
property ListModel notificationModel: NotificationService.notificationModel
// Track notifications being removed for animation
property var removingNotifications: ({})
// Access the notification model from the service - UPDATED NAME
property ListModel notificationModel: NotificationService.activeList
// If no notification display activated in settings, then show them all
active: Settings.isLoaded && modelData && (NotificationService.notificationModel.count > 0) ? (Settings.data.notifications.monitors.includes(modelData.name) || (Settings.data.notifications.monitors.length === 0)) : false
active: Settings.isLoaded && modelData && (Settings.data.notifications.monitors.includes(modelData.name) || (Settings.data.notifications.monitors.length === 0))
visible: (NotificationService.notificationModel.count > 0)
Connections {
target: ScalingService
function onScaleChanged(screenName, scale) {
if (root.modelData && screenName === root.modelData.name) {
root.scaling = scale
}
}
}
sourceComponent: PanelWindow {
screen: modelData
WlrLayershell.namespace: "noctalia-notifications"
WlrLayershell.layer: (Settings.isLoaded && Settings.data && Settings.data.notifications && Settings.data.notifications.alwaysOnTop) ? WlrLayer.Overlay : WlrLayer.Top
color: Color.transparent
// Position based on bar location - always at top
anchors.top: true
anchors.right: Settings.data.bar.position === "right" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom"
anchors.left: Settings.data.bar.position === "left"
readonly property string location: (Settings.isLoaded && Settings.data && Settings.data.notifications && Settings.data.notifications.location) ? Settings.data.notifications.location : "top_right"
readonly property bool isTop: (location === "top") || (location.length >= 3 && location.substring(0, 3) === "top")
readonly property bool isBottom: (location === "bottom") || (location.length >= 6 && location.substring(0, 6) === "bottom")
readonly property bool isLeft: location.indexOf("_left") >= 0
readonly property bool isRight: location.indexOf("_right") >= 0
readonly property bool isCentered: (location === "top" || location === "bottom")
// Anchor selection based on location (window edges)
anchors.top: isTop
anchors.bottom: isBottom
anchors.left: isLeft
anchors.right: isRight
// Margins depending on bar position and chosen location
margins.top: {
switch (Settings.data.bar.position) {
case "top":
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0)
default:
return Style.marginM * scaling
if (!(anchors.top))
return 0
var base = Style.marginM * scaling
if (Settings.data.bar.position === "top") {
var floatExtraV = Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0
return (Style.barHeight * scaling) + base + floatExtraV
}
return base
}
margins.bottom: {
switch (Settings.data.bar.position) {
case "bottom":
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0)
default:
if (!(anchors.bottom))
return 0
var base = Style.marginM * scaling
if (Settings.data.bar.position === "bottom") {
var floatExtraV = Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0
return (Style.barHeight * scaling) + base + floatExtraV
}
return base
}
margins.left: {
switch (Settings.data.bar.position) {
case "left":
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0)
default:
if (!(anchors.left))
return 0
var base = Style.marginM * scaling
if (Settings.data.bar.position === "left") {
var floatExtraH = Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0
return (Style.barHeight * scaling) + base + floatExtraH
}
return base
}
margins.right: {
switch (Settings.data.bar.position) {
case "right":
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0)
case "top":
case "bottom":
return Style.marginM * scaling
default:
if (!(anchors.right))
return 0
var base = Style.marginM * scaling
if (Settings.data.bar.position === "right") {
var floatExtraH = Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0
return (Style.barHeight * scaling) + base + floatExtraH
}
return base
}
implicitWidth: 360 * scaling
implicitHeight: Math.min(notificationStack.implicitHeight, (NotificationService.maxVisible * 120) * scaling)
//WlrLayershell.layer: WlrLayer.Overlay
implicitHeight: notificationStack.implicitHeight
WlrLayershell.exclusionMode: ExclusionMode.Ignore
// Connect to animation signal from service
// Connect to animation signal from service - UPDATED TO USE ID
Component.onCompleted: {
NotificationService.animateAndRemove.connect(function (notification, index) {
// Prefer lookup by identity to avoid index mismatches
NotificationService.animateAndRemove.connect(function (notificationId) {
// Find the delegate by notification ID
var delegate = null
if (notificationStack && notificationStack.children && notificationStack.children.length > 0) {
for (var i = 0; i < notificationStack.children.length; i++) {
var child = notificationStack.children[i]
if (child && child.model && child.model.rawNotification === notification) {
if (child && child.notificationId === notificationId) {
delegate = child
break
}
}
}
// Fallback to index if identity lookup failed
if (!delegate && notificationStack && notificationStack.children && notificationStack.children[index]) {
delegate = notificationStack.children[index]
}
if (delegate && delegate.animateOut) {
delegate.animateOut()
} else {
// As a last resort, force-remove without animation to avoid stuck popups
NotificationService.forceRemoveNotification(notification)
// Force removal without animation as fallback
NotificationService.dismissActiveNotification(notificationId)
}
})
}
@@ -114,10 +130,12 @@ Variants {
// Main notification container
ColumnLayout {
id: notificationStack
// Position based on bar location - always at top
anchors.top: parent.top
anchors.right: (Settings.data.bar.position === "right" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom") ? parent.right : undefined
anchors.left: Settings.data.bar.position === "left" ? parent.left : undefined
// Anchor the stack inside the window based on chosen location
anchors.top: parent.isTop ? parent.top : undefined
anchors.bottom: parent.isBottom ? parent.bottom : undefined
anchors.left: parent.isLeft ? parent.left : undefined
anchors.right: parent.isRight ? parent.right : undefined
anchors.horizontalCenter: parent.isCentered ? parent.horizontalCenter : undefined
spacing: Style.marginS * scaling
width: 360 * scaling
visible: true
@@ -126,6 +144,9 @@ Variants {
Repeater {
model: notificationModel
delegate: Rectangle {
// Store the notification ID for reference
property string notificationId: model.id
Layout.preferredWidth: 360 * scaling
Layout.preferredHeight: notificationLayout.implicitHeight + (Style.marginL * 2 * scaling)
Layout.maximumHeight: Layout.preferredHeight
@@ -135,6 +156,32 @@ Variants {
border.width: Math.max(1, Style.borderS * scaling)
color: Color.mSurface
Rectangle {
id: progressBar
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
height: 2 * scaling
color: "transparent"
property real availableWidth: parent.width - (2 * parent.radius)
Rectangle {
x: parent.parent.radius + (parent.availableWidth * (1 - model.progress)) / 2
width: parent.availableWidth * model.progress
height: parent.height
color: {
if (model.urgency === NotificationUrgency.Critical || model.urgency === 2)
return Color.mError
else if (model.urgency === NotificationUrgency.Low || model.urgency === 0)
return Color.mOnSurface
else
return Color.mPrimary
}
antialiasing: true
}
}
// Animation properties
property real scaleValue: 0.8
property real opacityValue: 0.0
@@ -174,14 +221,14 @@ Variants {
interval: Style.animationSlow
repeat: false
onTriggered: {
NotificationService.forceRemoveNotification(model.rawNotification)
// Use the new API method with notification ID
NotificationService.dismissActiveNotification(notificationId)
}
}
// Check if this notification is being removed
onIsRemovingChanged: {
if (isRemoving) {
// Remove from model after animation completes
removalTimer.start()
}
}
@@ -191,7 +238,6 @@ Variants {
NumberAnimation {
duration: Style.animationSlow
easing.type: Easing.OutExpo
//easing.type: Easing.OutBack looks better but notification get clipped on all sides
}
}
@@ -209,44 +255,28 @@ Variants {
anchors.rightMargin: (Style.marginM + 32) * scaling // Leave space for close button
spacing: Style.marginM * scaling
// Header section with app name and timestamp
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS * scaling
NText {
text: `${(model.appName || model.desktopEntry) || "Unknown App"} · ${NotificationService.formatTimestamp(model.timestamp)}`
color: Color.mSecondary
font.pointSize: Style.fontSizeXS * scaling
}
Rectangle {
Layout.preferredWidth: 6 * scaling
Layout.preferredHeight: 6 * scaling
radius: Style.radiusXS * scaling
color: (model.urgency === NotificationUrgency.Critical) ? Color.mError : (model.urgency === NotificationUrgency.Low) ? Color.mOnSurface : Color.mPrimary
Layout.alignment: Qt.AlignVCenter
}
Item {
Layout.fillWidth: true
}
}
// Main content section
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
// Image
NImageCircled {
Layout.preferredWidth: 40 * scaling
Layout.preferredHeight: 40 * scaling
Layout.alignment: Qt.AlignTop
imagePath: model.image && model.image !== "" ? model.image : ""
borderColor: Color.transparent
borderWidth: 0
visible: (model.image && model.image !== "")
ColumnLayout {
// For real-time notification always show the original image
// as the cached version is most likely still processing.
NImageCircled {
Layout.preferredWidth: 40 * scaling
Layout.preferredHeight: 40 * scaling
Layout.alignment: Qt.AlignTop
Layout.topMargin: 30 * scaling
imagePath: model.originalImage || ""
borderColor: Color.transparent
borderWidth: 0
fallbackIcon: "bell"
fallbackIconSize: 24 * scaling
}
Item {
Layout.fillHeight: true
}
}
// Text content
@@ -254,8 +284,39 @@ Variants {
Layout.fillWidth: true
spacing: Style.marginS * scaling
// Header section with app name and timestamp
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS * scaling
Rectangle {
Layout.preferredWidth: 6 * scaling
Layout.preferredHeight: 6 * scaling
radius: Style.radiusXS * scaling
color: {
if (model.urgency === NotificationUrgency.Critical || model.urgency === 2)
return Color.mError
else if (model.urgency === NotificationUrgency.Low || model.urgency === 0)
return Color.mOnSurface
else
return Color.mPrimary
}
Layout.alignment: Qt.AlignVCenter
}
NText {
text: `${model.appName || I18n.tr("system.unknown-app")} · ${Time.formatRelativeTime(model.timestamp)}`
color: Color.mSecondary
font.pointSize: Style.fontSizeXS * scaling
}
Item {
Layout.fillWidth: true
}
}
NText {
text: model.summary || "No summary"
text: model.summary || I18n.tr("general.no-summary")
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightMedium
color: Color.mOnSurface
@@ -264,6 +325,7 @@ Variants {
Layout.fillWidth: true
maximumLineCount: 3
elide: Text.ElideRight
visible: text.length > 0
}
NText {
@@ -277,57 +339,65 @@ Variants {
elide: Text.ElideRight
visible: text.length > 0
}
}
}
// Notification actions
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS * scaling
visible: model.rawNotification && model.rawNotification.actions && model.rawNotification.actions.length > 0
// Notification actions
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS * scaling
Layout.topMargin: Style.marginM * scaling
property var notificationActions: model.rawNotification ? model.rawNotification.actions : []
// Store the notification ID for access in button delegates
property string parentNotificationId: notificationId
Repeater {
model: parent.notificationActions
delegate: NButton {
text: {
var actionText = modelData.text || "Open"
// If text contains comma, take the part after the comma (the display text)
if (actionText.includes(",")) {
return actionText.split(",")[1] || actionText
// Parse actions from JSON string
property var parsedActions: {
try {
return model.actionsJson ? JSON.parse(model.actionsJson) : []
} catch (e) {
return []
}
return actionText
}
fontSize: Style.fontSizeS * scaling
backgroundColor: Color.mPrimary
textColor: Color.mOnPrimary
hoverColor: Color.mSecondary
pressColor: Color.mTertiary
outlined: false
customHeight: 32 * scaling
Layout.preferredHeight: 32 * scaling
visible: parsedActions.length > 0
onClicked: {
if (modelData && modelData.invoke) {
modelData.invoke()
Repeater {
model: parent.parsedActions
delegate: NButton {
property var actionData: modelData
text: {
var actionText = actionData.text || "Open"
// If text contains comma, take the part after the comma (the display text)
if (actionText.includes(",")) {
return actionText.split(",")[1] || actionText
}
return actionText
}
fontSize: Style.fontSizeS * scaling
backgroundColor: Color.mPrimary
textColor: hovered ? Color.mOnTertiary : Color.mOnPrimary
hoverColor: Color.mTertiary
outlined: false
Layout.preferredHeight: 24 * scaling
onClicked: {
NotificationService.invokeAction(parent.parentNotificationId, actionData.identifier)
}
}
}
// Spacer to push buttons to the left
Item {
Layout.fillWidth: true
}
}
}
// Spacer to push buttons to the left if needed
Item {
Layout.fillWidth: true
}
}
}
// Close button positioned absolutely
NIconButton {
icon: "close"
tooltipText: "Close."
tooltipText: I18n.tr("tooltips.close")
baseSize: Style.baseWidgetSize * 0.6
anchors.top: parent.top
anchors.topMargin: Style.marginM * scaling
@@ -13,7 +13,7 @@ NPanel {
id: root
preferredWidth: 380
preferredHeight: 500
preferredHeight: 480
panelKeyboardFocus: true
panelContent: Rectangle {
@@ -37,7 +37,7 @@ NPanel {
}
NText {
text: "Notification History"
text: I18n.tr("notifications.panel.title")
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
@@ -46,29 +46,27 @@ NPanel {
NIconButton {
icon: Settings.data.notifications.doNotDisturb ? "bell-off" : "bell"
tooltipText: Settings.data.notifications.doNotDisturb ? "'Do Not Disturb' is enabled." : "'Do Not Disturb' is disabled."
tooltipText: Settings.data.notifications.doNotDisturb ? I18n.tr("tooltips.do-not-disturb-enabled") : I18n.tr("tooltips.do-not-disturb-disabled")
baseSize: Style.baseWidgetSize * 0.8
onClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb
onRightClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb
}
NIconButton {
icon: "trash"
tooltipText: "Clear history."
tooltipText: I18n.tr("tooltips.clear-history")
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
NotificationService.clearHistory()
// Close panel as there is nothing more to see.
root.close()
}
}
NIconButton {
icon: "close"
tooltipText: "Close."
tooltipText: I18n.tr("tooltips.close")
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
root.close()
}
onClicked: root.close()
}
}
@@ -81,7 +79,7 @@ NPanel {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignHCenter
visible: NotificationService.historyModel.count === 0
visible: NotificationService.historyList.count === 0
spacing: Style.marginL * scaling
Item {
@@ -96,14 +94,14 @@ NPanel {
}
NText {
text: "No notifications"
text: I18n.tr("notifications.panel.no-notifications")
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Your notifications will show up here as they arrive."
text: I18n.tr("notifications.panel.description")
font.pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
@@ -125,13 +123,15 @@ NPanel {
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AsNeeded
model: NotificationService.historyModel
model: NotificationService.historyList
spacing: Style.marginM * scaling
clip: true
boundsBehavior: Flickable.StopAtBounds
visible: NotificationService.historyModel.count > 0
visible: NotificationService.historyList.count > 0
delegate: Rectangle {
property string notificationId: model.id
width: notificationList.width
height: notificationLayout.implicitHeight + (Style.marginM * scaling * 2)
radius: Style.radiusM * scaling
@@ -139,36 +139,87 @@ NPanel {
border.color: Qt.alpha(Color.mOutline, Style.opacityMedium)
border.width: Math.max(1, Style.borderS * scaling)
// Smooth color transition on hover
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
RowLayout {
id: notificationLayout
anchors.fill: parent
anchors.margins: Style.marginM * scaling
spacing: Style.marginM * scaling
// App icon (same style as popup)
NImageCircled {
Layout.preferredWidth: 28 * scaling
Layout.preferredHeight: 28 * scaling
Layout.alignment: Qt.AlignVCenter
// Prefer stable themed icons over transient image paths
imagePath: (appIcon && appIcon !== "") ? (AppIcons.iconFromName(appIcon, "application-x-executable") || appIcon) : ((AppIcons.iconForAppId(desktopEntry || appName, "application-x-executable") || (image && image !== "" ? image : AppIcons.iconFromName("application-x-executable", "application-x-executable"))))
borderColor: Color.transparent
borderWidth: 0
visible: true
ColumnLayout {
NImageCircled {
Layout.preferredWidth: 40 * scaling
Layout.preferredHeight: 40 * scaling
Layout.alignment: Qt.AlignTop
Layout.topMargin: 20 * scaling
imagePath: model.cachedImage || model.originalImage || ""
borderColor: Color.transparent
borderWidth: 0
fallbackIcon: "bell"
fallbackIconSize: 24 * scaling
}
Item {
Layout.fillHeight: true
}
}
// Notification content column
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
Layout.maximumWidth: notificationList.width - (Style.marginM * scaling * 4) // Account for margins and delete button
spacing: Style.marginXXS * scaling
Layout.alignment: Qt.AlignTop
spacing: Style.marginXS * scaling
// Header row with app name and timestamp
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS * scaling
// Urgency indicator
Rectangle {
Layout.preferredWidth: 6 * scaling
Layout.preferredHeight: 6 * scaling
Layout.alignment: Qt.AlignVCenter
radius: 3 * scaling
visible: model.urgency !== 1
color: {
if (model.urgency === 2)
return Color.mError
else if (model.urgency === 0)
return Color.mOnSurfaceVariant
else
return Color.transparent
}
}
NText {
text: model.appName || "Unknown App"
font.pointSize: Style.fontSizeXS * scaling
color: Color.mSecondary
}
NText {
text: Time.formatRelativeTime(model.timestamp)
font.pointSize: Style.fontSizeXS * scaling
color: Color.mSecondary
}
Item {
Layout.fillWidth: true
}
}
// Summary
NText {
text: (summary || "No summary").substring(0, 100)
text: model.summary || I18n.tr("general.no-summary")
font.pointSize: Style.fontSizeM * scaling
font.weight: Font.Medium
color: Color.mPrimary
color: Color.mOnSurface
textFormat: Text.PlainText
wrapMode: Text.Wrap
Layout.fillWidth: true
@@ -176,10 +227,11 @@ NPanel {
elide: Text.ElideRight
}
// Body
NText {
text: (body || "").substring(0, 150)
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurface
text: model.body || ""
font.pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant
textFormat: Text.PlainText
wrapMode: Text.Wrap
Layout.fillWidth: true
@@ -187,36 +239,21 @@ NPanel {
elide: Text.ElideRight
visible: text.length > 0
}
NText {
text: NotificationService.formatTimestamp(timestamp)
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurface
Layout.fillWidth: true
}
}
// Delete button
NIconButton {
icon: "trash"
tooltipText: "Delete notification."
tooltipText: I18n.tr("tooltips.delete-notification")
baseSize: Style.baseWidgetSize * 0.7
Layout.alignment: Qt.AlignTop
onClicked: {
Logger.log("NotificationHistory", "Removing notification:", summary)
NotificationService.historyModel.remove(index)
NotificationService.saveHistory()
// Remove from history using the service API
NotificationService.removeFromHistory(notificationId)
}
}
}
MouseArea {
id: notificationMouseArea
anchors.fill: parent
anchors.rightMargin: Style.marginXL * scaling
hoverEnabled: true
}
}
}
}
+380
View File
@@ -0,0 +1,380 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Widgets
// Unified OSD component - handles both volume and brightness with a single instance
// Loader activates only when showing OSD, deactivates when hidden to save resources
Variants {
model: Quickshell.screens
delegate: Loader {
id: root
required property ShellScreen modelData
property real scaling: ScalingService.getScreenScale(modelData)
// Access the notification model from the service
property ListModel notificationModel: NotificationService.activeList
// If no notification display activated in settings, then show them all
property bool canShowOnThisScreen: Settings.isLoaded && modelData && (Settings.data.notifications.monitors.includes(modelData.name) || (Settings.data.notifications.monitors.length === 0))
// Loader is only active when actually showing something
active: false
// Current OSD display state
property string currentOSDType: "" // "volume", "brightness", or ""
// Volume properties
readonly property real currentVolume: AudioService.volume
readonly property bool isMuted: AudioService.muted
property bool volumeInitialized: false
property bool muteInitialized: false
// Brightness properties
property bool brightnessInitialized: false
readonly property real currentBrightness: {
if (BrightnessService.monitors.length > 0) {
return BrightnessService.monitors[0].brightness || 0
}
return 0
}
// Get appropriate icon based on current OSD type
function getIcon() {
if (currentOSDType === "volume") {
if (AudioService.muted) {
return "volume-mute"
}
return (AudioService.volume <= Number.EPSILON) ? "volume-zero" : (AudioService.volume <= 0.5) ? "volume-low" : "volume-high"
} else if (currentOSDType === "brightness") {
return currentBrightness <= 0.5 ? "brightness-low" : "brightness-high"
}
return ""
}
// Get current value (0-1 range)
function getCurrentValue() {
if (currentOSDType === "volume") {
return isMuted ? 0 : currentVolume
} else if (currentOSDType === "brightness") {
return currentBrightness
}
return 0
}
// Get display percentage
function getDisplayPercentage() {
if (currentOSDType === "volume") {
return isMuted ? "0%" : Math.round(currentVolume * 100) + "%"
} else if (currentOSDType === "brightness") {
return Math.round(currentBrightness * 100) + "%"
}
return ""
}
// Get progress bar color
function getProgressColor() {
if (currentOSDType === "volume") {
if (isMuted)
return Color.mError
if (currentVolume > 1.0)
return Color.mError
return Color.mPrimary
}
return Color.mPrimary
}
// Get icon color
function getIconColor() {
if (currentOSDType === "volume" && isMuted) {
return Color.mError
}
return Color.mOnSurface
}
sourceComponent: PanelWindow {
screen: modelData
anchors {
top: true
}
implicitWidth: 320 * root.scaling
implicitHeight: osdItem.height
// Set margins based on bar position
margins.top: {
switch (Settings.data.bar.position) {
case "top":
return (Style.barHeight + Style.marginS) * root.scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * root.scaling : 0)
default:
return Style.marginL * root.scaling
}
}
color: Color.transparent
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
exclusionMode: PanelWindow.ExclusionMode.Ignore
Rectangle {
id: osdItem
width: parent.width
height: Math.round(contentLayout.implicitHeight + Style.marginL * 2 * root.scaling)
radius: Style.radiusL * root.scaling
color: Color.mSurface
border.color: Color.mOutline
border.width: Math.max(2, Style.borderM * root.scaling)
visible: false
opacity: 0
scale: 0.85
anchors.horizontalCenter: parent.horizontalCenter
Behavior on opacity {
NumberAnimation {
id: opacityAnimation
duration: Style.animationNormal
easing.type: Easing.InOutQuad
}
}
Behavior on scale {
NumberAnimation {
id: scaleAnimation
duration: Style.animationNormal
easing.type: Easing.InOutQuad
}
}
Timer {
id: hideTimer
interval: 2000
onTriggered: osdItem.hide()
}
// Timer to handle visibility after animations complete
Timer {
id: visibilityTimer
interval: Style.animationNormal + 50 // Add small buffer
onTriggered: {
osdItem.visible = false
root.currentOSDType = ""
// Deactivate the loader when done
root.active = false
}
}
RowLayout {
id: contentLayout
anchors.fill: parent
anchors.margins: Style.marginM * root.scaling
spacing: Style.marginM * root.scaling
NIcon {
icon: root.getIcon()
color: root.getIconColor()
font.pointSize: Style.fontSizeXL * root.scaling
Layout.alignment: Qt.AlignVCenter
// Smooth icon transitions
Behavior on color {
ColorAnimation {
duration: Style.animationNormal
easing.type: Easing.InOutQuad
}
}
}
RowLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
spacing: Style.marginXS * root.scaling
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: Math.round(6 * root.scaling)
radius: Math.round(3 * root.scaling)
color: Color.mSurfaceVariant
Rectangle {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: parent.width * Math.min(1.0, root.getCurrentValue())
radius: parent.radius
color: root.getProgressColor()
Behavior on width {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.InOutQuad
}
}
Behavior on color {
ColorAnimation {
duration: Style.animationNormal
easing.type: Easing.InOutQuad
}
}
}
}
NText {
text: root.getDisplayPercentage()
color: Color.mOnSurface
font.pointSize: Style.fontSizeS * root.scaling
Layout.alignment: Qt.AlignVCenter
Layout.minimumWidth: Math.round(32 * root.scaling)
}
}
}
function show() {
// Cancel any pending hide operations
hideTimer.stop()
visibilityTimer.stop()
// Make visible and animate in
osdItem.visible = true
// Use Qt.callLater to ensure the visible change is processed before animation
Qt.callLater(function () {
osdItem.opacity = 1
osdItem.scale = 1.0
})
// Start the auto-hide timer
hideTimer.start()
}
function hide() {
hideTimer.stop()
visibilityTimer.stop()
// Start fade out animation
osdItem.opacity = 0
osdItem.scale = 0.85 // Less dramatic scale change for smoother effect
// Delay hiding the element until after animation completes
visibilityTimer.start()
}
function hideImmediately() {
hideTimer.stop()
visibilityTimer.stop()
osdItem.opacity = 0
osdItem.scale = 0.85
osdItem.visible = false
root.currentOSDType = ""
root.active = false
}
}
function showOSD() {
osdItem.show()
}
}
// Volume change monitoring
Connections {
target: AudioService
function onVolumeChanged() {
if (volumeInitialized) {
showOSD("volume")
}
}
function onMutedChanged() {
if (muteInitialized) {
showOSD("volume")
}
}
}
// Timer to initialize volume/mute flags after services are ready
Timer {
id: initTimer
interval: 100 // 100ms delay to allow services to initialize
running: true
onTriggered: {
volumeInitialized = true
muteInitialized = true
}
}
// Brightness change monitoring
Connections {
target: BrightnessService
function onMonitorsChanged() {
connectBrightnessMonitors()
}
}
Component.onCompleted: {
connectBrightnessMonitors()
}
function connectBrightnessMonitors() {
for (var i = 0; i < BrightnessService.monitors.length; i++) {
let monitor = BrightnessService.monitors[i]
// Disconnect first to avoid duplicate connections
monitor.brightnessUpdated.disconnect(onBrightnessChanged)
monitor.brightnessUpdated.connect(onBrightnessChanged)
}
}
function onBrightnessChanged(newBrightness) {
if (!brightnessInitialized) {
brightnessInitialized = true
} else {
showOSD("brightness")
}
}
function showOSD(type) {
// Check if OSD is enabled in settings and can show on this screen
if (!Settings.data.notifications.enableOSD || !canShowOnThisScreen) {
return
}
// Update the current OSD type
currentOSDType = type
// Activate the loader if not already active
if (!root.active) {
root.active = true
}
// Show the OSD (may need to wait for loader to create the item)
if (root.item) {
root.item.showOSD()
} else {
// If item not ready yet, wait for it
Qt.callLater(function () {
if (root.item) {
root.item.showOSD()
}
})
}
}
function hideOSD() {
if (root.item && root.item.osdItem) {
root.item.osdItem.hideImmediately()
} else if (root.active) {
// If loader is active but item isn't ready, just deactivate
root.active = false
}
}
}
}
@@ -30,27 +30,27 @@ NPanel {
readonly property var powerOptions: [{
"action": "lock",
"icon": "lock",
"title": "Lock",
"subtitle": "Lock your session"
"title": I18n.tr("session-menu.lock"),
"subtitle": I18n.tr("session-menu.lock-subtitle")
}, {
"action": "suspend",
"icon": "suspend",
"title": "Suspend",
"title": I18n.tr("session-menu.suspend"),
"subtitle": "Put the system to sleep"
}, {
"action": "reboot",
"icon": "reboot",
"title": "Reboot",
"title": I18n.tr("session-menu.reboot"),
"subtitle": "Restart the system"
}, {
"action": "logout",
"icon": "logout",
"title": "Logout",
"subtitle": "End your session"
"title": I18n.tr("session-menu.logout"),
"subtitle": I18n.tr("session-menu.end-subtitle")
}, {
"action": "shutdown",
"icon": "shutdown",
"title": "Shutdown",
"title": I18n.tr("session-menu.shutdown"),
"subtitle": "Turn off the system",
"isShutdown": true
}]
@@ -263,7 +263,10 @@ NPanel {
Layout.preferredHeight: Style.baseWidgetSize * 0.8 * scaling
NText {
text: timerActive ? `${pendingAction.charAt(0).toUpperCase() + pendingAction.slice(1)} in ${Math.ceil(timeRemaining / 1000)} seconds...` : "Power Menu"
text: timerActive ? I18n.tr("session-menu.action-in-seconds", {
"action": pendingAction.charAt(0).toUpperCase() + pendingAction.slice(1),
"seconds": Math.ceil(timeRemaining / 1000)
}) : I18n.tr("session-menu.title")
font.weight: Style.fontWeightBold
font.pointSize: Style.fontSizeL * scaling
color: timerActive ? Color.mPrimary : Color.mOnSurface
@@ -277,7 +280,7 @@ NPanel {
NIconButton {
icon: timerActive ? "stop" : "close"
tooltipText: timerActive ? "Cancel Timer" : "Close"
tooltipText: timerActive ? I18n.tr("tooltips.cancel-timer") : I18n.tr("tooltips.close")
Layout.alignment: Qt.AlignVCenter
colorBg: timerActive ? Qt.alpha(Color.mError, 0.08) : Color.transparent
colorFg: timerActive ? Color.mError : Color.mOnSurface
@@ -419,7 +422,7 @@ NPanel {
NText {
text: {
if (buttonRoot.pending) {
return "Click again to execute immediately"
return I18n.tr("session-menu.click-again")
}
return buttonRoot.subtitle
}
@@ -20,6 +20,7 @@ NBox {
signal removeWidget(string section, int index)
signal reorderWidget(string section, int fromIndex, int toIndex)
signal updateWidgetSettings(string section, int index, var settings)
signal moveWidget(string fromSection, int index, string toSection)
signal dragPotentialStarted
signal dragPotentialEnded
@@ -78,16 +79,29 @@ NBox {
Item {
Layout.fillWidth: true
}
NComboBox {
NSearchableComboBox {
id: comboBox
model: availableWidgets
label: ""
description: ""
placeholder: "Select a widget to add..."
placeholder: I18n.tr("bar.widget-settings.section-editor.placeholder")
searchPlaceholder: I18n.tr("bar.widget-settings.section-editor.search-placeholder")
onSelected: key => comboBox.currentKey = key
popupHeight: 340 * scaling
minimumWidth: 200 * scaling
Layout.alignment: Qt.AlignVCenter
// Re-filter when the model count changes (when widgets are loaded)
Connections {
target: availableWidgets
function onCountChanged() {
// Trigger a re-filter by clearing and re-setting the search text
var currentSearch = comboBox.searchText
comboBox.searchText = ""
comboBox.searchText = currentSearch
}
}
}
NIconButton {
@@ -98,7 +112,7 @@ NBox {
colorBgHover: Color.mSecondary
colorFgHover: Color.mOnSecondary
enabled: comboBox.currentKey !== ""
tooltipText: "Add widget to section"
tooltipText: I18n.tr("tooltips.add-widget")
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: Style.marginS * scaling
onClicked: {
@@ -125,6 +139,7 @@ NBox {
Repeater {
model: widgetModel
delegate: Rectangle {
id: widgetItem
required property int index
@@ -158,6 +173,51 @@ NBox {
}
}
// Context menu for moving widget to other sections
NContextMenu {
id: contextMenu
parent: Overlay.overlay
width: 240 * scaling
model: [{
"label": I18n.tr("tooltips.move-to-left-section"),
"action": "left",
"icon": "arrow-bar-to-left",
"visible": root.sectionId !== "left"
}, {
"label": I18n.tr("tooltips.move-to-center-section"),
"action": "center",
"icon": "layout-columns",
"visible": root.sectionId !== "center"
}, {
"label": I18n.tr("tooltips.move-to-right-section"),
"action": "right",
"icon": "arrow-bar-to-right",
"visible": root.sectionId !== "right"
}]
onTriggered: action => root.moveWidget(root.sectionId, index, action)
}
// Update the MouseArea to use the new context menu
MouseArea {
id: contextMouseArea
anchors.fill: parent
acceptedButtons: Qt.RightButton
z: -1 // Below the buttons but above background
onClicked: mouse => {
if (mouse.button === Qt.RightButton) {
// Check if click is not on the buttons area
const localX = mouse.x
const buttonsStartX = parent.width - (parent.buttonsCount * parent.buttonsWidth)
if (localX < buttonsStartX) {
// Use the helper function to open at mouse position
contextMenu.openAtItem(widgetItem, mouse.x, mouse.y)
}
}
}
}
RowLayout {
id: widgetContent
anchors.centerIn: parent
@@ -180,6 +240,7 @@ NBox {
active: BarWidgetRegistry.widgetHasUserSettings(modelData.id)
sourceComponent: NIconButton {
icon: "settings"
tooltipText: I18n.tr("tooltips.widget-settings")
baseSize: miniButtonSize
colorBorder: Qt.alpha(Color.mOutline, Style.opacityLight)
colorBg: Color.mOnSurface
@@ -220,6 +281,7 @@ NBox {
NIconButton {
icon: "close"
tooltipText: I18n.tr("tooltips.remove-widget")
baseSize: miniButtonSize
colorBorder: Qt.alpha(Color.mOutline, Style.opacityLight)
colorBg: Color.mOnSurface
@@ -5,70 +5,55 @@ import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
import "./WidgetSettings" as WidgetSettings
// Widget Settings Dialog Component
Popup {
id: settingsPopup
// Don't replace by root!
id: widgetSettings
property int widgetIndex: -1
property var widgetData: null
property string widgetId: ""
property bool isMasked: false
// Center popup in parent
x: (parent.width - width) * 0.5
y: (parent.height - height) * 0.5
width: 500 * scaling
width: Math.max(content.implicitWidth + padding * 2, 500 * scaling)
height: content.implicitHeight + padding * 2
padding: Style.marginXL * scaling
modal: true
background: Rectangle {
id: bgRect
color: Color.mSurface
radius: Style.radiusL * scaling
border.color: Color.mPrimary
border.width: Style.borderM * scaling
}
// Load settings when popup opens with data
onOpened: {
// Mark this popup has opened in the PanelService
PanelService.willOpenPopup(widgetSettings)
// Load settings when popup opens with data
if (widgetData && widgetId) {
loadWidgetSettings()
}
}
function loadWidgetSettings() {
const widgetSettingsMap = {
"ActiveWindow": "WidgetSettings/ActiveWindowSettings.qml",
"Battery": "WidgetSettings/BatterySettings.qml",
"Brightness": "WidgetSettings/BrightnessSettings.qml",
"Clock": "WidgetSettings/ClockSettings.qml",
"CustomButton": "WidgetSettings/CustomButtonSettings.qml",
"KeyboardLayout": "WidgetSettings/KeyboardLayoutSettings.qml",
"MediaMini": "WidgetSettings/MediaMiniSettings.qml",
"Microphone": "WidgetSettings/MicrophoneSettings.qml",
"NotificationHistory": "WidgetSettings/NotificationHistorySettings.qml",
"Workspace": "WidgetSettings/WorkspaceSettings.qml",
"SidePanelToggle": "WidgetSettings/SidePanelToggleSettings.qml",
"Spacer": "WidgetSettings/SpacerSettings.qml",
"SystemMonitor": "WidgetSettings/SystemMonitorSettings.qml",
"Volume": "WidgetSettings/VolumeSettings.qml"
}
const source = widgetSettingsMap[widgetId]
if (source) {
// Use setSource to pass properties at creation time
settingsLoader.setSource(source, {
"widgetData": widgetData,
"widgetMetadata": BarWidgetRegistry.widgetMetadata[widgetId]
})
}
onClosed: {
PanelService.willClosePopup(widgetSettings)
}
ColumnLayout {
background: Rectangle {
id: bgRect
opacity: widgetSettings.isMasked ? 0 : 1.0
color: Color.mSurface
radius: Style.radiusL * scaling
border.color: Color.mPrimary
border.width: Math.max(1, Style.borderM * scaling)
}
contentItem: ColumnLayout {
id: content
opacity: widgetSettings.isMasked ? 0 : 1.0
width: parent.width
spacing: Style.marginM * scaling
@@ -77,7 +62,9 @@ Popup {
Layout.fillWidth: true
NText {
text: `${settingsPopup.widgetId} Settings`
text: I18n.tr("system.widget-settings-title", {
"widget": widgetSettings.widgetId
})
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
@@ -86,7 +73,7 @@ Popup {
NIconButton {
icon: "close"
onClicked: settingsPopup.close()
onClicked: widgetSettings.close()
}
}
@@ -115,22 +102,50 @@ Popup {
}
NButton {
text: "Cancel"
text: I18n.tr("bar.widget-settings.dialog.cancel")
outlined: true
onClicked: settingsPopup.close()
onClicked: widgetSettings.close()
}
NButton {
text: "Apply"
text: I18n.tr("bar.widget-settings.dialog.apply")
icon: "check"
onClicked: {
if (settingsLoader.item && settingsLoader.item.saveSettings) {
var newSettings = settingsLoader.item.saveSettings()
root.updateWidgetSettings(sectionId, settingsPopup.widgetIndex, newSettings)
settingsPopup.close()
root.updateWidgetSettings(sectionId, widgetSettings.widgetIndex, newSettings)
widgetSettings.close()
}
}
}
}
}
function loadWidgetSettings() {
const widgetSettingsMap = {
"ActiveWindow": "WidgetSettings/ActiveWindowSettings.qml",
"Battery": "WidgetSettings/BatterySettings.qml",
"Brightness": "WidgetSettings/BrightnessSettings.qml",
"Clock": "WidgetSettings/ClockSettings.qml",
"ControlCenter": "WidgetSettings/ControlCenterSettings.qml",
"CustomButton": "WidgetSettings/CustomButtonSettings.qml",
"KeyboardLayout": "WidgetSettings/KeyboardLayoutSettings.qml",
"MediaMini": "WidgetSettings/MediaMiniSettings.qml",
"Microphone": "WidgetSettings/MicrophoneSettings.qml",
"NotificationHistory": "WidgetSettings/NotificationHistorySettings.qml",
"Spacer": "WidgetSettings/SpacerSettings.qml",
"SystemMonitor": "WidgetSettings/SystemMonitorSettings.qml",
"Volume": "WidgetSettings/VolumeSettings.qml",
"Workspace": "WidgetSettings/WorkspaceSettings.qml"
}
const source = widgetSettingsMap[widgetId]
if (source) {
// Use setSource to pass properties at creation time
settingsLoader.setSource(source, {
"widgetData": widgetData,
"widgetMetadata": BarWidgetRegistry.widgetMetadata[widgetId]
})
}
}
}
@@ -0,0 +1,60 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
// Local state
property bool valueShowIcon: widgetData.showIcon !== undefined ? widgetData.showIcon : widgetMetadata.showIcon
property bool valueAutoHide: widgetData.autoHide !== undefined ? widgetData.autoHide : widgetMetadata.autoHide
property string valueScrollingMode: widgetData.scrollingMode || widgetMetadata.scrollingMode
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.autoHide = valueAutoHide
settings.showIcon = valueShowIcon
settings.scrollingMode = valueScrollingMode
console.log(JSON.stringify(settings))
return settings
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("bar.widget-settings.active-window.auto-hide")
checked: root.valueAutoHide
onToggled: checked => root.valueAutoHide = checked
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("bar.widget-settings.active-window.show-app-icon")
checked: root.valueShowIcon
onToggled: checked => root.valueShowIcon = checked
}
NComboBox {
label: I18n.tr("bar.widget-settings.active-window.scrolling-mode")
model: [{
"key": "always",
"name": I18n.tr("options.scrolling-modes.always")
}, {
"key": "hover",
"name": I18n.tr("options.scrolling-modes.hover")
}, {
"key": "never",
"name": I18n.tr("options.scrolling-modes.never")
}]
currentKey: valueScrollingMode
onSelected: key => valueScrollingMode = key
minimumWidth: 200 * scaling
}
}
@@ -25,30 +25,26 @@ ColumnLayout {
}
NComboBox {
label: "Display mode"
description: "Choose how you'd like this value to appear."
label: I18n.tr("bar.widget-settings.battery.display-mode.label")
description: I18n.tr("bar.widget-settings.battery.display-mode.description")
minimumWidth: 134 * scaling
model: ListModel {
ListElement {
key: "onhover"
name: "On Hover"
}
ListElement {
key: "alwaysShow"
name: "Always Show"
}
ListElement {
key: "alwaysHide"
name: "Always Hide"
}
}
model: [{
"key": "onhover",
"name": I18n.tr("options.display-mode.on-hover")
}, {
"key": "alwaysShow",
"name": I18n.tr("options.display-mode.always-show")
}, {
"key": "alwaysHide",
"name": I18n.tr("options.display-mode.always-hide")
}]
currentKey: root.valueDisplayMode
onSelected: key => root.valueDisplayMode = key
}
NSpinBox {
label: "Low battery warning threshold"
description: "Show a warning when battery falls below this percentage."
label: I18n.tr("bar.widget-settings.battery.low-battery-threshold.label")
description: I18n.tr("bar.widget-settings.battery.low-battery-threshold.description")
value: valueWarningThreshold
suffix: "%"
minimum: 5
@@ -23,23 +23,19 @@ ColumnLayout {
}
NComboBox {
label: "Display mode"
description: "Choose how you'd like this value to appear."
label: I18n.tr("bar.widget-settings.brightness.display-mode.label")
description: I18n.tr("bar.widget-settings.brightness.display-mode.description")
minimumWidth: 134 * scaling
model: ListModel {
ListElement {
key: "onhover"
name: "On Hover"
}
ListElement {
key: "alwaysShow"
name: "Always Show"
}
ListElement {
key: "alwaysHide"
name: "Always Hide"
}
}
model: [{
"key": "onhover",
"name": I18n.tr("options.display-mode.on-hover")
}, {
"key": "alwaysShow",
"name": I18n.tr("options.display-mode.always-show")
}, {
"key": "alwaysHide",
"name": I18n.tr("options.display-mode.always-hide")
}]
currentKey: valueDisplayMode
onSelected: key => valueDisplayMode = key
}
@@ -0,0 +1,252 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
width: 700 * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
// Local state
property bool valueUsePrimaryColor: widgetData.usePrimaryColor !== undefined ? widgetData.usePrimaryColor : widgetMetadata.usePrimaryColor
property bool valueUseMonospacedFont: widgetData.useMonospacedFont !== undefined ? widgetData.useMonospacedFont : widgetMetadata.useMonospacedFont
property string valueFormatHorizontal: widgetData.formatHorizontal !== undefined ? widgetData.formatHorizontal : widgetMetadata.formatHorizontal
property string valueFormatVertical: widgetData.formatVertical !== undefined ? widgetData.formatVertical : widgetMetadata.formatVertical
// Track the currently focused input field
property var focusedInput: null
property int focusedLineIndex: -1
readonly property var now: Time.date
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.usePrimaryColor = valueUsePrimaryColor
settings.useMonospacedFont = valueUseMonospacedFont
settings.formatHorizontal = valueFormatHorizontal.trim()
settings.formatVertical = valueFormatVertical.trim()
return settings
}
// Function to insert token at cursor position in the focused input
function insertToken(token) {
if (!focusedInput || !focusedInput.inputItem) {
// If no input is focused, default to horiz
if (inputHoriz.inputItem) {
inputHoriz.inputItem.focus = true
focusedInput = inputHoriz
}
}
if (focusedInput && focusedInput.inputItem) {
var input = focusedInput.inputItem
var cursorPos = input.cursorPosition
var currentText = input.text
// Insert token at cursor position
var newText = currentText.substring(0, cursorPos) + token + currentText.substring(cursorPos)
input.text = newText + " "
// Move cursor after the inserted token
input.cursorPosition = cursorPos + token.length + 1
// Ensure the input keeps focus
input.focus = true
}
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("bar.widget-settings.clock.use-primary-color.label")
description: I18n.tr("bar.widget-settings.clock.use-primary-color.description")
checked: valueUsePrimaryColor
onToggled: checked => valueUsePrimaryColor = checked
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("bar.widget-settings.clock.use-monospaced-font.label")
description: I18n.tr("bar.widget-settings.clock.use-monospaced-font.description")
checked: valueUseMonospacedFont
onToggled: checked => valueUseMonospacedFont = checked
}
NDivider {
Layout.fillWidth: true
}
NHeader {
label: I18n.tr("bar.widget-settings.clock.clock-display.label")
description: I18n.tr("bar.widget-settings.clock.clock-display.description")
}
RowLayout {
id: main
spacing: Style.marginL * scaling
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
ColumnLayout {
spacing: Style.marginM * scaling
Layout.fillWidth: true
Layout.preferredWidth: 1 // Equal sizing hint
Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
NTextInput {
id: inputHoriz
Layout.fillWidth: true
label: I18n.tr("bar.widget-settings.clock.horizontal-bar.label")
description: I18n.tr("bar.widget-settings.clock.horizontal-bar.description")
placeholderText: "HH:mm ddd, MMM dd"
text: valueFormatHorizontal
onTextChanged: valueFormatHorizontal = text
Component.onCompleted: {
if (inputItem) {
inputItem.onActiveFocusChanged.connect(function () {
if (inputItem.activeFocus) {
root.focusedInput = inputHoriz
}
})
}
}
}
Item {
Layout.fillHeight: true
}
NTextInput {
id: inputVert
Layout.fillWidth: true
label: I18n.tr("bar.widget-settings.clock.vertical-bar.label")
description: I18n.tr("bar.widget-settings.clock.vertical-bar.description")
// Tokens are Qt format tokens and must not be localized
placeholderText: "HH mm dd MM"
text: valueFormatVertical
onTextChanged: valueFormatVertical = text
Component.onCompleted: {
if (inputItem) {
inputItem.onActiveFocusChanged.connect(function () {
if (inputItem.activeFocus) {
root.focusedInput = inputVert
}
})
}
}
}
}
// --------------
// Preview
ColumnLayout {
Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
Layout.fillWidth: false
NLabel {
label: I18n.tr("bar.widget-settings.clock.preview")
Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
}
Rectangle {
Layout.preferredWidth: 320 * scaling
Layout.preferredHeight: 160 * scaling // Fixed height instead of fillHeight
color: Color.mSurfaceVariant
radius: Style.radiusM * scaling
border.color: Color.mSecondary
border.width: Math.max(1, Style.borderS * scaling)
Behavior on border.color {
ColorAnimation {
duration: Style.animationFast
}
}
ColumnLayout {
spacing: Style.marginM * scaling
anchors.centerIn: parent
ColumnLayout {
spacing: -2 * scaling
Layout.alignment: Qt.AlignHCenter
// Horizontal
Repeater {
Layout.topMargin: Style.marginM * scaling
model: Qt.formatDateTime(now, valueFormatHorizontal.trim()).split("\\n")
delegate: NText {
visible: text !== ""
text: modelData
font.family: valueUseMonospacedFont ? Settings.data.ui.fontFixed : Settings.data.ui.fontDefault
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
color: valueUsePrimaryColor ? Color.mPrimary : Color.mOnSurface
wrapMode: Text.WordWrap
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
}
}
NDivider {
Layout.fillWidth: true
}
// Vertical
ColumnLayout {
spacing: -2 * scaling
Layout.alignment: Qt.AlignHCenter
Repeater {
Layout.topMargin: Style.marginM * scaling
model: Qt.formatDateTime(now, valueFormatVertical.trim()).split(" ")
delegate: NText {
visible: text !== ""
text: modelData
font.family: valueUseMonospacedFont ? Settings.data.ui.fontFixed : Settings.data.ui.fontDefault
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
color: valueUsePrimaryColor ? Color.mPrimary : Color.mOnSurface
wrapMode: Text.WordWrap
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
}
}
}
}
}
}
NDivider {
Layout.topMargin: Style.marginM * scaling
Layout.bottomMargin: Style.marginM * scaling
}
NDateTimeTokens {
Layout.fillWidth: true
height: 200 * scaling
// Connect to token clicked signal if NDateTimeTokens provides it
onTokenClicked: token => root.insertToken(token)
}
}
@@ -0,0 +1,94 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
// Local state
property string valueIcon: widgetData.icon !== undefined ? widgetData.icon : widgetMetadata.icon
property bool valueUseDistroLogo: widgetData.useDistroLogo !== undefined ? widgetData.useDistroLogo : widgetMetadata.useDistroLogo
property string valueCustomIconPath: widgetData.customIconPath !== undefined ? widgetData.customIconPath : ""
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.icon = valueIcon
settings.useDistroLogo = valueUseDistroLogo
settings.customIconPath = valueCustomIconPath
return settings
}
NToggle {
label: I18n.tr("bar.widget-settings.control-center.use-distro-logo")
checked: valueUseDistroLogo
onToggled: {
valueUseDistroLogo = checked
if (checked) {
valueCustomIconPath = ""
valueIcon = ""
}
}
}
RowLayout {
spacing: Style.marginM * scaling
NLabel {
label: I18n.tr("bar.widget-settings.control-center.icon.label")
description: I18n.tr("bar.widget-settings.control-center.icon.description")
}
NImageCircled {
Layout.alignment: Qt.AlignVCenter
imagePath: valueCustomIconPath
visible: valueCustomIconPath !== ""
width: Style.fontSizeXL * 2 * scaling
height: Style.fontSizeXL * 2 * scaling
}
NIcon {
Layout.alignment: Qt.AlignVCenter
icon: valueIcon
font.pointSize: Style.fontSizeXXL * 1.5 * scaling
visible: valueIcon !== "" && valueCustomIconPath === ""
}
}
RowLayout {
spacing: Style.marginM * scaling
NButton {
enabled: !valueUseDistroLogo
text: I18n.tr("bar.widget-settings.control-center.browse-library")
onClicked: iconPicker.open()
}
NButton {
enabled: !valueUseDistroLogo
text: I18n.tr("bar.widget-settings.control-center.browse-file")
onClicked: filePicker.open()
}
}
NIconPicker {
id: iconPicker
initialIcon: valueIcon
onIconSelected: iconName => {
valueIcon = iconName
valueCustomIconPath = ""
}
}
NFilePicker {
id: filePicker
title: I18n.tr("bar.widget-settings.control-center.select-custom-icon")
onAccepted: paths => valueCustomIconPath = paths[0]
}
}
@@ -0,0 +1,107 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Window
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
property var widgetData: null
property var widgetMetadata: null
property string valueIcon: widgetData.icon !== undefined ? widgetData.icon : widgetMetadata.icon
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.icon = valueIcon
settings.leftClickExec = leftClickExecInput.text
settings.rightClickExec = rightClickExecInput.text
settings.middleClickExec = middleClickExecInput.text
settings.textCommand = textCommandInput.text
settings.textIntervalMs = parseInt(textIntervalInput.text || textIntervalInput.placeholderText, 10)
return settings
}
RowLayout {
spacing: Style.marginM * scaling
NLabel {
label: I18n.tr("bar.widget-settings.custom-button.icon.label")
description: I18n.tr("bar.widget-settings.custom-button.icon.description")
}
NIcon {
Layout.alignment: Qt.AlignVCenter
icon: valueIcon
font.pointSize: Style.fontSizeXL * scaling
visible: valueIcon !== ""
}
NButton {
text: I18n.tr("bar.widget-settings.custom-button.browse")
onClicked: iconPicker.open()
}
}
NIconPicker {
id: iconPicker
initialIcon: valueIcon
onIconSelected: function (iconName) {
valueIcon = iconName
}
}
NTextInput {
id: leftClickExecInput
Layout.fillWidth: true
label: I18n.tr("bar.widget-settings.custom-button.left-click")
placeholderText: I18n.tr("placeholders.enter-command")
text: widgetData?.leftClickExec || widgetMetadata.leftClickExec
}
NTextInput {
id: rightClickExecInput
Layout.fillWidth: true
label: I18n.tr("bar.widget-settings.custom-button.right-click")
placeholderText: I18n.tr("placeholders.enter-command")
text: widgetData?.rightClickExec || widgetMetadata.rightClickExec
}
NTextInput {
id: middleClickExecInput
Layout.fillWidth: true
label: I18n.tr("bar.widget-settings.custom-button.middle-click")
placeholderText: I18n.tr("placeholders.enter-command")
text: widgetData.middleClickExec || widgetMetadata.middleClickExec
}
NDivider {
Layout.fillWidth: true
}
NHeader {
label: I18n.tr("bar.widget-settings.custom-button.dynamic-text")
}
NTextInput {
id: textCommandInput
Layout.fillWidth: true
label: I18n.tr("bar.widget-settings.custom-button.display-command-output.label")
description: I18n.tr("bar.widget-settings.custom-button.display-command-output.description")
placeholderText: I18n.tr("placeholders.command-example")
text: widgetData?.textCommand || widgetMetadata.textCommand
}
NTextInput {
id: textIntervalInput
Layout.fillWidth: true
label: I18n.tr("bar.widget-settings.custom-button.refresh-interval.label")
description: I18n.tr("bar.widget-settings.custom-button.refresh-interval.description")
placeholderText: String(widgetMetadata.textIntervalMs || 3000)
text: widgetData && widgetData.textIntervalMs !== undefined ? String(widgetData.textIntervalMs) : ""
}
}
@@ -23,23 +23,19 @@ ColumnLayout {
}
NComboBox {
label: "Display mode"
description: "Choose how you'd like this value to appear."
label: I18n.tr("bar.widget-settings.keyboard-layout.display-mode.label")
description: I18n.tr("bar.widget-settings.keyboard-layout.display-mode.description")
minimumWidth: 134 * scaling
model: ListModel {
ListElement {
key: "onhover"
name: "On Hover"
}
ListElement {
key: "forceOpen"
name: "Force Open"
}
ListElement {
key: "alwaysHide"
name: "Always Hide"
}
}
model: [{
"key": "onhover",
"name": I18n.tr("options.display-mode.on-hover")
}, {
"key": "forceOpen",
"name": I18n.tr("options.display-mode.force-open")
}, {
"key": "alwaysHide",
"name": I18n.tr("options.display-mode.always-hide")
}]
currentKey: valueDisplayMode
onSelected: key => valueDisplayMode = key
}
@@ -0,0 +1,86 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
// Local state
property bool valueAutoHide: widgetData.autoHide !== undefined ? widgetData.autoHide : widgetMetadata.autoHide
property bool valueShowAlbumArt: widgetData.showAlbumArt !== undefined ? widgetData.showAlbumArt : widgetMetadata.showAlbumArt
property bool valueShowVisualizer: widgetData.showVisualizer !== undefined ? widgetData.showVisualizer : widgetMetadata.showVisualizer
property string valueVisualizerType: widgetData.visualizerType || widgetMetadata.visualizerType
property string valueScrollingMode: widgetData.scrollingMode || widgetMetadata.scrollingMode
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.autoHide = valueAutoHide
settings.showAlbumArt = valueShowAlbumArt
settings.showVisualizer = valueShowVisualizer
settings.visualizerType = valueVisualizerType
settings.scrollingMode = valueScrollingMode
return settings
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("bar.widget-settings.media-mini.auto-hide")
checked: root.valueAutoHide
onToggled: checked => root.valueAutoHide = checked
}
NToggle {
label: I18n.tr("bar.widget-settings.media-mini.show-album-art")
checked: valueShowAlbumArt
onToggled: checked => valueShowAlbumArt = checked
}
NToggle {
label: I18n.tr("bar.widget-settings.media-mini.show-visualizer")
checked: valueShowVisualizer
onToggled: checked => valueShowVisualizer = checked
}
NComboBox {
visible: valueShowVisualizer
label: I18n.tr("bar.widget-settings.media-mini.visualizer-type")
model: [{
"key": "linear",
"name": I18n.tr("options.visualizer-types.linear")
}, {
"key": "mirrored",
"name": I18n.tr("options.visualizer-types.mirrored")
}, {
"key": "wave",
"name": I18n.tr("options.visualizer-types.wave")
}]
currentKey: valueVisualizerType
onSelected: key => valueVisualizerType = key
minimumWidth: 200 * scaling
}
NComboBox {
label: I18n.tr("bar.widget-settings.media-mini.scrolling-mode")
model: [{
"key": "always",
"name": I18n.tr("options.scrolling-modes.always")
}, {
"key": "hover",
"name": I18n.tr("options.scrolling-modes.hover")
}, {
"key": "never",
"name": I18n.tr("options.scrolling-modes.never")
}]
currentKey: valueScrollingMode
onSelected: key => valueScrollingMode = key
minimumWidth: 200 * scaling
}
}
@@ -23,23 +23,19 @@ ColumnLayout {
}
NComboBox {
label: "Display mode"
description: "Choose how you'd like this value to appear."
label: I18n.tr("bar.widget-settings.microphone.display-mode.label")
description: I18n.tr("bar.widget-settings.microphone.display-mode.description")
minimumWidth: 134 * scaling
model: ListModel {
ListElement {
key: "onhover"
name: "On Hover"
}
ListElement {
key: "alwaysShow"
name: "Always Show"
}
ListElement {
key: "alwaysHide"
name: "Always Hide"
}
}
model: [{
"key": "onhover",
"name": I18n.tr("options.display-mode.on-hover")
}, {
"key": "alwaysShow",
"name": I18n.tr("options.display-mode.always-show")
}, {
"key": "alwaysHide",
"name": I18n.tr("options.display-mode.always-hide")
}]
currentKey: valueDisplayMode
onSelected: key => valueDisplayMode = key
}
@@ -25,13 +25,13 @@ ColumnLayout {
}
NToggle {
label: "Show unread badge"
label: I18n.tr("bar.widget-settings.notification-history.show-unread-badge")
checked: valueShowUnreadBadge
onToggled: checked => valueShowUnreadBadge = checked
}
NToggle {
label: "Hide badge when zero"
label: I18n.tr("bar.widget-settings.notification-history.hide-badge-when-zero")
checked: valueHideWhenZero
onToggled: checked => valueHideWhenZero = checked
}
@@ -22,9 +22,9 @@ ColumnLayout {
NTextInput {
id: widthInput
Layout.fillWidth: true
label: "Width"
description: "Spacing width in pixels"
label: I18n.tr("bar.widget-settings.spacer.width.label")
description: I18n.tr("bar.widget-settings.spacer.width.description")
text: widgetData.width || widgetMetadata.width
placeholderText: "Enter width in pixels"
placeholderText: I18n.tr("placeholders.enter-width-pixels")
}
}
@@ -35,7 +35,7 @@ ColumnLayout {
NToggle {
id: showCpuUsage
Layout.fillWidth: true
label: "CPU usage"
label: I18n.tr("bar.widget-settings.system-monitor.cpu-usage")
checked: valueShowCpuUsage
onToggled: checked => valueShowCpuUsage = checked
}
@@ -43,7 +43,7 @@ ColumnLayout {
NToggle {
id: showCpuTemp
Layout.fillWidth: true
label: "CPU temperature"
label: I18n.tr("bar.widget-settings.system-monitor.cpu-temperature")
checked: valueShowCpuTemp
onToggled: checked => valueShowCpuTemp = checked
}
@@ -51,7 +51,7 @@ ColumnLayout {
NToggle {
id: showMemoryUsage
Layout.fillWidth: true
label: "Memory usage"
label: I18n.tr("bar.widget-settings.system-monitor.memory-usage")
checked: valueShowMemoryUsage
onToggled: checked => valueShowMemoryUsage = checked
}
@@ -59,7 +59,7 @@ ColumnLayout {
NToggle {
id: showMemoryAsPercent
Layout.fillWidth: true
label: "Memory as percentage"
label: I18n.tr("bar.widget-settings.system-monitor.memory-percentage")
checked: valueShowMemoryAsPercent
onToggled: checked => valueShowMemoryAsPercent = checked
}
@@ -67,7 +67,7 @@ ColumnLayout {
NToggle {
id: showNetworkStats
Layout.fillWidth: true
label: "Network traffic"
label: I18n.tr("bar.widget-settings.system-monitor.network-traffic")
checked: valueShowNetworkStats
onToggled: checked => valueShowNetworkStats = checked
}
@@ -75,7 +75,7 @@ ColumnLayout {
NToggle {
id: showDiskUsage
Layout.fillWidth: true
label: "Storage usage"
label: I18n.tr("bar.widget-settings.system-monitor.storage-usage")
checked: valueShowDiskUsage
onToggled: checked => valueShowDiskUsage = checked
}
@@ -23,23 +23,19 @@ ColumnLayout {
}
NComboBox {
label: "Display mode"
description: "Choose how you'd like this value to appear."
label: I18n.tr("bar.widget-settings.volume.display-mode.label")
description: I18n.tr("bar.widget-settings.volume.display-mode.description")
minimumWidth: 134 * scaling
model: ListModel {
ListElement {
key: "onhover"
name: "On Hover"
}
ListElement {
key: "alwaysShow"
name: "Always Show"
}
ListElement {
key: "alwaysHide"
name: "Always Hide"
}
}
model: [{
"key": "onhover",
"name": I18n.tr("options.display-mode.on-hover")
}, {
"key": "alwaysShow",
"name": I18n.tr("options.display-mode.always-show")
}, {
"key": "alwaysHide",
"name": I18n.tr("options.display-mode.always-hide")
}]
currentKey: valueDisplayMode
onSelected: key => valueDisplayMode = key
}
@@ -23,21 +23,17 @@ ColumnLayout {
NComboBox {
id: labelModeCombo
label: "Label Mode"
model: ListModel {
ListElement {
key: "none"
name: "None"
}
ListElement {
key: "index"
name: "Index"
}
ListElement {
key: "name"
name: "Name"
}
}
label: I18n.tr("bar.widget-settings.workspace.label-mode")
model: [{
"key": "none",
"name": I18n.tr("options.workspace-labels.none")
}, {
"key": "index",
"name": I18n.tr("options.workspace-labels.index")
}, {
"key": "name",
"name": I18n.tr("options.workspace-labels.name")
}]
currentKey: widgetData.labelMode || widgetMetadata.labelMode
onSelected: key => labelModeCombo.currentKey = key
minimumWidth: 200 * scaling
@@ -45,8 +41,8 @@ ColumnLayout {
NToggle {
id: hideUnoccupiedToggle
label: "Hide unoccupied"
description: "Don't display workspaces without windows."
label: I18n.tr("bar.widget-settings.workspace.hide-unoccupied.label")
description: I18n.tr("bar.widget-settings.workspace.hide-unoccupied.description")
checked: widgetData.hideUnoccupied
onToggled: checked => hideUnoccupiedToggle.checked = checked
}
@@ -3,7 +3,7 @@ import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import qs.Modules.SettingsPanel.Tabs as Tabs
import qs.Modules.Settings.Tabs
import qs.Commons
import qs.Services
import qs.Widgets
@@ -21,7 +21,7 @@ NPanel {
panelKeyboardFocus: true
draggable: true
draggable: !PanelService.hasOpenedPopup
// Tabs enumeration, order is NOT relevant
enum Tab {
@@ -52,131 +52,131 @@ NPanel {
Component {
id: generalTab
Tabs.GeneralTab {}
GeneralTab {}
}
Component {
id: launcherTab
Tabs.LauncherTab {}
LauncherTab {}
}
Component {
id: barTab
Tabs.BarTab {}
BarTab {}
}
Component {
id: audioTab
Tabs.AudioTab {}
AudioTab {}
}
Component {
id: displayTab
Tabs.DisplayTab {}
DisplayTab {}
}
Component {
id: networkTab
Tabs.NetworkTab {}
NetworkTab {}
}
Component {
id: locationTab
Tabs.LocationTab {}
LocationTab {}
}
Component {
id: colorSchemeTab
Tabs.ColorSchemeTab {}
ColorSchemeTab {}
}
Component {
id: wallpaperTab
Tabs.WallpaperTab {}
WallpaperTab {}
}
Component {
id: screenRecorderTab
Tabs.ScreenRecorderTab {}
ScreenRecorderTab {}
}
Component {
id: aboutTab
Tabs.AboutTab {}
AboutTab {}
}
Component {
id: hooksTab
Tabs.HooksTab {}
HooksTab {}
}
Component {
id: dockTab
Tabs.DockTab {}
DockTab {}
}
Component {
id: notificationsTab
Tabs.NotificationsTab {}
NotificationsTab {}
}
// Order *DOES* matter
function updateTabsModel() {
let newTabs = [{
"id": SettingsPanel.Tab.General,
"label": "General",
"label": "settings.general.title",
"icon": "settings-general",
"source": generalTab
}, {
"id": SettingsPanel.Tab.Bar,
"label": "Bar",
"label": "settings.bar.title",
"icon": "settings-bar",
"source": barTab
}, {
"id": SettingsPanel.Tab.Dock,
"label": "Dock",
"label": "settings.dock.title",
"icon": "settings-dock",
"source": dockTab
}, {
"id": SettingsPanel.Tab.Launcher,
"label": "Launcher",
"label": "settings.launcher.title",
"icon": "settings-launcher",
"source": launcherTab
}, {
"id": SettingsPanel.Tab.Audio,
"label": "Audio",
"label": "settings.audio.title",
"icon": "settings-audio",
"source": audioTab
}, {
"id": SettingsPanel.Tab.Display,
"label": "Display",
"label": "settings.display.title",
"icon": "settings-display",
"source": displayTab
}, {
"id": SettingsPanel.Tab.Notifications,
"label": "Notifications",
"label": "settings.notifications.title",
"icon": "settings-notifications",
"source": notificationsTab
}, {
"id": SettingsPanel.Tab.Network,
"label": "Network",
"label": "settings.network.title",
"icon": "settings-network",
"source": networkTab
}, {
"id": SettingsPanel.Tab.Location,
"label": "Location",
"label": "settings.location.title",
"icon": "settings-location",
"source": locationTab
}, {
"id": SettingsPanel.Tab.ColorScheme,
"label": "Color Scheme",
"label": "settings.color-scheme.title",
"icon": "settings-color-scheme",
"source": colorSchemeTab
}, {
"id": SettingsPanel.Tab.Wallpaper,
"label": "Wallpaper",
"label": "settings.wallpaper.title",
"icon": "settings-wallpaper",
"source": wallpaperTab
}, {
"id": SettingsPanel.Tab.ScreenRecorder,
"label": "Screen Recorder",
"label": "settings.screen-recorder.title",
"icon": "settings-screen-recorder",
"source": screenRecorderTab
}, {
"id": SettingsPanel.Tab.Hooks,
"label": "Hooks",
"label": "settings.hooks.title",
"icon": "settings-hooks",
"source": hooksTab
}, {
"id": SettingsPanel.Tab.About,
"label": "About",
"label": "settings.about.title",
"icon": "settings-about",
"source": aboutTab
}]
@@ -391,7 +391,7 @@ NPanel {
// Tab label
NText {
text: modelData.label
text: I18n.tr(modelData.label)
color: tabTextColor
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
@@ -451,7 +451,7 @@ NPanel {
// Main title
NText {
text: root.tabsModel[currentTabIndex]?.label || ""
text: I18n.tr(root.tabsModel[currentTabIndex]?.label) || ""
font.pointSize: Style.fontSizeXL * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
@@ -462,7 +462,7 @@ NPanel {
// Close button
NIconButton {
icon: "close"
tooltipText: "Close."
tooltipText: I18n.tr("tooltips.close")
Layout.alignment: Qt.AlignVCenter
onClicked: root.close()
}
@@ -17,8 +17,8 @@ ColumnLayout {
property var contributors: GitHubService.contributors
NHeader {
label: "Noctalia shell"
description: "A sleek and minimal desktop shell thoughtfully crafted for Wayland, built with Quickshell."
label: I18n.tr("settings.about.noctalia.section.label")
description: I18n.tr("settings.about.noctalia.section.description")
}
RowLayout {
@@ -31,7 +31,7 @@ ColumnLayout {
columnSpacing: Style.marginS * scaling
NText {
text: "Latest version:"
text: I18n.tr("settings.about.noctalia.latest-version")
color: Color.mOnSurface
}
@@ -42,7 +42,7 @@ ColumnLayout {
}
NText {
text: "Installed version:"
text: I18n.tr("settings.about.noctalia.installed-version")
color: Color.mOnSurface
}
@@ -97,7 +97,7 @@ ColumnLayout {
NText {
id: updateText
text: "Download latest release"
text: I18n.tr("settings.about.noctalia.download-latest")
font.pointSize: Style.fontSizeL * scaling
color: updateArea.containsMouse ? Color.mSurface : Color.mPrimary
}
@@ -123,8 +123,12 @@ ColumnLayout {
}
NHeader {
label: "Contributors"
description: `Shout-out to our ${root.contributors.length} <b>awesome</b> contributors!`
label: I18n.tr("settings.about.contributors.section.label")
description: root.contributors.length === 1 ? I18n.tr("settings.about.contributors.section.description", {
"count": root.contributors.length
}) : I18n.tr("settings.about.contributors.section.description_plural", {
"count": root.contributors.length
})
}
GridView {
@@ -11,8 +11,8 @@ ColumnLayout {
spacing: Style.marginL * scaling
NHeader {
label: "Volumes"
description: "Adjust volume controls and audio levels."
label: I18n.tr("settings.audio.volumes.section.label")
description: I18n.tr("settings.audio.volumes.section.description")
}
property real localVolume: AudioService.volume
@@ -30,15 +30,15 @@ ColumnLayout {
Layout.fillWidth: true
NLabel {
label: "Output volume"
description: "System-wide volume level."
label: I18n.tr("settings.audio.volumes.output-volume.label")
description: I18n.tr("settings.audio.volumes.output-volume.description")
}
// 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: Style.animationFast
interval: 100
running: true
repeat: true
onTriggered: {
@@ -51,13 +51,11 @@ ColumnLayout {
NValueSlider {
Layout.fillWidth: true
from: 0
to: Settings.data.audio.volumeOverdrive ? 2.0 : 1.0
to: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0
value: localVolume
stepSize: 0.01
text: Math.floor(AudioService.volume * 100) + "%"
onMoved: {
localVolume = value
}
text: Math.round(AudioService.volume * 100) + "%"
onMoved: value => localVolume = value
}
}
@@ -67,8 +65,8 @@ ColumnLayout {
Layout.fillWidth: true
NToggle {
label: "Mute audio output"
description: "Mute the system's main audio output."
label: I18n.tr("settings.audio.volumes.mute-output.label")
description: I18n.tr("settings.audio.volumes.mute-output.description")
checked: AudioService.muted
onToggled: checked => {
if (AudioService.sink && AudioService.sink.audio) {
@@ -84,17 +82,17 @@ ColumnLayout {
Layout.fillWidth: true
NLabel {
label: "Input volume"
description: "Microphone input volume level."
label: I18n.tr("settings.audio.volumes.input-volume.label")
description: I18n.tr("settings.audio.volumes.input-volume.description")
}
NValueSlider {
Layout.fillWidth: true
from: 0
to: 1.0
to: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0
value: AudioService.inputVolume
stepSize: 0.01
text: Math.floor(AudioService.inputVolume * 100) + "%"
text: Math.round(AudioService.inputVolume * 100) + "%"
onMoved: value => AudioService.setInputVolume(value)
}
}
@@ -105,8 +103,8 @@ ColumnLayout {
Layout.fillWidth: true
NToggle {
label: "Mute audio input"
description: "Mute the default audio input (microphone)."
label: I18n.tr("settings.audio.volumes.mute-input.label")
description: I18n.tr("settings.audio.volumes.mute-input.description")
checked: AudioService.inputMuted
onToggled: checked => AudioService.setInputMuted(checked)
}
@@ -119,8 +117,8 @@ ColumnLayout {
NSpinBox {
Layout.fillWidth: true
label: "Volume step size"
description: "Adjust the step size for volume changes (scroll wheel, keyboard shortcuts)."
label: I18n.tr("settings.audio.volumes.step-size.label")
description: I18n.tr("settings.audio.volumes.step-size.description")
minimum: 1
maximum: 25
value: Settings.data.audio.volumeStep
@@ -130,6 +128,19 @@ ColumnLayout {
}
}
// Raise maximum volume above 100%
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
NToggle {
label: I18n.tr("settings.audio.volumes.volume-overdrive.label")
description: I18n.tr("settings.audio.volumes.volume-overdrive.description")
checked: Settings.data.audio.volumeOverdrive
onToggled: checked => Settings.data.audio.volumeOverdrive = checked
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
@@ -141,8 +152,8 @@ ColumnLayout {
spacing: Style.marginS * scaling
NHeader {
label: "Audio devices"
description: "Choose your audio input and output devices."
label: I18n.tr("settings.audio.devices.section.label")
description: I18n.tr("settings.audio.devices.section.description")
}
// -------------------------------
@@ -157,8 +168,8 @@ ColumnLayout {
Layout.bottomMargin: Style.marginL * scaling
NLabel {
label: "Output device"
description: "Select the desired audio output device."
label: I18n.tr("settings.audio.devices.output-device.label")
description: I18n.tr("settings.audio.devices.output-device.description")
}
Repeater {
@@ -167,7 +178,10 @@ ColumnLayout {
required property PwNode modelData
ButtonGroup.group: sinks
checked: AudioService.sink?.id === modelData.id
onClicked: AudioService.setAudioSink(modelData)
onClicked: {
AudioService.setAudioSink(modelData)
localVolume = AudioService.volume
}
text: modelData.description
}
}
@@ -184,8 +198,8 @@ ColumnLayout {
Layout.fillWidth: true
NLabel {
label: "Input device"
description: "Select the desired audio input device."
label: I18n.tr("settings.audio.devices.input-device.label")
description: I18n.tr("settings.audio.devices.input-device.description")
}
Repeater {
@@ -213,15 +227,15 @@ ColumnLayout {
spacing: Style.marginL * scaling
NHeader {
label: "Media players"
description: "Set your preferred and ignored media applications."
label: I18n.tr("settings.audio.media.section.label")
description: I18n.tr("settings.audio.media.section.description")
}
// Preferred player
NTextInput {
label: "Primary player"
description: "Enter a keyword to identify your main player."
placeholderText: "e.g. spotify, vlc, mpv"
label: I18n.tr("settings.audio.media.primary-player.label")
description: I18n.tr("settings.audio.media.primary-player.description")
placeholderText: I18n.tr("settings.audio.media.primary-player.placeholder")
text: Settings.data.audio.preferredPlayer
onTextChanged: {
Settings.data.audio.preferredPlayer = text
@@ -240,9 +254,9 @@ ColumnLayout {
NTextInput {
id: blacklistInput
label: "Excluded player"
description: "Add keywords for players you want the system to ignore. Each keyword should be on a new line."
placeholderText: "type substring and press +"
label: I18n.tr("settings.audio.media.excluded-player.label")
description: I18n.tr("settings.audio.media.excluded-player.description")
placeholderText: I18n.tr("settings.audio.media.excluded-player.placeholder")
}
// Button aligned to the center of the actual input field
@@ -321,90 +335,72 @@ ColumnLayout {
}
}
}
}
// Divider
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// AudioService Visualizer Category
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
NHeader {
label: "Audio visualizer"
description: "Customize visual effects that respond to audio playback."
}
// AudioService Visualizer section
NComboBox {
id: audioVisualizerCombo
label: "Visualization type"
description: "Choose a visualization type for media playback"
model: ListModel {
ListElement {
key: "none"
name: "None"
}
ListElement {
key: "linear"
name: "Linear"
}
ListElement {
key: "mirrored"
name: "Mirrored"
}
ListElement {
key: "wave"
name: "Wave"
}
}
label: I18n.tr("settings.audio.media.visualizer-type.label")
description: I18n.tr("settings.audio.media.visualizer-type.description")
model: [{
"key": "none",
"name": I18n.tr("options.visualizer-types.none")
}, {
"key": "linear",
"name": I18n.tr("options.visualizer-types.linear")
}, {
"key": "mirrored",
"name": I18n.tr("options.visualizer-types.mirrored")
}, {
"key": "wave",
"name": I18n.tr("options.visualizer-types.wave")
}]
currentKey: Settings.data.audio.visualizerType
onSelected: key => Settings.data.audio.visualizerType = key
}
NComboBox {
label: "Frame rate"
description: "Higher rates are smoother but use more resources."
model: ListModel {
ListElement {
key: "30"
name: "30 FPS"
}
ListElement {
key: "60"
name: "60 FPS"
}
ListElement {
key: "100"
name: "100 FPS"
}
ListElement {
key: "120"
name: "120 FPS"
}
ListElement {
key: "144"
name: "144 FPS"
}
ListElement {
key: "165"
name: "165 FPS"
}
ListElement {
key: "240"
name: "240 FPS"
}
}
label: I18n.tr("settings.audio.media.frame-rate.label")
description: I18n.tr("settings.audio.media.frame-rate.description")
model: [{
"key": "30",
"name": I18n.tr("options.frame-rates.fps", {
"fps": "30"
})
}, {
"key": "60",
"name": I18n.tr("options.frame-rates.fps", {
"fps": "60"
})
}, {
"key": "100",
"name": I18n.tr("options.frame-rates.fps", {
"fps": "100"
})
}, {
"key": "120",
"name": I18n.tr("options.frame-rates.fps", {
"fps": "120"
})
}, {
"key": "144",
"name": I18n.tr("options.frame-rates.fps", {
"fps": "144"
})
}, {
"key": "165",
"name": I18n.tr("options.frame-rates.fps", {
"fps": "165"
})
}, {
"key": "240",
"name": I18n.tr("options.frame-rates.fps", {
"fps": "240"
})
}]
currentKey: Settings.data.audio.cavaFrameRate
onSelected: key => Settings.data.audio.cavaFrameRate = key
}
}
// Divider
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
@@ -5,7 +5,7 @@ import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
import qs.Modules.SettingsPanel.Bar
import qs.Modules.Settings.Bar
ColumnLayout {
id: root
@@ -41,54 +41,45 @@ ColumnLayout {
}
NHeader {
label: "Appearance"
description: "Customize the bar's appearance and position."
label: I18n.tr("settings.bar.appearance.section.label")
description: I18n.tr("settings.bar.appearance.section.description")
}
NComboBox {
Layout.fillWidth: true
label: "Bar position"
description: "Choose where to place the bar on the screen."
model: ListModel {
ListElement {
key: "top"
name: "Top"
}
ListElement {
key: "bottom"
name: "Bottom"
}
ListElement {
key: "left"
name: "Left"
}
ListElement {
key: "right"
name: "Right"
}
}
label: I18n.tr("settings.bar.appearance.position.label")
description: I18n.tr("settings.bar.appearance.position.description")
model: [{
"key": "top",
"name": I18n.tr("options.bar.position.top")
}, {
"key": "bottom",
"name": I18n.tr("options.bar.position.bottom")
}, {
"key": "left",
"name": I18n.tr("options.bar.position.left")
}, {
"key": "right",
"name": I18n.tr("options.bar.position.right")
}]
currentKey: Settings.data.bar.position
onSelected: key => Settings.data.bar.position = key
}
NComboBox {
Layout.fillWidth: true
label: "Bar density"
description: "Adjust the bar's padding for a compact or spacious look."
model: ListModel {
ListElement {
key: "compact"
name: "Compact"
}
ListElement {
key: "default"
name: "Default"
}
ListElement {
key: "comfortable"
name: "Comfortable"
}
}
label: I18n.tr("settings.bar.appearance.density.label")
description: I18n.tr("settings.bar.appearance.density.description")
model: [{
"key": "compact",
"name": I18n.tr("options.bar.density.compact")
}, {
"key": "default",
"name": I18n.tr("options.bar.density.default")
}, {
"key": "comfortable",
"name": I18n.tr("options.bar.density.comfortable")
}]
currentKey: Settings.data.bar.density
onSelected: key => Settings.data.bar.density = key
}
@@ -98,8 +89,8 @@ ColumnLayout {
Layout.fillWidth: true
NLabel {
label: "Background opacity"
description: "Adjust the background opacity of the bar."
label: I18n.tr("settings.bar.appearance.background-opacity.label")
description: I18n.tr("settings.bar.appearance.background-opacity.description")
}
NValueSlider {
@@ -115,16 +106,16 @@ ColumnLayout {
NToggle {
Layout.fillWidth: true
label: "Show capsule"
description: "Show widget backgrounds."
label: I18n.tr("settings.bar.appearance.show-capsule.label")
description: I18n.tr("settings.bar.appearance.show-capsule.description")
checked: Settings.data.bar.showCapsule
onToggled: checked => Settings.data.bar.showCapsule = checked
}
NToggle {
Layout.fillWidth: true
label: "Floating bar"
description: "Displays the bar as a floating 'pill'. Note: This will move the screen corners to the edges."
label: I18n.tr("settings.bar.appearance.floating.label")
description: I18n.tr("settings.bar.appearance.floating.description")
checked: Settings.data.bar.floating
onToggled: checked => Settings.data.bar.floating = checked
}
@@ -136,8 +127,8 @@ ColumnLayout {
Layout.fillWidth: true
NLabel {
label: "Margins"
description: "Adjust the margins around the floating bar."
label: I18n.tr("settings.bar.appearance.margins.label")
description: I18n.tr("settings.bar.appearance.margins.description")
}
RowLayout {
@@ -148,7 +139,7 @@ ColumnLayout {
spacing: Style.marginXXS * scaling
NText {
text: "Vertical"
text: I18n.tr("settings.bar.appearance.margins.vertical")
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
}
@@ -168,7 +159,7 @@ ColumnLayout {
spacing: Style.marginXXS * scaling
NText {
text: "Horizontal"
text: I18n.tr("settings.bar.appearance.margins.horizontal")
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
}
@@ -198,8 +189,8 @@ ColumnLayout {
Layout.fillWidth: true
NHeader {
label: "Widgets positioning"
description: "Drag and drop widgets to reorder them within each section, or use the add/remove buttons to manage widgets."
label: I18n.tr("settings.bar.widgets.section.label")
description: I18n.tr("settings.bar.widgets.section.description")
}
// Bar Sections
@@ -219,6 +210,7 @@ ColumnLayout {
onRemoveWidget: (section, index) => _removeWidgetFromSection(section, index)
onReorderWidget: (section, fromIndex, toIndex) => _reorderWidgetInSection(section, fromIndex, toIndex)
onUpdateWidgetSettings: (section, index, settings) => _updateWidgetSettingsInSection(section, index, settings)
onMoveWidget: (fromSection, index, toSection) => _moveWidgetBetweenSections(fromSection, index, toSection)
onDragPotentialStarted: root.handleDragStart()
onDragPotentialEnded: root.handleDragEnd()
}
@@ -233,6 +225,7 @@ ColumnLayout {
onRemoveWidget: (section, index) => _removeWidgetFromSection(section, index)
onReorderWidget: (section, fromIndex, toIndex) => _reorderWidgetInSection(section, fromIndex, toIndex)
onUpdateWidgetSettings: (section, index, settings) => _updateWidgetSettingsInSection(section, index, settings)
onMoveWidget: (fromSection, index, toSection) => _moveWidgetBetweenSections(fromSection, index, toSection)
onDragPotentialStarted: root.handleDragStart()
onDragPotentialEnded: root.handleDragEnd()
}
@@ -247,6 +240,7 @@ ColumnLayout {
onRemoveWidget: (section, index) => _removeWidgetFromSection(section, index)
onReorderWidget: (section, fromIndex, toIndex) => _reorderWidgetInSection(section, fromIndex, toIndex)
onUpdateWidgetSettings: (section, index, settings) => _updateWidgetSettingsInSection(section, index, settings)
onMoveWidget: (fromSection, index, toSection) => _moveWidgetBetweenSections(fromSection, index, toSection)
onDragPotentialStarted: root.handleDragStart()
onDragPotentialEnded: root.handleDragEnd()
}
@@ -265,8 +259,8 @@ ColumnLayout {
Layout.fillWidth: true
NHeader {
label: "Monitor display"
description: "Show bar on specific monitors. Defaults to all if none are chosen."
label: I18n.tr("settings.bar.monitors.section.label")
description: I18n.tr("settings.bar.monitors.section.description")
}
Repeater {
@@ -274,7 +268,11 @@ ColumnLayout {
delegate: NCheckbox {
Layout.fillWidth: true
label: modelData.name || "Unknown"
description: `${modelData.model} (${modelData.width}x${modelData.height})`
description: I18n.tr("system.monitor-description", {
"model": modelData.model,
"width": modelData.width,
"height": modelData.height
})
checked: (Settings.data.bar.monitors || []).indexOf(modelData.name) !== -1
onToggled: checked => {
if (checked) {
@@ -341,6 +339,25 @@ ColumnLayout {
//Logger.log("BarTab", `Updated widget settings for ${settings.id} in ${section} section`)
}
function _moveWidgetBetweenSections(fromSection, index, toSection) {
// Get the widget from the source section
if (index >= 0 && index < Settings.data.bar.widgets[fromSection].length) {
var widget = Settings.data.bar.widgets[fromSection][index]
// Remove from source section
var sourceArray = Settings.data.bar.widgets[fromSection].slice()
sourceArray.splice(index, 1)
Settings.data.bar.widgets[fromSection] = sourceArray
// Add to target section
var targetArray = Settings.data.bar.widgets[toSection].slice()
targetArray.push(widget)
Settings.data.bar.widgets[toSection] = targetArray
//Logger.log("BarTab", `Moved widget ${widget.id} from ${fromSection} to ${toSection}`)
}
}
// Base list model for all combo boxes
ListModel {
id: availableWidgets
@@ -65,10 +65,10 @@ ColumnLayout {
// Matugen exists, enable it
Settings.data.colorSchemes.useWallpaperColors = true
MatugenService.generateFromWallpaper()
ToastService.showNotice("Matugen", "Enabled")
ToastService.showNotice(I18n.tr("settings.color-scheme.color-source.enable-matugen.label"), I18n.tr("toast.matugen.enabled"))
} else {
// Matugen not found
ToastService.showWarning("Matugen", "Not installed")
ToastService.showWarning(I18n.tr("settings.color-scheme.color-source.enable-matugen.label"), I18n.tr("toast.matugen.not-installed"))
}
}
@@ -106,14 +106,14 @@ ColumnLayout {
// Main Toggles - Dark Mode / Matugen
NHeader {
label: "Color Source"
description: "Main settings for Noctalia's colors."
label: I18n.tr("settings.color-scheme.color-source.section.label")
description: I18n.tr("settings.color-scheme.color-source.section.description")
}
// Dark Mode Toggle (affects both Matugen and predefined schemes that provide variants)
NToggle {
label: "Dark mode"
description: "Switches to a darker theme for easier viewing at night."
label: I18n.tr("settings.color-scheme.color-source.dark-mode.label")
description: I18n.tr("settings.color-scheme.color-source.dark-mode.description")
checked: Settings.data.colorSchemes.darkMode
enabled: true
onToggled: checked => Settings.data.colorSchemes.darkMode = checked
@@ -121,8 +121,8 @@ ColumnLayout {
// Use Matugen
NToggle {
label: "Enable Matugen"
description: "Automatically generate colors based on your active wallpaper."
label: I18n.tr("settings.color-scheme.color-source.enable-matugen.label")
description: I18n.tr("settings.color-scheme.color-source.enable-matugen.description")
checked: Settings.data.colorSchemes.useWallpaperColors
onToggled: checked => {
if (checked) {
@@ -130,7 +130,7 @@ ColumnLayout {
matugenCheck.running = true
} else {
Settings.data.colorSchemes.useWallpaperColors = false
ToastService.showNotice("Matugen", "Disabled")
ToastService.showNotice(I18n.tr("settings.color-scheme.color-source.enable-matugen.label"), I18n.tr("toast.matugen.disabled"))
if (Settings.data.colorSchemes.predefinedScheme) {
@@ -152,8 +152,8 @@ ColumnLayout {
Layout.fillWidth: true
NHeader {
label: "Predefined color schemes"
description: "To use these color schemes, you must turn off Matugen. With Matugen enabled, colors are automatically generated from your wallpaper."
label: I18n.tr("settings.color-scheme.predefined.section.label")
description: I18n.tr("settings.color-scheme.predefined.section.description")
}
// Color Schemes Grid
@@ -324,16 +324,23 @@ ColumnLayout {
visible: Settings.data.colorSchemes.useWallpaperColors
spacing: Style.marginL * scaling
NHeader {
label: I18n.tr("settings.color-scheme.matugen.section.label")
description: I18n.tr("settings.color-scheme.matugen.section.description")
}
// UI Components
NCollapsible {
Layout.fillWidth: true
label: "UI"
description: "Desktop environment and UI toolkit theming."
label: I18n.tr("settings.color-scheme.matugen.ui.label")
description: I18n.tr("settings.color-scheme.matugen.ui.description")
defaultExpanded: false
NCheckbox {
label: "GTK 4 (libadwaita)"
description: "Write ~/.config/gtk-4.0/gtk.css"
description: I18n.tr("settings.color-scheme.matugen.ui.gtk4.description", {
"filepath": "~/.config/gtk-4.0/gtk.css"
})
checked: Settings.data.matugen.gtk4
onToggled: checked => {
Settings.data.matugen.gtk4 = checked
@@ -344,7 +351,9 @@ ColumnLayout {
NCheckbox {
label: "GTK 3"
description: "Write ~/.config/gtk-3.0/gtk.css"
description: I18n.tr("settings.color-scheme.matugen.ui.gtk3.description", {
"filepath": "~/.config/gtk-3.0/gtk.css"
})
checked: Settings.data.matugen.gtk3
onToggled: checked => {
Settings.data.matugen.gtk3 = checked
@@ -355,7 +364,9 @@ ColumnLayout {
NCheckbox {
label: "Qt6ct"
description: "Write ~/.config/qt6ct/colors/noctalia.conf"
description: I18n.tr("settings.color-scheme.matugen.ui.qt6.description", {
"filepath": "~/.config/qt6ct/colors/noctalia.conf"
})
checked: Settings.data.matugen.qt6
onToggled: checked => {
Settings.data.matugen.qt6 = checked
@@ -366,7 +377,9 @@ ColumnLayout {
NCheckbox {
label: "Qt5ct"
description: "Write ~/.config/qt5ct/colors/noctalia.conf"
description: I18n.tr("settings.color-scheme.matugen.ui.qt5.description", {
"filepath": "~/.config/qt5ct/colors/noctalia.conf"
})
checked: Settings.data.matugen.qt5
onToggled: checked => {
Settings.data.matugen.qt5 = checked
@@ -379,13 +392,17 @@ ColumnLayout {
// Terminal Emulators
NCollapsible {
Layout.fillWidth: true
label: "Terminal"
description: "Terminal emulator theming."
label: I18n.tr("settings.color-scheme.matugen.terminal.label")
description: I18n.tr("settings.color-scheme.matugen.terminal.description")
defaultExpanded: false
NCheckbox {
label: "Kitty"
description: ProgramCheckerService.kittyAvailable ? "Write ~/.config/kitty/themes/noctalia.conf and reload" : "Requires kitty terminal to be installed"
description: ProgramCheckerService.kittyAvailable ? I18n.tr("settings.color-scheme.matugen.terminal.kitty.description", {
"filepath": "~/.config/kitty/themes/noctalia.conf"
}) : I18n.tr("settings.color-scheme.matugen.terminal.kitty.description-missing", {
"app": "kitty"
})
checked: Settings.data.matugen.kitty
enabled: ProgramCheckerService.kittyAvailable
opacity: ProgramCheckerService.kittyAvailable ? 1.0 : 0.6
@@ -400,7 +417,11 @@ ColumnLayout {
NCheckbox {
label: "Ghostty"
description: ProgramCheckerService.ghosttyAvailable ? "Write ~/.config/ghostty/themes/noctalia and reload" : "Requires ghostty terminal to be installed"
description: ProgramCheckerService.ghosttyAvailable ? I18n.tr("settings.color-scheme.matugen.terminal.ghostty.description", {
"filepath": "~/.config/ghostty/themes/noctalia"
}) : I18n.tr("settings.color-scheme.matugen.terminal.ghostty.description-missing", {
"app": "ghostty"
})
checked: Settings.data.matugen.ghostty
enabled: ProgramCheckerService.ghosttyAvailable
opacity: ProgramCheckerService.ghosttyAvailable ? 1.0 : 0.6
@@ -415,7 +436,11 @@ ColumnLayout {
NCheckbox {
label: "Foot"
description: ProgramCheckerService.footAvailable ? "Write ~/.config/foot/themes/noctalia and reload" : "Requires foot terminal to be installed"
description: ProgramCheckerService.footAvailable ? I18n.tr("settings.color-scheme.matugen.terminal.foot.description", {
"filepath": "~/.config/foot/themes/noctalia"
}) : I18n.tr("settings.color-scheme.matugen.terminal.foot.description-missing", {
"app": "foot"
})
checked: Settings.data.matugen.foot
enabled: ProgramCheckerService.footAvailable
opacity: ProgramCheckerService.footAvailable ? 1.0 : 0.6
@@ -432,13 +457,17 @@ ColumnLayout {
// Applications
NCollapsible {
Layout.fillWidth: true
label: "Programs"
description: "Application-specific theming."
label: I18n.tr("settings.color-scheme.matugen.programs.label")
description: I18n.tr("settings.color-scheme.matugen.programs.description")
defaultExpanded: false
NCheckbox {
label: "Fuzzel"
description: ProgramCheckerService.fuzzelAvailable ? "Write ~/.config/fuzzel/themes/noctalia and reload" : "Requires fuzzel launcher to be installed"
description: ProgramCheckerService.fuzzelAvailable ? I18n.tr("settings.color-scheme.matugen.programs.fuzzel.description", {
"filepath": "~/.config/fuzzel/themes/noctalia"
}) : I18n.tr("settings.color-scheme.matugen.programs.fuzzel.description-missing", {
"app": "fuzzel"
})
checked: Settings.data.matugen.fuzzel
enabled: ProgramCheckerService.fuzzelAvailable
opacity: ProgramCheckerService.fuzzelAvailable ? 1.0 : 0.6
@@ -453,7 +482,11 @@ ColumnLayout {
NCheckbox {
label: "Vesktop"
description: ProgramCheckerService.vesktopAvailable ? "Write ~/.config/vesktop/themes/noctalia.theme.css" : "Requires vesktop Discord client to be installed"
description: ProgramCheckerService.vesktopAvailable ? I18n.tr("settings.color-scheme.matugen.programs.vesktop.description", {
"filepath": "~/.config/vesktop/themes/noctalia.theme.css"
}) : I18n.tr("settings.color-scheme.matugen.programs.vesktop.description-missing", {
"app": "vesktop"
})
checked: Settings.data.matugen.vesktop
enabled: ProgramCheckerService.vesktopAvailable
opacity: ProgramCheckerService.vesktopAvailable ? 1.0 : 0.6
@@ -467,8 +500,12 @@ ColumnLayout {
}
NCheckbox {
label: "Pywalfox (Firefox)"
description: ProgramCheckerService.pywalfoxAvailable ? "Write ~/.cache/wal/colors.json and run pywalfox update" : "Requires pywalfox package to be installed"
label: "Pywalfox"
description: ProgramCheckerService.pywalfoxAvailable ? I18n.tr("settings.color-scheme.matugen.programs.pywalfox.description", {
"filepath": "~/.cache/wal/colors.json"
}) : I18n.tr("settings.color-scheme.matugen.programs.pywalfox.description-missing", {
"app": "pywalfox"
})
checked: Settings.data.matugen.pywalfox
enabled: ProgramCheckerService.pywalfoxAvailable
opacity: ProgramCheckerService.pywalfoxAvailable ? 1.0 : 0.6
@@ -485,13 +522,13 @@ ColumnLayout {
// Miscellaneous
NCollapsible {
Layout.fillWidth: true
label: "Misc"
description: "Additional configuration options."
label: I18n.tr("settings.color-scheme.matugen.misc.label")
description: I18n.tr("settings.color-scheme.matugen.misc.description")
defaultExpanded: false
NCheckbox {
label: "User Templates"
description: "Enable user-defined Matugen config from ~/.config/matugen/config.toml"
label: I18n.tr("settings.color-scheme.matugen.misc.user-templates.label")
description: I18n.tr("settings.color-scheme.matugen.misc.user-templates.description")
checked: Settings.data.matugen.enableUserTemplates
onToggled: checked => {
Settings.data.matugen.enableUserTemplates = checked
@@ -38,10 +38,10 @@ ColumnLayout {
if (exitCode === 0) {
Settings.data.nightLight.enabled = true
NightLightService.apply()
ToastService.showNotice("Night Light", "Enabled")
ToastService.showNotice(I18n.tr("settings.display.night-light.section.label"), I18n.tr("toast.night-light.enabled"))
} else {
Settings.data.nightLight.enabled = false
ToastService.showWarning("Night Light", "wlsunset not installed")
ToastService.showWarning(I18n.tr("settings.display.night-light.section.label"), I18n.tr("toast.night-light.not-installed"))
}
}
@@ -52,8 +52,8 @@ ColumnLayout {
spacing: Style.marginL * scaling
NHeader {
label: "Per-monitor settings"
description: "Adjust scaling and brightness for each display."
label: I18n.tr("settings.display.monitors.section.label")
description: I18n.tr("settings.display.monitors.section.description")
}
ColumnLayout {
@@ -63,7 +63,7 @@ ColumnLayout {
model: Quickshell.screens || []
delegate: Rectangle {
Layout.fillWidth: true
implicitHeight: contentCol.implicitHeight + Style.marginXL * 2 * scaling
implicitHeight: contentCol.implicitHeight + Style.marginL * 2 * scaling
radius: Style.radiusM * scaling
color: Color.mSurfaceVariant
border.color: Color.mOutline
@@ -83,13 +83,18 @@ ColumnLayout {
ColumnLayout {
id: contentCol
anchors.fill: parent
anchors.margins: Style.marginL * scaling
width: parent.width - 2 * Style.marginL * scaling
x: Style.marginL * scaling
y: Style.marginL * scaling
spacing: Style.marginXXS * scaling
NLabel {
label: modelData.name || "Unknown"
description: `${modelData.model} (${modelData.width}x${modelData.height})`
description: I18n.tr("system.monitor-description", {
"model": modelData.model,
"width": modelData.width,
"height": modelData.height
})
}
// Scale
@@ -98,11 +103,11 @@ ColumnLayout {
Layout.fillWidth: true
RowLayout {
spacing: Style.marginM * scaling
spacing: Style.marginL * scaling
Layout.fillWidth: true
NText {
text: "Scale"
text: I18n.tr("settings.display.monitors.scale")
Layout.preferredWidth: 80 * scaling
}
@@ -113,7 +118,9 @@ ColumnLayout {
stepSize: 0.01
value: localScaling
onPressedChanged: (pressed, value) => ScalingService.setScreenScale(modelData, value)
text: `${Math.round(localScaling * 100)}%`
text: I18n.tr("system.scaling-percentage", {
"percentage": Math.round(localScaling * 100)
})
Layout.fillWidth: true
}
@@ -125,7 +132,7 @@ ColumnLayout {
NIconButton {
icon: "refresh"
baseSize: Style.baseWidgetSize * 0.9
tooltipText: "Reset scaling."
tooltipText: I18n.tr("settings.display.monitors.reset-scaling")
onClicked: ScalingService.setScreenScale(modelData, 1.0)
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
@@ -142,10 +149,10 @@ ColumnLayout {
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
spacing: Style.marginL * scaling
NText {
text: "Brightness"
text: I18n.tr("settings.display.monitors.brightness")
Layout.preferredWidth: 80 * scaling
}
@@ -179,24 +186,6 @@ ColumnLayout {
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// Brightness Section
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
NHeader {
label: "Brightness"
description: "Adjust brightness related settings."
}
// Brightness Step Section
ColumnLayout {
spacing: Style.marginS * scaling
@@ -204,8 +193,8 @@ ColumnLayout {
NSpinBox {
Layout.fillWidth: true
label: "Brightness step size"
description: "Adjust the step size for brightness changes (scroll wheel and keyboard shortcuts)."
label: I18n.tr("settings.display.monitors.brightness-step.label")
description: I18n.tr("settings.display.monitors.brightness-step.description")
minimum: 1
maximum: 50
value: Settings.data.brightness.brightnessStep
@@ -228,14 +217,14 @@ ColumnLayout {
Layout.fillWidth: true
NHeader {
label: "Night light"
description: "Reduce blue light emission to help you sleep better and reduce eye strain."
label: I18n.tr("settings.display.night-light.section.label")
description: I18n.tr("settings.display.night-light.section.description")
}
}
NToggle {
label: "Enable night light"
description: "Apply a warm color filter to reduce blue light emission."
label: I18n.tr("settings.display.night-light.enable.label")
description: I18n.tr("settings.display.night-light.enable.description")
checked: Settings.data.nightLight.enabled
onToggled: checked => {
if (checked) {
@@ -245,7 +234,7 @@ ColumnLayout {
Settings.data.nightLight.enabled = false
Settings.data.nightLight.forced = false
NightLightService.apply()
ToastService.showNotice("Night Light", "Disabled")
ToastService.showNotice(I18n.tr("settings.display.night-light.section.label"), I18n.tr("toast.night-light.disabled"))
}
}
}
@@ -256,8 +245,8 @@ ColumnLayout {
Layout.alignment: Qt.AlignVCenter
NLabel {
label: "Color temperature"
description: "Set the color warmth for nighttime and daytime."
label: I18n.tr("settings.display.night-light.temperature.label")
description: I18n.tr("settings.display.night-light.temperature.description")
}
RowLayout {
@@ -268,7 +257,7 @@ ColumnLayout {
Layout.alignment: Qt.AlignVCenter
NText {
text: "Night"
text: I18n.tr("settings.display.night-light.temperature.night")
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignVCenter
@@ -289,10 +278,8 @@ ColumnLayout {
}
}
Item {}
NText {
text: "Day"
text: I18n.tr("settings.display.night-light.temperature.day")
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignVCenter
@@ -315,31 +302,31 @@ ColumnLayout {
}
NToggle {
label: "Automatic Scheduling"
description: `Based on the sunset and sunrise time in <i>${LocationService.stableName}</i> - recommended.`
label: I18n.tr("settings.display.night-light.auto-schedule.label")
description: I18n.tr("settings.display.night-light.auto-schedule.description", {
"location": LocationService.stableName
})
checked: Settings.data.nightLight.autoSchedule
onToggled: checked => Settings.data.nightLight.autoSchedule = checked
visible: Settings.data.nightLight.enabled
}
// Schedule settings
// Manual scheduling
ColumnLayout {
spacing: Style.marginXS * scaling
spacing: Style.marginS * scaling
visible: Settings.data.nightLight.enabled && !Settings.data.nightLight.autoSchedule && !Settings.data.nightLight.forced
NLabel {
label: I18n.tr("settings.display.night-light.manual-schedule.label")
description: I18n.tr("settings.display.night-light.manual-schedule.description")
}
RowLayout {
Layout.fillWidth: false
spacing: Style.marginM * scaling
NLabel {
label: "Manual Scheduling"
}
Item {// add a little more spacing
}
spacing: Style.marginS * scaling
NText {
text: "Sunrise Time"
text: I18n.tr("settings.display.night-light.manual-schedule.sunrise")
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
}
@@ -347,23 +334,25 @@ ColumnLayout {
NComboBox {
model: timeOptions
currentKey: Settings.data.nightLight.manualSunrise
placeholder: "Select start time"
placeholder: I18n.tr("settings.display.night-light.manual-schedule.select-start")
onSelected: key => Settings.data.nightLight.manualSunrise = key
minimumWidth: 120 * scaling
}
Item {// add a little more spacing
Item {
Layout.preferredWidth: 20 * scaling
}
NText {
text: "Sunset Time"
text: I18n.tr("settings.display.night-light.manual-schedule.sunset")
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
}
NComboBox {
model: timeOptions
currentKey: Settings.data.nightLight.manualSunset
placeholder: "Select stop time"
placeholder: I18n.tr("settings.display.night-light.manual-schedule.select-stop")
onSelected: key => Settings.data.nightLight.manualSunset = key
minimumWidth: 120 * scaling
}
@@ -372,8 +361,8 @@ ColumnLayout {
// Force activation toggle
NToggle {
label: "Force activation"
description: "Ignores the schedule and applies the night filter immediately."
label: I18n.tr("settings.display.night-light.force-activation.label")
description: I18n.tr("settings.display.night-light.force-activation.description")
checked: Settings.data.nightLight.forced
onToggled: checked => {
Settings.data.nightLight.forced = checked
@@ -24,20 +24,20 @@ ColumnLayout {
}
NHeader {
label: "Appearance"
description: "Customize the dock's behavior and appearance."
label: I18n.tr("settings.dock.appearance.section.label")
description: I18n.tr("settings.dock.appearance.section.description")
}
NToggle {
label: "Auto-hide"
description: "Automatically hide when not in use."
label: I18n.tr("settings.dock.appearance.auto-hide.label")
description: I18n.tr("settings.dock.appearance.auto-hide.description")
checked: Settings.data.dock.autoHide
onToggled: checked => Settings.data.dock.autoHide = checked
}
NToggle {
label: "Exclusive zone"
description: "Prevent window overlap."
label: I18n.tr("settings.dock.appearance.exclusive-zone.label")
description: I18n.tr("settings.dock.appearance.exclusive-zone.description")
checked: Settings.data.dock.exclusive
onToggled: checked => Settings.data.dock.exclusive = checked
}
@@ -46,8 +46,8 @@ ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
NLabel {
label: "Background opacity"
description: "Adjust the dock's background opacity."
label: I18n.tr("settings.dock.appearance.background-opacity.label")
description: I18n.tr("settings.dock.appearance.background-opacity.description")
}
NValueSlider {
Layout.fillWidth: true
@@ -65,8 +65,8 @@ ColumnLayout {
Layout.fillWidth: true
NLabel {
label: "Dock floating distance"
description: "Adjust the floating distance from the screen edge."
label: I18n.tr("settings.dock.appearance.floating-distance.label")
description: I18n.tr("settings.dock.appearance.floating-distance.description")
}
NValueSlider {
@@ -92,8 +92,8 @@ ColumnLayout {
Layout.fillWidth: true
NHeader {
label: "Monitor display"
description: "Choose which monitor to display the dock on."
label: I18n.tr("settings.dock.monitors.section.label")
description: I18n.tr("settings.dock.monitors.section.description")
}
Repeater {
@@ -101,7 +101,11 @@ ColumnLayout {
delegate: NCheckbox {
Layout.fillWidth: true
label: modelData.name || "Unknown"
description: `${modelData.model} (${modelData.width}x${modelData.height})`
description: I18n.tr("system.monitor-description", {
"model": modelData.model,
"width": modelData.width,
"height": modelData.height
})
checked: (Settings.data.dock.monitors || []).indexOf(modelData.name) !== -1
onToggled: checked => {
if (checked) {
@@ -10,8 +10,8 @@ ColumnLayout {
id: root
NHeader {
label: "Profile"
description: "Edit your user details and avatar."
label: I18n.tr("settings.general.profile.section.label")
description: I18n.tr("settings.general.profile.section.description")
}
// Profile section
@@ -30,18 +30,31 @@ ColumnLayout {
Layout.alignment: Qt.AlignTop
}
NTextInput {
Layout.fillWidth: true
label: `${Quickshell.env("USER") || "user"}'s profile picture`
description: "Your profile picture that appears throughout the interface."
NTextInputButton {
label: I18n.tr("settings.general.profile.picture.label", {
"user": Quickshell.env("USER" || "User")
})
description: I18n.tr("settings.general.profile.picture.description")
text: Settings.data.general.avatarImage
placeholderText: "/home/user/.face"
onEditingFinished: {
Settings.data.general.avatarImage = text
placeholderText: I18n.tr("placeholders.profile-picture-path")
buttonIcon: "photo"
buttonTooltip: "Browse for avatar image"
onInputEditingFinished: Settings.data.general.avatarImage = text
onButtonClicked: {
filePicker.open()
}
}
}
NFilePicker {
id: filePicker
pickerType: "file"
title: I18n.tr("settings.general.profile.select-avatar")
initialPath: Settings.data.general.avatarImage.substr(0, Settings.data.general.avatarImage.lastIndexOf("/")) || Quickshell.env("HOME")
nameFilters: ["Image files (*.jpg *.jpeg *.png *.gif *.pnm *.bmp *.face)", "All files (*)"]
onAccepted: paths => Settings.data.general.avatarImage = paths[0]
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
@@ -54,13 +67,13 @@ ColumnLayout {
Layout.fillWidth: true
NHeader {
label: "User interface"
description: "Customize the look, feel, and behavior of the interface."
label: I18n.tr("settings.general.ui.section.label")
description: I18n.tr("settings.general.ui.section.description")
}
NToggle {
label: "Dim desktop"
description: "Dim the desktop when panels or menus are open."
label: I18n.tr("settings.general.ui.dim-desktop.label")
description: I18n.tr("settings.general.ui.dim-desktop.description")
checked: Settings.data.general.dimDesktop
onToggled: checked => Settings.data.general.dimDesktop = checked
}
@@ -70,8 +83,8 @@ ColumnLayout {
Layout.fillWidth: true
NLabel {
label: "Border radius"
description: "Controls the corner roundness of windows, buttons, and other elements."
label: I18n.tr("settings.general.ui.border-radius.label")
description: I18n.tr("settings.general.ui.border-radius.description")
}
NValueSlider {
@@ -91,8 +104,8 @@ ColumnLayout {
Layout.fillWidth: true
NLabel {
label: "Animation speed"
description: "Adjust global animation speed."
label: I18n.tr("settings.general.ui.animation-speed.label")
description: I18n.tr("settings.general.ui.animation-speed.description")
}
NValueSlider {
@@ -119,20 +132,20 @@ ColumnLayout {
Layout.fillWidth: true
NHeader {
label: "Screen corners"
description: "Customize screen corner rounding and visual effects."
label: I18n.tr("settings.general.screen-corners.section.label")
description: I18n.tr("settings.general.screen-corners.section.description")
}
NToggle {
label: "Show screen corners"
description: "Display rounded corners on the edge of the screen."
label: I18n.tr("settings.general.screen-corners.show-corners.label")
description: I18n.tr("settings.general.screen-corners.show-corners.description")
checked: Settings.data.general.showScreenCorners
onToggled: checked => Settings.data.general.showScreenCorners = checked
}
NToggle {
label: "Solid black corners"
description: "Use solid black instead of the bar background color."
label: I18n.tr("settings.general.screen-corners.solid-black.label")
description: I18n.tr("settings.general.screen-corners.solid-black.description")
checked: Settings.data.general.forceBlackScreenCorners
onToggled: checked => Settings.data.general.forceBlackScreenCorners = checked
}
@@ -142,8 +155,8 @@ ColumnLayout {
Layout.fillWidth: true
NLabel {
label: "Screen corners radius"
description: "Adjust the rounded corners of the screen."
label: I18n.tr("settings.general.screen-corners.radius.label")
description: I18n.tr("settings.general.screen-corners.radius.description")
}
NValueSlider {
@@ -169,8 +182,8 @@ ColumnLayout {
Layout.fillWidth: true
NHeader {
label: "Fonts"
description: "Choose the fonts used throughout the interface."
label: I18n.tr("settings.general.fonts.section.label")
description: I18n.tr("settings.general.fonts.section.description")
}
// Font configuration section
@@ -179,12 +192,12 @@ ColumnLayout {
Layout.fillWidth: true
NSearchableComboBox {
label: "Default font"
description: "Main font used throughout the interface."
label: I18n.tr("settings.general.fonts.default.label")
description: I18n.tr("settings.general.fonts.default.description")
model: FontService.availableFonts
currentKey: Settings.data.ui.fontDefault
placeholder: "Select default font..."
searchPlaceholder: "Search fonts..."
placeholder: I18n.tr("settings.general.fonts.default.placeholder")
searchPlaceholder: I18n.tr("settings.general.fonts.default.search-placeholder")
popupHeight: 420 * scaling
minimumWidth: 300 * scaling
onSelected: function (key) {
@@ -193,12 +206,12 @@ ColumnLayout {
}
NSearchableComboBox {
label: "Monospaced font"
description: "Monospaced font used for numbers and stats display."
label: I18n.tr("settings.general.fonts.monospace.label")
description: I18n.tr("settings.general.fonts.monospace.description")
model: FontService.monospaceFonts
currentKey: Settings.data.ui.fontFixed
placeholder: "Select monospace font..."
searchPlaceholder: "Search monospace fonts..."
placeholder: I18n.tr("settings.general.fonts.monospace.placeholder")
searchPlaceholder: I18n.tr("settings.general.fonts.monospace.search-placeholder")
popupHeight: 320 * scaling
minimumWidth: 300 * scaling
onSelected: function (key) {
@@ -207,12 +220,12 @@ ColumnLayout {
}
NSearchableComboBox {
label: "Accent font"
description: "Large font used for prominent displays."
label: I18n.tr("settings.general.fonts.accent.label")
description: I18n.tr("settings.general.fonts.accent.description")
model: FontService.displayFonts
currentKey: Settings.data.ui.fontBillboard
placeholder: "Select display font..."
searchPlaceholder: "Search display fonts..."
placeholder: I18n.tr("settings.general.fonts.accent.placeholder")
searchPlaceholder: I18n.tr("settings.general.fonts.accent.search-placeholder")
popupHeight: 320 * scaling
minimumWidth: 300 * scaling
onSelected: function (key) {
@@ -11,14 +11,14 @@ ColumnLayout {
width: root.width
NHeader {
label: "System hooks"
description: "Configure commands to be executed when system events occur."
label: I18n.tr("settings.hooks.system-hooks.section.label")
description: I18n.tr("settings.hooks.system-hooks.section.description")
}
// Enable/Disable Toggle
NToggle {
label: "Enable hooks"
description: "Enable or disable all hook commands."
label: I18n.tr("settings.hooks.system-hooks.enable.label")
description: I18n.tr("settings.hooks.system-hooks.enable.description")
checked: Settings.data.hooks.enabled
onToggled: checked => Settings.data.hooks.enabled = checked
}
@@ -35,9 +35,9 @@ ColumnLayout {
// Wallpaper Hook Section
NInputAction {
id: wallpaperHookInput
label: "Wallpaper Change Hook"
description: "Command to be executed when wallpaper changes."
placeholderText: "e.g., notify-send \"Wallpaper\" \"Changed\""
label: I18n.tr("settings.hooks.wallpaper-changed.label")
description: I18n.tr("settings.hooks.wallpaper-changed.description")
placeholderText: I18n.tr("settings.hooks.wallpaper-changed.placeholder")
text: Settings.data.hooks.wallpaperChange
onEditingFinished: {
Settings.data.hooks.wallpaperChange = wallpaperHookInput.text
@@ -57,9 +57,9 @@ ColumnLayout {
// Dark Mode Hook Section
NInputAction {
id: darkModeHookInput
label: "Theme Toggle Hook"
description: "Command to be executed when theme toggles between dark and light mode."
placeholderText: "e.g., notify-send \"Theme\" \"Toggled\""
label: I18n.tr("settings.hooks.theme-changed.label")
description: I18n.tr("settings.hooks.theme-changed.description")
placeholderText: I18n.tr("settings.hooks.theme-changed.placeholder")
text: Settings.data.hooks.darkModeChange
onEditingFinished: {
Settings.data.hooks.darkModeChange = darkModeHookInput.text
@@ -82,13 +82,13 @@ ColumnLayout {
Layout.fillWidth: true
NLabel {
label: "Hook Command Information"
description: "• Commands are executed via shell (sh -c)\n• Commands run in background (detached)\n• Test buttons execute with current values"
label: I18n.tr("settings.hooks.info.command-info.label")
description: I18n.tr("settings.hooks.info.command-info.description")
}
NLabel {
label: "Available Parameters"
description: "• Wallpaper Hook: $1 = wallpaper path, $2 = screen name\n• Theme Toggle Hook: $1 = true/false (dark mode state)"
label: I18n.tr("settings.hooks.info.parameters.label")
description: I18n.tr("settings.hooks.info.parameters.description")
}
}
}
@@ -10,45 +10,37 @@ ColumnLayout {
spacing: Style.marginL * scaling
NHeader {
label: "Appearance"
description: "Customize the launcher's behavior and appearance."
label: I18n.tr("settings.launcher.settings.section.label")
description: I18n.tr("settings.launcher.settings.section.description")
}
NComboBox {
id: launcherPosition
label: "Position"
description: "Choose where the Launcher panel appears."
label: I18n.tr("settings.launcher.settings.position.label")
description: I18n.tr("settings.launcher.settings.position.description")
Layout.fillWidth: true
model: ListModel {
ListElement {
key: "center"
name: "Center (default)"
}
ListElement {
key: "top_left"
name: "Top left"
}
ListElement {
key: "top_right"
name: "Top right"
}
ListElement {
key: "bottom_left"
name: "Bottom left"
}
ListElement {
key: "bottom_right"
name: "Bottom right"
}
ListElement {
key: "bottom_center"
name: "Bottom center"
}
ListElement {
key: "top_center"
name: "Top center"
}
}
model: [{
"key": "center",
"name": I18n.tr("options.launcher.position.center")
}, {
"key": "top_left",
"name": I18n.tr("options.launcher.position.top_left")
}, {
"key": "top_right",
"name": I18n.tr("options.launcher.position.top_right")
}, {
"key": "bottom_left",
"name": I18n.tr("options.launcher.position.bottom_left")
}, {
"key": "bottom_right",
"name": I18n.tr("options.launcher.position.bottom_right")
}, {
"key": "bottom_center",
"name": I18n.tr("options.launcher.position.bottom_center")
}, {
"key": "top_center",
"name": I18n.tr("options.launcher.position.top_center")
}]
currentKey: Settings.data.appLauncher.position
onSelected: function (key) {
Settings.data.appLauncher.position = key
@@ -60,14 +52,14 @@ ColumnLayout {
Layout.fillWidth: true
NText {
text: "Background opacity"
text: I18n.tr("settings.launcher.settings.background-opacity.label")
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
NText {
text: "Adjust the background opacity of the launcher."
text: I18n.tr("settings.launcher.settings.background-opacity.description")
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
@@ -87,22 +79,22 @@ ColumnLayout {
}
NToggle {
label: "Enable clipboard history"
description: "Access previously copied items from the launcher."
label: I18n.tr("settings.launcher.settings.clipboard-history.label")
description: I18n.tr("settings.launcher.settings.clipboard-history.description")
checked: Settings.data.appLauncher.enableClipboardHistory
onToggled: checked => Settings.data.appLauncher.enableClipboardHistory = checked
}
NToggle {
label: "Sort by most used"
description: "When enabled, frequently launched apps appear first in the list."
label: I18n.tr("settings.launcher.settings.sort-by-usage.label")
description: I18n.tr("settings.launcher.settings.sort-by-usage.description")
checked: Settings.data.appLauncher.sortByMostUsed
onToggled: checked => Settings.data.appLauncher.sortByMostUsed = checked
}
NToggle {
label: "Use App2Unit to launch applications"
description: "Uses an alternative launch method to better manage app processes and prevent issues."
label: I18n.tr("settings.launcher.settings.use-app2unit.label")
description: I18n.tr("settings.launcher.settings.use-app2unit.description")
checked: Settings.data.appLauncher.useApp2Unit
onToggled: checked => Settings.data.appLauncher.useApp2Unit = checked
}
@@ -10,8 +10,8 @@ ColumnLayout {
spacing: Style.marginL * scaling
NHeader {
label: "Your location"
description: "Get accurate weather and night light scheduling by setting your location."
label: I18n.tr("settings.location.location.section.label")
description: I18n.tr("settings.location.location.section.description")
}
// Location section
@@ -20,10 +20,10 @@ ColumnLayout {
spacing: Style.marginL * scaling
NTextInput {
label: "Search for a location"
description: "e.g., Toronto, ON"
label: I18n.tr("settings.location.location.search.label")
description: I18n.tr("settings.location.location.search.description")
text: Settings.data.location.name || Settings.defaultLocation
placeholderText: "Enter the location name"
placeholderText: I18n.tr("settings.location.location.search.placeholder")
onEditingFinished: {
// Verify the location has really changed to avoid extra resets
var newLocation = text.trim()
@@ -42,13 +42,16 @@ ColumnLayout {
NText {
visible: LocationService.coordinatesReady
text: `${LocationService.stableName} (${LocationService.displayCoordinates})`
text: I18n.tr("system.location-display", {
"name": LocationService.stableName,
"coordinates": LocationService.displayCoordinates
})
font.pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignRight
Layout.alignment: Qt.AlignBottom
Layout.bottomMargin: 12 * scaling
Layout.bottomMargin: Style.marginM * scaling
}
}
@@ -64,13 +67,13 @@ ColumnLayout {
Layout.fillWidth: true
NHeader {
label: "Weather"
description: "Choose your preferred temperature unit."
label: I18n.tr("settings.location.weather.section.label")
description: I18n.tr("settings.location.weather.section.description")
}
NToggle {
label: "Display temperature in Fahrenheit (°F)"
description: "Display temperature in Fahrenheit instead of Celsius."
label: I18n.tr("settings.location.weather.fahrenheit.label")
description: I18n.tr("settings.location.weather.fahrenheit.description")
checked: Settings.data.location.useFahrenheit
onToggled: checked => Settings.data.location.useFahrenheit = checked
}
@@ -82,33 +85,26 @@ ColumnLayout {
Layout.bottomMargin: Style.marginXL * scaling
}
// Weather section
// Date & time section
ColumnLayout {
spacing: Style.marginM * scaling
Layout.fillWidth: true
NHeader {
label: "Date & time"
description: "Customize how date and time appear."
label: I18n.tr("settings.location.date-time.section.label")
description: I18n.tr("settings.location.date-time.section.description")
}
NToggle {
label: "Use 12-hour time format"
description: "On for AM/PM format (e.g., 8:00 PM), off for 24-hour format (e.g., 20:00)."
label: I18n.tr("settings.location.date-time.12hour-format.label")
description: I18n.tr("settings.location.date-time.12hour-format.description")
checked: Settings.data.location.use12hourFormat
onToggled: checked => Settings.data.location.use12hourFormat = checked
}
NToggle {
label: "Show month before day"
description: "On for 09/17/2025, off for 17/09/2025."
checked: Settings.data.location.monthBeforeDay
onToggled: checked => Settings.data.location.monthBeforeDay = checked
}
NToggle {
label: "Show week numbers"
description: "Displays the week of the year (e.g., Week 38) in the calendar."
label: I18n.tr("settings.location.date-time.week-numbers.label")
description: I18n.tr("settings.location.date-time.week-numbers.description")
checked: Settings.data.location.showWeekNumberInCalendar
onToggled: checked => Settings.data.location.showWeekNumberInCalendar = checked
}
@@ -12,17 +12,17 @@ ColumnLayout {
spacing: Style.marginL * scaling
NHeader {
label: "Manage Wi-Fi and Bluetooth connections."
description: I18n.tr("settings.network.section.description")
}
NToggle {
label: "Enable Wi-Fi"
label: I18n.tr("settings.network.wifi.label")
checked: Settings.data.network.wifiEnabled
onToggled: checked => NetworkService.setWifiEnabled(checked)
}
NToggle {
label: "Enable Bluetooth"
label: I18n.tr("settings.network.bluetooth.label")
checked: Settings.data.network.bluetoothEnabled
onToggled: checked => BluetoothService.setBluetoothEnabled(checked)
}
+209
View File
@@ -0,0 +1,209 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
ColumnLayout {
id: root
// 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
})
}
// General Notification Settings
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.notifications.settings.section.label")
description: I18n.tr("settings.notifications.settings.section.description")
}
NToggle {
label: I18n.tr("settings.notifications.settings.do-not-disturb.label")
description: I18n.tr("settings.notifications.settings.do-not-disturb.description")
checked: Settings.data.notifications.doNotDisturb
onToggled: checked => Settings.data.notifications.doNotDisturb = checked
}
NComboBox {
label: I18n.tr("settings.notifications.settings.location.label")
description: I18n.tr("settings.notifications.settings.location.description")
model: [{
"key": "top",
"name": I18n.tr("options.launcher.position.top_center")
}, {
"key": "top_left",
"name": I18n.tr("options.launcher.position.top_left")
}, {
"key": "top_right",
"name": I18n.tr("options.launcher.position.top_right")
}, {
"key": "bottom",
"name": I18n.tr("options.launcher.position.bottom_center")
}, {
"key": "bottom_left",
"name": I18n.tr("options.launcher.position.bottom_left")
}, {
"key": "bottom_right",
"name": I18n.tr("options.launcher.position.bottom_right")
}]
currentKey: Settings.data.notifications.location || "top_right"
onSelected: key => Settings.data.notifications.location = key
}
NToggle {
label: I18n.tr("settings.notifications.settings.always-on-top.label")
description: I18n.tr("settings.notifications.settings.always-on-top.description")
checked: Settings.data.notifications.alwaysOnTop
onToggled: checked => Settings.data.notifications.alwaysOnTop = checked
}
NToggle {
label: I18n.tr("settings.notifications.settings.enable-osd.label")
description: I18n.tr("settings.notifications.settings.enable-osd.description")
checked: Settings.data.notifications.enableOSD
onToggled: checked => Settings.data.notifications.enableOSD = checked
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// Duration
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NHeader {
label: I18n.tr("settings.notifications.duration.section.label")
description: I18n.tr("settings.notifications.duration.section.description")
}
// Respect Expire Timeout (eg. --expire-time flag in notify-send)
NToggle {
label: I18n.tr("settings.notifications.duration.respect-expire.label")
description: I18n.tr("settings.notifications.duration.respect-expire.description")
checked: Settings.data.notifications.respectExpireTimeout
onToggled: checked => Settings.data.notifications.respectExpireTimeout = checked
}
// Low Urgency Duration
ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
NLabel {
label: I18n.tr("settings.notifications.duration.low-urgency.label")
description: I18n.tr("settings.notifications.duration.low-urgency.description")
}
NValueSlider {
Layout.fillWidth: true
from: 1
to: 30
stepSize: 1
value: Settings.data.notifications.lowUrgencyDuration
onMoved: value => Settings.data.notifications.lowUrgencyDuration = value
text: Settings.data.notifications.lowUrgencyDuration + "s"
}
}
// Normal Urgency Duration
ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
NLabel {
label: I18n.tr("settings.notifications.duration.normal-urgency.label")
description: I18n.tr("settings.notifications.duration.normal-urgency.description")
}
NValueSlider {
Layout.fillWidth: true
from: 1
to: 30
stepSize: 1
value: Settings.data.notifications.normalUrgencyDuration
onMoved: value => Settings.data.notifications.normalUrgencyDuration = value
text: Settings.data.notifications.normalUrgencyDuration + "s"
}
}
// Critical Urgency Duration
ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
NLabel {
label: I18n.tr("settings.notifications.duration.critical-urgency.label")
description: I18n.tr("settings.notifications.duration.critical-urgency.description")
}
NValueSlider {
Layout.fillWidth: true
from: 1
to: 30
stepSize: 1
value: Settings.data.notifications.criticalUrgencyDuration
onMoved: value => Settings.data.notifications.criticalUrgencyDuration = value
text: Settings.data.notifications.criticalUrgencyDuration + "s"
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// Monitor Configuration
NHeader {
label: I18n.tr("settings.notifications.monitors.section.label")
description: I18n.tr("settings.notifications.monitors.section.description")
}
Repeater {
model: Quickshell.screens || []
delegate: NCheckbox {
Layout.fillWidth: true
label: modelData.name || I18n.tr("system.unknown")
description: I18n.tr("system.monitor-description", {
"model": modelData.model,
"width": modelData.width,
"height": modelData.height
})
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)
}
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
}

Some files were not shown because too many files have changed in this diff Show More