Compare commits

...

517 Commits

Author SHA1 Message Date
Ly-sec
b22069c455 Release v2.15.1 2025-10-03 13:15:26 +02:00
Ly-sec
b391d03967 Background: revert to old version which fixed the RAM issue
Overview: only load if niri event-stream emits overview active
2025-10-03 13:13:40 +02:00
ItsLemmy
adb84a9e24 Shell: replacing LazyLoader by Loader in an attempt to fix crash when hot-reloading after update. 2025-10-02 22:29:00 -04:00
Ly-sec
4b84e48e8e Overview: potential fix for fallback wallpaper showing after logout/login 2025-10-02 17:06:30 +02:00
Lysec
20cbc03b22 Merge pull request #409 from acdcbyl/main
Matugen: Add 'org.gnome.desktop.interface' related post_hooks for GTK3/4
2025-10-02 16:20:11 +02:00
Aiser
aa33747686 Matugen: Add 'org.gnome.desktop.interface' related post_hooks for GTK 3/4 2025-10-02 22:17:17 +08:00
ItsLemmy
49a0c8449f Tooltips: fixed a bunch of tooltips which where not following the screen's scaling 2025-10-01 16:50:54 -04:00
ItsLemmy
88871e3fbe ActiveWindow-MediaMini: added a minimum size 2025-10-01 15:47:01 -04:00
ItsLemmy
b3989a13da MediaMini: better behavior on smaller screen where the placeholder text may not fit in the capsule 2025-10-01 15:41:31 -04:00
ItsLemmy
07a94de5e2 Shell: more robust reload 2025-10-01 10:43:19 -04:00
ItsLemmy
994f0ca812 Revert "i18n: grab full locale"
This reverts commit 1c1cb8e026.
2025-10-01 10:37:31 -04:00
Ly-sec
1c1cb8e026 i18n: grab full locale 2025-10-01 16:17:35 +02:00
Ly-sec
74270e9478 Set version to dev 2025-10-01 15:54:54 +02:00
Ly-sec
8c9396f325 Release v2.15.0 2025-10-01 15:51:51 +02:00
ItsLemmy
afccf048e7 Taskbar: inactive icon bumped from 0.5 to 0.6 opacity 2025-10-01 09:40:33 -04:00
ItsLemmy
f37625719d Clock: removed useMonospacedFont to keep things simple, + translations + cleanup 2025-10-01 09:20:14 -04:00
Lemmy
cad8fd671f Merge pull request #398 from DiscoCevapi/add-clock-font-setting
Add clock font setting for customizable clock displays
2025-10-01 09:13:18 -04:00
DiscoNiri
68e76abfc7 Move clock font settings to widget-specific configuration
- Moved clock font selection from general settings to clock widget settings
- Added custom font toggle and selection in ClockSettings.qml
- Updated BarWidgetRegistry.qml with new clock font metadata
- Removed global clockFont setting from Settings.qml and GeneralTab.qml
- Updated Clock.qml to use widget-specific custom font setting
- Added proper translation keys for new font options
- Maintained backward compatibility with existing font hierarchy
2025-10-01 20:26:13 +10:00
Lemmy
45c8fe7782 Merge pull request #358 from lonerOrz/fix/brightness
Fix brightness sync after external command changes
2025-09-30 22:49:41 -04:00
ItsLemmy
5ebf4b5377 i18n: launcher terminal-command 2025-09-30 22:45:00 -04:00
Lemmy
59fbe92fe4 Merge pull request #377 from lonerOrz/fix/launcher
fix: the launcher cannot run pure command-line (CLI) programs
2025-09-30 22:44:09 -04:00
ItsLemmy
b051e19f68 i18n: updated all translations via autotranslate! 2025-09-30 22:32:37 -04:00
ItsLemmy
6b9370ac85 i18n: added basic auto translation 2025-09-30 22:24:25 -04:00
lonerorz
9702a300ca Merge branch 'main' into fix/launcher 2025-10-01 10:11:12 +08:00
ItsLemmy
b043664617 Taskbar: Improved the look of the focused app. Made unfocused app icons semi transparent. 2025-09-30 21:33:06 -04:00
ItsLemmy
368e80daf2 .gitignore cleanup 2025-09-30 20:29:18 -04:00
ItsLemmy
056217bf43 Wallpaper: fix double wallpaper init. 2025-09-30 20:24:23 -04:00
ItsLemmy
c1abb3a7dc Default settings updated with Dock's: only same output. 2025-09-30 19:50:24 -04:00
ItsLemmy
52d2055699 MediaMini: fix another binding loop. 2025-09-30 18:20:28 -04:00
ItsLemmy
e324a33137 NiriService: added safe guards to avoid issue with wrong window indexes. 2025-09-30 18:16:35 -04:00
ItsLemmy
6f4aa1a1a1 MediaMini: fix binding loop + edge case where no icon would appear. Also set Autohide to false by default for ActiveWindow and MediaMini 2025-09-30 17:56:59 -04:00
Lemmy
f49462f999 Merge pull request #402 from luleyleo/output-filtered-dock
Per-monitor dock
2025-09-30 17:36:14 -04:00
Leopold Luley
4fb1e2de1e i18n: Add German translation for new dock settings. 2025-09-30 23:07:24 +02:00
Leopold Luley
6d05a20556 Dock: Reformat code. 2025-09-30 23:03:09 +02:00
Leopold Luley
ec2fbb53dc Dock: Allow showing the dock on outputs without a bar. 2025-09-30 23:02:13 +02:00
Leopold Luley
fdc61acfe4 Dock: Add option to filter by output. 2025-09-30 23:01:46 +02:00
Ly-sec
32712c7052 MediaMini: replace placeholder icon 2025-09-30 19:23:18 +02:00
Ly-sec
a0f6d14334 MediaMini: add no active player placeholder 2025-09-30 18:37:45 +02:00
Lysec
6ae8d8536e Merge pull request #400 from acdcbyl/main
i18n: Optimize Chinese translation
2025-09-30 15:35:15 +02:00
Aiser
650dcb8811 i18n: Optimize Chinese translation 2025-09-30 21:32:03 +08:00
ItsLemmy
970684e304 Niri: temp warning fix 2025-09-30 08:07:18 -04:00
Lemmy
e786946abf Merge pull request #394 from ixxie/feat/temp-settings
[NixOS] feat/temp settings
2025-09-30 07:55:14 -04:00
Lemmy
da046cade6 Merge pull request #396 from luleyleo/mouse-sorted-taskbar
NiriService: Keep windows sorted when moving them with the mouse
2025-09-30 07:51:37 -04:00
ItsLemmy
43dee793de More pointSize cleanup 2025-09-30 07:44:03 -04:00
Lysec
0a893f9c5f Merge pull request #399 from pugaizai/main
i18n: update zh-CN translations
2025-09-30 13:28:06 +02:00
Ly-sec
23887574cf NIcon: fix fontSize 2025-09-30 13:12:49 +02:00
pugaizai
2008ba85bc update sessionmenu translation 2025-09-30 19:07:49 +08:00
Ly-sec
773318191d NIcon: use textSize for font.pointSize 2025-09-30 13:02:56 +02:00
pugaizai
78cf0bc8a2 i18n: update zh-CN translations 2025-09-30 18:42:59 +08:00
DiscoNiri
8b0e0f6e0e Add clock font setting for customizable clock displays
This commit adds a new 'Clock Font' setting that allows users to customize
the font used specifically for clock displays in the bar and widgets,
independent of the default UI font.

Features:
- New clockFont property in Settings.data.ui (defaults to 'Roboto')
- Updated Bar Clock widget to use the custom font with fallback support
- Added searchable font dropdown in General Settings tab
- Backward compatible - uses default font if clockFont is not set
- Real-time updates - changes apply immediately

The font selection uses FontService.availableFonts and includes proper
fallback logic that respects the existing monospaced font setting.
2025-09-30 18:37:47 +10:00
Lysec
8c6b3a793f Merge pull request #397 from msdevpt/apply-theme
chore: refresh ghostty configuration
2025-09-30 09:37:42 +02:00
M.Silva
4c3eca80a4 chore: refresh ghostty configuration 2025-09-30 08:32:01 +01:00
Leopold Luley
f61f9a5809 NiriService: Keep windows sorted when moving them with the mouse. 2025-09-30 09:01:58 +02:00
ItsLemmy
518e90d910 SystemMonitor: apply fontScale to TextMetrics for smarted calculation 2025-09-29 21:46:10 -04:00
ItsLemmy
d2e5d0664a Font: added reset button for scaling 2025-09-29 21:42:47 -04:00
ItsLemmy
602d79c98e TrayMenu: fix icon size 2025-09-29 21:38:51 -04:00
ItsLemmy
4b13e89a64 Font: added per font family scaling. removed billboard font 2025-09-29 21:31:45 -04:00
ItsLemmy
1e8b122911 NiriService: syntax fix 2025-09-29 21:19:08 -04:00
Ly-sec
1f257ce847 ControlCenter: fix custom image 2025-09-30 01:33:09 +02:00
Matan Bendix Shenhav
df35589328 feat(flake): write settings to a fallback path 2025-09-30 00:11:03 +02:00
Matan Bendix Shenhav
c92478d27d feat(flake): restart systemd service on package update 2025-09-30 00:10:32 +02:00
Lemmy
ffe39e0ec9 Merge pull request #393 from luleyleo/sorted-taskbar
Sort windows in Taskbar by their scrolling position on Niri
2025-09-29 18:08:50 -04:00
ItsLemmy
b12cf345dc Background Wallpaper: attempt to free up memory earlier. 2025-09-29 16:53:59 -04:00
ItsLemmy
fc4418be0c Shader: fix "disc" shader (no disc at 0 progress) 2025-09-29 16:53:33 -04:00
Leopold Luley
82bfa346a7 NiriService: Fix stale focus state when opening a new window. 2025-09-29 22:16:46 +02:00
Leopold Luley
26ee5046f6 NiriService: Sort windows by their scrolling position. 2025-09-29 22:16:25 +02:00
ItsLemmy
51ed6ea2b0 Compositor: fix getFocusedWindow() 2025-09-29 15:10:44 -04:00
ItsLemmy
c53dd6fade Compositor: fix getFocusedWindowTitle. Since active workspace has been implemented.
+ autoformatting
2025-09-29 15:04:13 -04:00
Lemmy
bb24b6904d Merge pull request #386 from luleyleo/filtered-taskbar
Taskbar: Filter by screen and workspace
2025-09-29 15:02:31 -04:00
Ly-sec
d5857e3363 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-29 16:32:00 +02:00
Ly-sec
559609be64 Launcher: add pin to dock button if dock is enabled 2025-09-29 16:31:53 +02:00
ItsLemmy
5cea61114b Scaling: fix scaling not properly applied on startup. 2025-09-29 10:20:19 -04:00
ItsLemmy
22794ea922 DateTime: proper locale usage. Fix #390
Replaced all Qt.formatDateTime() by Qt.locale().toString()
2025-09-29 10:07:58 -04:00
ItsLemmy
933ba54612 Init Sequence: minor reordering 2025-09-29 09:58:48 -04:00
ItsLemmy
0d0b9a21f2 Wallpaper Selector: added a shortcut to the wallpaper settings in the top bar. 2025-09-29 09:25:45 -04:00
ItsLemmy
9ed9231070 Init Sequence: removed a bunch of no longer necessary Settings.isLoaded 2025-09-29 09:11:37 -04:00
Ly-sec
b8b54825d5 SessionMenu: move lockAndSuspend to CompositorService 2025-09-29 14:20:15 +02:00
Ly-sec
250822e819 Revert "Matugen: add custom-colors.toml"
This reverts commit ece9789f6b.
2025-09-29 14:13:22 +02:00
Ly-sec
ece9789f6b Matugen: add custom-colors.toml 2025-09-29 13:43:37 +02:00
Ly-sec
f11d27bcf1 Background: "explicitly set currentWallpaper.source to nothing as an
attempt to fix the odd memory usage after a few hours"
2025-09-29 13:18:45 +02:00
Ly-sec
0e69256279 Background: fix short flash of default wallpaper before actual wallpaper shows 2025-09-29 13:13:21 +02:00
Leopold Luley
fa49d4aaa0 Taskbar: Add German translation for Taskbar settings. 2025-09-29 11:08:48 +02:00
Leopold Luley
b1f7ae5d9a Taskbar: Add settings. 2025-09-29 11:01:14 +02:00
Leopold Luley
e6b0be77e7 Taskbar: Filter by same output and active workspaces. 2025-09-29 11:01:14 +02:00
ItsLemmy
49961882dd Shell: changed init sequence so that i18n + Settings are fully loaded before any UI component spawn. 2025-09-28 23:39:34 -04:00
ItsLemmy
c1d2d82fa2 NSpinBox: fixes
- replaced row by rowlayount
- using proper Color.mOnTertiary for hover text/icon
- fixed binding break when entering value manually
2025-09-28 21:19:10 -04:00
ItsLemmy
c35f37c7d7 Use Color.transparent instead of "transparent" 2025-09-28 21:17:10 -04:00
Lemmy
e23cb90c5b Merge pull request #388 from MrDowntempo/Consistent-Hover
Nicer SpinBox with better mTertiary hover
2025-09-28 20:53:24 -04:00
ItsLemmy
b2688e9100 More conversion of Row/Column to Layout 2025-09-28 20:49:57 -04:00
ItsLemmy
7f3842ddbf Log cleanup (avoid super long string with path) 2025-09-28 20:39:28 -04:00
ItsLemmy
68b2c83be1 DockMenu: use RowLayout and ColumnLayout 2025-09-28 20:35:25 -04:00
Corey Woodworth
97fa2fb1b5 Back to Chevrons. +/- were inconsistent sizes. Better alignment 2025-09-28 20:20:02 -04:00
ItsLemmy
0ed8ed7fe5 Tooltips: fix clipping for tooltips with long sentences. 2025-09-28 19:45:37 -04:00
Corey Woodworth
a41be0b5d9 Removed gradient and redesigned buttons 2025-09-28 19:08:33 -04:00
ItsLemmy
072d80e2f3 Bar vs Dock: Dock are loaded only once the bar is fully loaded. This ensure the vertical bar use the full screen height if the dock is exclusive. 2025-09-28 16:39:23 -04:00
loner
1f898171e0 Merge remote-tracking branch 'upstream/main' into fix/launcher
# Conflicts:
#	Assets/Translations/zh-CN.json
2025-09-29 03:22:48 +08:00
loner
ef64395dd4 Resolve conflict 2025-09-29 03:09:30 +08:00
loner
a5c89fadb5 fix(services): emit brightnessUpdated signal in setBrightness 2025-09-29 02:40:01 +08:00
loner
cccf0e6017 fix: Fix brightness synchronization in multi-monitor setups 2025-09-29 02:34:42 +08:00
Ly-sec
5da474007e i18n: add lock-and-suspend to all languages 2025-09-28 19:53:20 +02:00
Ly-sec
ffd2cdaf74 SessionMenu: add lock & suspend option as requested in #301 2025-09-28 19:50:52 +02:00
MrDowntempo
5f3c088f22 Update NSpinBox.qml
I missed a line
2025-09-28 13:16:07 -04:00
MrDowntempo
382116e795 Merge branch 'main' into Consistent-Hover 2025-09-28 13:10:13 -04:00
Ly-sec
c7c49433f7 NotificationService: add flatpak name support 2025-09-28 19:08:04 +02:00
Corey Woodworth
0d2d0f1931 Nicer SpinBox with better mTertiary hover 2025-09-28 12:49:52 -04:00
Ly-sec
2e947edc5a Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-28 18:42:59 +02:00
Ly-sec
cdc32f3eac NSpinBox: add text input support 2025-09-28 18:42:53 +02:00
ItsLemmy
21736b3095 DockMenu: auto hides when not hovering the menu, simplified with a single mouse area. 2025-09-28 12:06:41 -04:00
ItsLemmy
48852a9ca4 Tray: close the menu on re-hovering the tooltip 2025-09-28 11:37:12 -04:00
ItsLemmy
65fab7b367 Tray: Fixing hiding tooltip 2025-09-28 11:17:02 -04:00
ItsLemmy
dc414df9bc NRadioButton: proper elipsis. Fix #385 2025-09-28 11:09:17 -04:00
ItsLemmy
69a6c052db LockScreen: adapted custom tooltips to the new lighter look. 2025-09-28 10:55:48 -04:00
ItsLemmy
c422435d3d Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-28 10:52:09 -04:00
ItsLemmy
fc1742e167 Tooltips: proper tooltip service 2025-09-28 10:51:56 -04:00
ItsLemmy
061e7f32da Tooltips: proper tooltip service 2025-09-28 10:40:15 -04:00
Lemmy
8dda007847 Merge pull request #371 from pugaizai/main
allow zh-CN like language code
2025-09-28 09:53:16 -04:00
pugaizai
1cdff28cca Merge from upstream 2025-09-28 21:43:50 +08:00
铺盖崽
f32a34e320 Rename zh.json to zh-CN.json 2025-09-28 21:34:02 +08:00
铺盖崽
0d0088bd52 allow zh-CN like language code 2025-09-28 21:34:02 +08:00
ItsLemmy
a7a7a96585 Merge branch 'tooltips' 2025-09-28 09:23:42 -04:00
ItsLemmy
026d602770 Tooltips: more robust tooltips after hot-reload 2025-09-28 09:23:28 -04:00
Ly-sec
5b54be633d Aya: rename to ayu (probably a typo) 2025-09-28 13:07:51 +02:00
Lysec
3bb10e9561 Merge pull request #383 from acdcbyl/main
i18n: Optimize Chinese translation
2025-09-28 11:44:04 +02:00
Aiser
b9b233a873 i18n: Optimize Chinese translation 2025-09-28 17:38:43 +08:00
Ly-sec
388824bf37 i18n: add description to all Bar widget settings 2025-09-28 11:16:26 +02:00
Ly-sec
25eb31747a ColorSchemeTab: hide predefined colorschemes when matugen is enabled 2025-09-28 10:43:02 +02:00
Lysec
f7109b0bf9 Merge pull request #382 from acdcbyl/main
i18n: Optimize Chinese translation
2025-09-28 10:08:48 +02:00
Aiser
c41fa1aef7 i18n: Optimize Chinese translation 2025-09-28 16:03:59 +08:00
Aiser
1a0ea3893c i18n: Optimize Chinese translation 2025-09-28 15:52:54 +08:00
ItsLemmy
0593543d7a Tooltip: Refactoring in a single global tooltip. 2025-09-28 00:15:43 -04:00
ItsLemmy
fbf80ab577 v2.14.4-dev 2025-09-27 20:40:48 -04:00
ItsLemmy
7e9f7f40ef v2.14.4 2025-09-27 20:40:15 -04:00
ItsLemmy
92460fc5c3 IPC call to enable/disable/toggle wallpaper random automation. Fix #378 2025-09-27 18:22:57 -04:00
ItsLemmy
c1c91edb6c NButton: no bar position is allowed in Widgets/
- Only exception is NPanel.
2025-09-27 17:51:52 -04:00
ItsLemmy
e73d85de04 Bluetooth: Removed the copy of the adapter's state in Settings, makes code much simpler and robust by always relying on the actual adapter's state. 2025-09-27 17:33:09 -04:00
ItsLemmy
fafd7a518b Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-27 16:12:29 -04:00
ItsLemmy
8b89e95b13 New setting to disable all UI animations 2025-09-27 16:12:28 -04:00
ItsLemmy
2112f675c0 Taskbar: fix warning due to non existing property. 2025-09-27 16:12:11 -04:00
Lemmy
d873c2205b Merge pull request #380 from ixxie/feat/flake-defaults
feat(flake): deep merge settings with defaults
2025-09-27 16:00:42 -04:00
ItsLemmy
348c1e8f9f General: Animation speed max back to 200% 2025-09-27 15:01:40 -04:00
ItsLemmy
8e248f6795 Tooltip: removed auto-positionning relative to the bar. as many tooltips are used in panels
- still a few edge cases to work on
2025-09-27 14:57:11 -04:00
ItsLemmy
4c516200dc SystemMonitor: syntax error 2025-09-27 14:19:26 -04:00
ItsLemmy
b5b8b62cf0 Animation speed: allow 500% speed for quasi instant. 2025-09-27 14:03:54 -04:00
ItsLemmy
a4b4caa2ce Bar SysMonitor: Implemented different sizing strategy to avoid unwanted shifting of items inside and outside the component. 2025-09-27 13:38:56 -04:00
Lemmy
423ea60939 Merge pull request #372 from MrDowntempo/Centered-Circles
Centered circles
2025-09-27 13:24:17 -04:00
MrDowntempo
55dd48ce66 Merge branch 'noctalia-dev:main' into Centered-Circles 2025-09-27 12:37:38 -04:00
Corey Woodworth
7dc8d2cd88 Fix: Works regardless of scaling value 2025-09-27 12:34:58 -04:00
Ly-sec
d4dd3b1734 ColorSchemeTab: hide matugen scheme type when Matugen is disabled 2025-09-27 17:20:47 +02:00
Ly-sec
0f30a10a14 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-27 17:16:47 +02:00
Ly-sec
50e2a95f52 Update settings-default 2025-09-27 17:16:37 +02:00
Ly-sec
35bf30ef5e ColorSchemeTab: add matugen type option 2025-09-27 17:16:00 +02:00
ItsLemmy
afce091473 Bluetooth: simplify the way we handle adapter state vs settings value. 2025-09-27 11:03:52 -04:00
Matan Bendix Shenhav
d802b6a2fa feat(flake): deep merge settings with defaults 2025-09-27 16:28:44 +02:00
ItsLemmy
65cd95c62b Notifications: properly handle large/many action buttons. Fix #379 2025-09-27 09:17:23 -04:00
Ly-sec
fe2654268d NightLight: check if wlsunset exists, else dont enable NightLight
SystemMonitorSettings: If RAM usage is not toggled, don't show % option
Settings: remove NightLight from default bar widgets
2025-09-27 15:14:44 +02:00
ItsLemmy
13e32dc11b Notifications test with a lot of actions 2025-09-27 08:58:48 -04:00
loner
b27728e5bf i18n(zh): add translation for terminal command 2025-09-27 12:12:31 +08:00
loner
2379ad134b i18n(pt): add translation for terminal command 2025-09-27 12:12:21 +08:00
loner
3ab9ffed78 i18n(fr): add translation for terminal command 2025-09-27 12:12:11 +08:00
loner
3182d1969b i18n(es): add translation for terminal command 2025-09-27 12:11:53 +08:00
loner
591d099255 i18n(de): add translation for terminal command 2025-09-27 12:11:43 +08:00
loner
256f9b4a76 feat(launcher): add configurable terminal command
The terminal command for launching applications was previously hardcoded to 'kitty', causing issues for users without it installed.

This change introduces a new setting, 'appLauncher.terminalCommand', allowing users to specify their preferred terminal emulator. The default value is set to 'xterm -e'.

The implementation includes:
- Defining the setting in 'Commons/Settings.qml'.
- Adding a text input in the launcher settings tab.
- Updating the application plugin to use the new setting.
2025-09-27 12:06:54 +08:00
ItsLemmy
dd29a739f3 v2.14.3-dev 2025-09-26 23:48:03 -04:00
ItsLemmy
83d82a825b v2.14.3 2025-09-26 23:46:25 -04:00
ItsLemmy
e2f7012c5b NScrollView: properly disable horizontal scrrol when setting proper horizontalPolicy 2025-09-26 23:35:05 -04:00
loner
ff1509939a test kitty 2025-09-27 11:29:57 +08:00
ItsLemmy
f8ee0bb8df FilePicker: debugging and improvements. 2025-09-26 23:21:56 -04:00
ItsLemmy
96d3051151 Update service 2025-09-26 23:18:35 -04:00
Lysec
e8e96a9f68 Merge pull request #376 from kevindiaz314/main
fix(ci): not in a git directory
2025-09-27 02:02:44 +02:00
Kevin Diaz
b7c99905f3 fix(ci): not in a git directory 2025-09-26 20:00:56 -04:00
Lysec
ab89b0e964 Merge pull request #375 from kevindiaz314/main
CI: add GitHub Actions workflow to automate AUR package updates on release
2025-09-27 01:19:48 +02:00
Kevin Diaz
7b9ecd048d CI: add GitHub Actions workflow to automate AUR package updates on release 2025-09-26 19:18:16 -04:00
Corey Woodworth
9d30eac13a Fix: Correct same issue with Radio Buttons too. 2025-09-26 16:01:25 -04:00
Corey Woodworth
4785e287ba Fix: Small fix. 4* instead of 2*2* 2025-09-26 15:37:02 -04:00
Corey Woodworth
aa1cea8d03 Fix: Fix the vertical alignment of circles 2025-09-26 15:30:16 -04:00
Lemmy
823ab9c6a3 Merge pull request #370 from MrDowntempo/Just-The-Tip
Rounds the ends of NSliders to be more consistent with the look
2025-09-26 14:58:19 -04:00
Corey Woodworth
74a0c9dbf4 Fix: Knob was getting clipped. 2025-09-26 14:22:22 -04:00
Corey Woodworth
d1a89387f9 Fix: Make sure left side doesn't get squished 2025-09-26 13:24:19 -04:00
Corey Woodworth
9da310ade4 Rounds the ends of NSliders to be more consistent with the rest of Noctalia's look 2025-09-26 11:01:38 -04:00
Lemmy
348604e45a Merge pull request #368 from MrDowntempo/Old-Theme
Restored the vintage Noctalia theme as Noctalia (legacy)
2025-09-26 10:24:28 -04:00
Ly-sec
5e44af8e6d Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-26 16:23:08 +02:00
Ly-sec
27eaeee5fd i18n-zh/pt: add missing keys 2025-09-26 16:23:04 +02:00
Corey Woodworth
338f4cde6d Restored the vintage Noctalia theme as Noctalia (legacy) 2025-09-26 10:20:01 -04:00
ItsLemmy
1531275707 Wallpaper: smarter init 2025-09-26 10:09:17 -04:00
Ly-sec
5cfa66f9e8 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-26 15:07:30 +02:00
Ly-sec
695d002d6a OsdTab: move all OSD related settings into their own tab
OSD: add Left/Right Center options (will display vertically)
TablerIcons: add OSD Tab icon
i18n: added translation to all files for OSDTab (generated)
2025-09-26 15:05:53 +02:00
ItsLemmy
7afd0177cb Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-26 08:33:32 -04:00
ItsLemmy
180366073f Toast: less intrusive toast logging 2025-09-26 08:33:30 -04:00
Lysec
7eb19237ba Merge pull request #366 from pugaizai/main
i18n(zh): add zh(simplified chinese) translation
2025-09-26 14:21:01 +02:00
铺盖崽
ed7b4f5552 i18n(zh): add zh(simplified chinese) translation 2025-09-26 20:19:43 +08:00
Lysec
9d927bd7fc Merge pull request #364 from lonerOrz/opt/osd
Increase OSD initTimer interval to 500ms
2025-09-26 09:18:36 +02:00
loner
ac683caa1e Increase OSD initTimer interval to 500ms 2025-09-26 13:50:14 +08:00
ItsLemmy
39883ceb10 WallpaperService: proper i18n support of the list models. 2025-09-25 23:35:18 -04:00
ItsLemmy
c1386c491e v2.14.1-dev 2025-09-25 21:58:33 -04:00
ItsLemmy
e7f8a452b8 v2.14.1 2025-09-25 21:57:39 -04:00
ItsLemmy
012ae28dd9 Bar editor: removing the last ControlCenter triggers a toast warning. 2025-09-25 21:54:51 -04:00
ItsLemmy
95d059007e ClipboardService: fix invalid toast invocation 2025-09-25 21:54:09 -04:00
ItsLemmy
b76a252b94 Screencorners: if bar is not visible have them in actual cornes (similar to floating bar) Fix #362 2025-09-25 21:31:49 -04:00
ItsLemmy
6bd4167638 FilePicker: better icons positioning 2025-09-25 21:13:13 -04:00
ItsLemmy
22b843587c FilePicker: back to our custom file picker. 2025-09-25 20:59:50 -04:00
ItsLemmy
cb3fc1a45c Bar: Right clicking the bar will open the ControlCenter 2025-09-25 17:18:07 -04:00
ItsLemmy
b1df7624cc Settings: bullet proofing the widget upgrade code. 2025-09-25 17:09:00 -04:00
Lemmy
8be64359ef Merge pull request #359 from juvevood/osd-toast-location
The locations of OSD and Toast follow the notifications location
2025-09-25 13:44:42 -04:00
ItsLemmy
8e6badc0d6 Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-25 13:22:06 -04:00
ItsLemmy
4ac27be0e8 NPanel: don't dim if panel is masked 2025-09-25 13:21:57 -04:00
Ly-sec
2a496a7831 UpdaterService: set dev version 2025-09-25 17:41:34 +02:00
Ly-sec
619420349c i18n: add keep awake to all languages 2025-09-25 17:34:57 +02:00
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
Juve
4a9f37a390 The locations of osd and toast follow the notifications location 2025-09-25 14:03:24 +08:00
ItsLemmy
36489491e4 Bar new IPC: ipc call bar toggle 2025-09-24 22:18:22 -04:00
loner
2c7038c504 Fix brightness sync after external command changes
Fix brightness sync after external command changes, improve brightness
module compatibility
2025-09-25 10:18:09 +08: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
LemmyCook
2d290bf5f7 Release v2.12.0 2025-09-18 22:06:05 -04:00
LemmyCook
891c8660e3 Properly hide ScreenRecorderIndicator when inactive (no spacing) 2025-09-18 22:05:55 -04:00
LemmyCook
a734235cd0 Autoformating 2025-09-18 22:05:33 -04:00
Lemmy
8fdc6a0f72 Merge pull request #314 from kevindiaz314/main
fix(clock): respect monthBeforeDay setting in vertical clock date dis…
2025-09-18 21:38:31 -04:00
LemmyCook
603f499355 Settings: removed systemic capitalization improved labels and descriptions. 2025-09-18 21:34:30 -04:00
Kevin Diaz
2b8b97ab3b fix(clock): respect monthBeforeDay setting in vertical clock date display 2025-09-18 20:30:22 -04:00
LemmyCook
458ef3c0d5 TrayMenu: not using 'Screen' as we have a proper 'screen' 2025-09-18 18:28:01 -04:00
LemmyCook
c4008e3899 CustomButtonSettings: Don't use Screen with a capital 'S' unless really necessary. 2025-09-18 18:25:15 -04:00
LemmyCook
6c3299ad10 Merge branch 'wallpaper-selector' 2025-09-18 18:22:32 -04:00
LemmyCook
6fe498ce19 Wallpaper Selector: auto-focus search field 2025-09-18 17:47:26 -04:00
LemmyCook
4e67f26576 Wallpaper Selector: fix for multi screens / multi directories setup 2025-09-18 17:35:25 -04:00
LemmyCook
b2d46ab759 Settings: cleanup since we moved the wallpaper selector out. 2025-09-18 17:34:55 -04:00
Lemmy
0d3cc917fa Merge pull request #302 from randibudi/main
NixOS: Add Night Light Dependency and Enable Required Services
2025-09-18 15:51:48 -04:00
Lemmy
ac591da6c5 Update README.md 2025-09-18 15:51:21 -04:00
Lemmy
c7709b5f21 Update README.md 2025-09-18 15:50:19 -04:00
Lemmy
e6370904cd Update README.md 2025-09-18 15:47:37 -04:00
Randi Budi
e412cee52f Merge branch 'main' into main 2025-09-19 01:32:07 +07:00
Ly-sec
c3019230ae WallpaperSelector: even more layout changes 2025-09-18 20:04:03 +02:00
Ly-sec
c7ab350cbd MatugenService: add check for Settings.isLoaded 2025-09-18 19:53:06 +02:00
Ly-sec
b65d82d895 WallpaperSelector: more layout changes 2025-09-18 19:51:45 +02:00
Ly-sec
89eb5ecde6 IPCManager: add wallpaper selector toggle 2025-09-18 19:31:04 +02:00
Ly-sec
b374f167ef WallpaperSelectorPanel: rename to WallpaperSelector 2025-09-18 19:26:35 +02:00
Ly-sec
28026a4c37 NPanel: add bar detection while dragging
WallpaperSelectorPanel: adjust layout
2025-09-18 19:24:00 +02:00
Ly-sec
b8bce3d421 NPanel: add border while dragging 2025-09-18 18:34:48 +02:00
Ly-sec
6fba3457f7 NPanel: add drag support 2025-09-18 18:27:35 +02:00
Ly-sec
07a6a16011 WallpaperSelector: cleanup 2025-09-18 18:11:37 +02:00
Ly-sec
6b61599633 WallpaperSelector: change sizing 2025-09-18 18:06:18 +02:00
Ly-sec
1bd093db7f WallpaperSelector overhaul: initial commit 2025-09-18 17:55:30 +02:00
Ly-sec
3d9295856c Launcher: add sort by most used option 2025-09-18 16:53:38 +02:00
LemmyCook
a1aabd02f5 Toast: reworked the display and logic to make it more robust.
+ some bluetooth logic debouncing to avoid extra toast when adapter
comes back to life after suspend.
2025-09-18 10:10:40 -04:00
Ly-sec
ae2d3eddd6 README: revert Credits & Acknowledgment sections 2025-09-18 11:12:48 +02:00
Ly-sec
b75c358f54 README: full overhaul, linking to docs 2025-09-18 11:10:29 +02:00
Lysec
0972a55aad Merge pull request #312 from nalakawula/lockScreen/adjust-password-prompt
Make password prompt look like a terminal/tty
2025-09-18 11:02:15 +02:00
sumarsono
112f71b633 Make password prompt look like a terminal/tty 2025-09-18 15:52:45 +07:00
LemmyCook
e67d7166de Merge branch 'bar-service' 2025-09-17 22:50:56 -04:00
LemmyCook
6e88118ca9 Calendar: add conditional week number column. New option is in the Location tab of the settings. 2025-09-17 22:32:44 -04:00
LemmyCook
75b7f0fcb0 Bluetooth device: fixed missing busy icon on the call to action. 2025-09-17 21:58:44 -04:00
LemmyCook
47f72d9498 Location/Clock: Moved use12hourformat and reverseDaymonth from the clock widget settings to the main settings, location tab
- Fix #303
2025-09-17 21:10:51 -04:00
LemmyCook
85d7dc2506 Settings/Notification: typo fix 2025-09-17 15:40:10 -04:00
LemmyCook
1305efec24 Settings/Notification: fixed typo 2025-09-17 15:38:25 -04:00
LemmyCook
8af8bf2e2e BarService: to keep tracks of bar widgets and improve IPC behavior. 2025-09-17 10:19:55 -04:00
Lemmy
abd6a66297 Merge pull request #295 from knuesel/colorscheme-kanagawa
Kanagawa colorscheme
2025-09-17 09:34:31 -04:00
LemmyCook
2e9a812513 PowerProfile: Standardization + Factorisation. Fix #307 2025-09-17 09:30:23 -04:00
Jeremie Knuesel
8d845e7cd0 Kanagawa colorscheme 2025-09-17 14:56:13 +02:00
Ly-sec
a1dcef8dec Revert "Brightness: holding down keybind with brightness IPC now keeps changing brightness until release"
This reverts commit 38e0bb8e64.
2025-09-17 12:51:02 +02:00
Ly-sec
38e0bb8e64 Brightness: holding down keybind with brightness IPC now keeps changing brightness until release 2025-09-17 12:50:19 +02:00
Ly-sec
8811cb3d13 Notification: display links as plain text 2025-09-17 12:40:52 +02:00
ItsLemmy
a872682eb8 Brightness: fix #300 2025-09-17 00:28:57 -04:00
LemmyCook
46b8317330 v2.11.0-dev 2025-09-16 23:30:04 -04:00
LemmyCook
8204460112 v2.11.0 2025-09-16 23:29:02 -04:00
LemmyCook
292337dc00 Settings: Put monitor configs below other settings on Bar and Notif. tabs 2025-09-16 23:26:35 -04:00
LemmyCook
0b790c219d Dimming: replaced dimmer by panel dimming, now that we have no margins it works fine. 2025-09-16 23:23:16 -04:00
LemmyCook
7acca17b83 2.10.0-dev 2025-09-16 23:10:12 -04:00
Randi Budi
cdfb110007 fix(nixos): power profile and battery monitoring with module 2025-09-17 04:20:37 +07:00
Randi Budi
b7d8f92414 fix(nixos): add wlsunset dependency for night light 2025-09-17 00:59:13 +07:00
201 changed files with 22530 additions and 6597 deletions

110
.github/workflows/update-aur-package.yml vendored Normal file
View File

@@ -0,0 +1,110 @@
name: Update AUR Package
on:
push:
tags:
- 'v*'
workflow_dispatch:
jobs:
aur-sync:
name: Sync PKGBUILD with release
runs-on: ubuntu-latest
container:
image: archlinux:latest
defaults:
run:
shell: bash
env:
AUR_REPO: ssh://aur@aur.archlinux.org/noctalia-shell.git
GIT_SSH_COMMAND: ssh -i /root/.ssh/id_aur -o StrictHostKeyChecking=yes -o IdentitiesOnly=yes
PKGNAME: noctalia-shell
AUR_LINK: https://aur.archlinux.org/packages/noctalia-shell
steps:
- name: Install dependencies
run: |
set -euo pipefail
pacman -Syu --noconfirm git base-devel pacman-contrib openssh
- name: Create build user
run: |
set -euo pipefail
useradd -m builduser
echo 'builduser ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers
- name: Configure SSH
env:
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
run: |
set -euo pipefail
mkdir -p /root/.ssh
chmod 700 /root/.ssh
printf '%s\n' "$AUR_SSH_PRIVATE_KEY" > /root/.ssh/id_aur
chmod 600 /root/.ssh/id_aur
ssh-keyscan aur.archlinux.org >> /root/.ssh/known_hosts
chmod 600 /root/.ssh/known_hosts
- name: Determine version
id: vars
env:
TAG_NAME: ${{ github.ref_name }}
run: |
set -euo pipefail
PKGVER="${TAG_NAME#v}"
echo "pkgver=$PKGVER" >> "$GITHUB_OUTPUT"
- name: Clone AUR repository
run: |
set -euo pipefail
git clone "$AUR_REPO" "$GITHUB_WORKSPACE/aur"
- name: Update PKGBUILD
env:
PKGVER: ${{ steps.vars.outputs.pkgver }}
working-directory: ${{ github.workspace }}/aur
run: |
set -euo pipefail
sed -i "s/^pkgver=.*/pkgver=${PKGVER}/" PKGBUILD
sed -i "s/^pkgrel=.*/pkgrel=1/" PKGBUILD
- name: Refresh checksums and metadata
env:
AUR_DIR: ${{ github.workspace }}/aur
run: |
set -euo pipefail
chown -R builduser:builduser "$AUR_DIR"
su - builduser -c "cd $AUR_DIR && updpkgsums"
su - builduser -c "cd $AUR_DIR && makepkg --printsrcinfo > .SRCINFO"
- name: Commit and push changes
env:
PKGVER: ${{ steps.vars.outputs.pkgver }}
working-directory: ${{ github.workspace }}/aur
run: |
set -euo pipefail
git config --global --add safe.directory "$PWD"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
if [[ -n "$(git status --porcelain)" ]]; then
git add PKGBUILD .SRCINFO
git commit -m "chore(package): release ${PKGVER}"
git push origin HEAD
else
echo "No updates necessary."
fi
- name: Summarize update
env:
PKGNAME: noctalia-shell
PKGVER: ${{ steps.vars.outputs.pkgver }}
AUR_LINK: https://aur.archlinux.org/packages/noctalia-shell
run: |
set -euo pipefail
{
echo "## AUR Update"
echo ""
echo "- Package: ${PKGNAME}"
echo "- Updated version: ${PKGVER}"
echo "- AUR page: ${AUR_LINK}"
} >> "$GITHUB_STEP_SUMMARY"

1
.gitignore vendored
View File

@@ -1 +0,0 @@
.qmlls.ini

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"
}
}

View File

@@ -0,0 +1,34 @@
{
"dark": {
"mPrimary": "#76946a",
"mOnPrimary": "#1f1f28",
"mSecondary": "#c0a36e",
"mOnSecondary": "#1f1f28",
"mTertiary": "#7e9cd8",
"mOnTertiary": "#1f1f28",
"mError": "#c34043",
"mOnError": "#1f1f28",
"mSurface": "#1f1f28",
"mOnSurface": "#717c7c",
"mSurfaceVariant": "#2a2a37",
"mOnSurfaceVariant": "#c8c093",
"mOutline": "#363646",
"mShadow": "#1f1f28"
},
"light": {
"mPrimary": "#6f894e",
"mOnPrimary": "#f2ecbc",
"mSecondary": "#77713f",
"mOnSecondary": "#f2ecbc",
"mTertiary": "#4d699b",
"mOnTertiary": "#f2ecbc",
"mError": "#c84053",
"mOnError": "#f2ecbc",
"mSurface": "#f2ecbc",
"mOnSurface": "#8a8980",
"mSurfaceVariant": "#e5ddb0",
"mOnSurfaceVariant": "#545464",
"mOutline": "#cfc49c",
"mShadow": "#f2ecbc"
}
}

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"
}
}

View File

@@ -0,0 +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"
},
"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"
}
}

Binary file not shown.

View File

@@ -15,7 +15,7 @@ Singleton {
function buildConfigToml() {
var lines = []
lines.push("[config]")
var mode = Settings.data.colorSchemes.darkMode ? "dark" : "light"
// Always include noctalia colors output for the shell
lines.push("[templates.noctalia]")
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/noctalia.json"')
@@ -25,11 +25,13 @@ Singleton {
lines.push("\n[templates.gtk4]")
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/gtk4.css"')
lines.push('output_path = "~/.config/gtk-4.0/gtk.css"')
lines.push("post_hook = 'gsettings set org.gnome.desktop.interface color-scheme prefer-" + mode + "'")
}
if (Settings.data.matugen.gtk3) {
lines.push("\n[templates.gtk3]")
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/gtk3.css"')
lines.push('output_path = "~/.config/gtk-3.0/gtk.css"')
lines.push("post_hook = 'gsettings set org.gnome.desktop.interface color-scheme prefer-" + mode + "'")
}
if (Settings.data.matugen.qt6) {
lines.push("\n[templates.qt6]")
@@ -51,7 +53,7 @@ Singleton {
lines.push("\n[templates.ghostty]")
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/ghostty.conf"')
lines.push('output_path = "~/.config/ghostty/themes/noctalia"')
lines.push("post_hook = \"grep -q '^theme *= *' ~/.config/ghostty/config; and sed -i 's/^theme *= *.*/theme = noctalia/' ~/.config/ghostty/config; or echo 'theme = noctalia' >> ~/.config/ghostty/config\"")
lines.push("post_hook = \"grep -q '^theme *= *' ~/.config/ghostty/config; and sed -i 's/^theme *= *.*/theme = noctalia/' ~/.config/ghostty/config; or echo 'theme = noctalia' >> ~/.config/ghostty/config; and pkill -SIGUSR2 ghostty\"")
}
if (Settings.data.matugen.foot) {
lines.push("\n[templates.foot]")

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}}

1433
Assets/Translations/de.json Normal file

File diff suppressed because it is too large Load Diff

1433
Assets/Translations/en.json Normal file

File diff suppressed because it is too large Load Diff

1429
Assets/Translations/es.json Normal file

File diff suppressed because it is too large Load Diff

1429
Assets/Translations/fr.json Normal file

File diff suppressed because it is too large Load Diff

1429
Assets/Translations/pt.json Normal file

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

View File

@@ -0,0 +1,194 @@
{
"settingsVersion": 12,
"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": "Clock"
},
{
"id": "ControlCenter"
}
]
}
},
"general": {
"avatarImage": "",
"dimDesktop": true,
"showScreenCorners": false,
"forceBlackScreenCorners": false,
"radiusRatio": 1,
"screenRadiusRatio": 1,
"animationSpeed": 1,
"animationDisabled": false
},
"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,
"terminalCommand": "xterm -e"
},
"dock": {
"autoHide": false,
"exclusive": false,
"backgroundOpacity": 1,
"floatingRatio": 1,
"onlySameOutput": true,
"monitors": [],
"pinnedApps": []
},
"network": {
"wifiEnabled": true
},
"notifications": {
"doNotDisturb": false,
"monitors": [],
"location": "top_right",
"alwaysOnTop": false,
"lastSeenTs": 0,
"respectExpireTimeout": false,
"lowUrgencyDuration": 3,
"normalUrgencyDuration": 8,
"criticalUrgencyDuration": 15
},
"osd": {
"enabled": true,
"location": "top_right",
"monitors": [],
"autoHideMs": 2000
},
"audio": {
"volumeStep": 5,
"volumeOverdrive": false,
"cavaFrameRate": 60,
"visualizerType": "linear",
"mprisBlacklist": [],
"preferredPlayer": ""
},
"ui": {
"fontDefault": "Roboto",
"fontFixed": "DejaVu Sans Mono",
"fontDefaultScale": 1,
"fontFixedScale": 1,
"monitorsScaling": [],
"idleInhibitorEnabled": false
},
"brightness": {
"brightnessStep": 5
},
"colorSchemes": {
"useWallpaperColors": false,
"predefinedScheme": "Noctalia (default)",
"darkMode": true,
"matugenSchemeType": "scheme-fruit-salad"
},
"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": ""
}
}

636
Bin/i18n-json-check.sh Executable file
View File

@@ -0,0 +1,636 @@
#!/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"
TRANSLATE_MODE=false
# 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
if $TRANSLATE_MODE && ! command -v curl &> /dev/null; then
print_color $RED "Error: 'curl' is required for translation mode but not installed." >&2
exit 1
fi
}
# Function to get Gemini API key
get_gemini_api_key() {
if [[ -z "${GEMINI_API_KEY:-}" ]]; then
print_color $RED "Error: GEMINI_API_KEY environment variable is not set" >&2
print_color $YELLOW "Please set it with: export GEMINI_API_KEY='your-api-key'" >&2
exit 1
fi
echo "$GEMINI_API_KEY"
}
# Function to get value from JSON using key path
get_json_value() {
local json_file=$1
local key_path=$2
# Convert dot-separated path to jq path
local jq_path=$(echo "$key_path" | sed 's/\./\.\["/g' | sed 's/$/"]/' | sed 's/^\.//')
local jq_query=".${jq_path}"
# Use a more robust approach: split by dots and build path
local -a path_parts
IFS='.' read -ra path_parts <<< "$key_path"
local jq_filter="."
for part in "${path_parts[@]}"; do
jq_filter="${jq_filter}[\"${part}\"]"
done
jq -r "$jq_filter // empty" "$json_file" 2>/dev/null || echo ""
}
# Function to list available Gemini models
list_gemini_models() {
local api_key=$(get_gemini_api_key)
print_color $BLUE "Fetching available Gemini models..." >&2
echo "" >&2
local response=$(curl -s -X GET \
"https://generativelanguage.googleapis.com/v1/models?key=${api_key}" \
-H "Content-Type: application/json" 2>/dev/null)
# Parse and display models
echo "$response" | jq -r '.models[] | "- \(.name) (\(.displayName))"' 2>/dev/null || {
print_color $RED "Failed to parse models list" >&2
echo "$response" >&2
exit 1
}
exit 0
}
# Function to translate text using Gemini API
translate_text() {
local text=$1
local target_language=$2
local api_key=$3
# Escape text for JSON
local escaped_text=$(echo "$text" | jq -Rs .)
# Prepare the API request
local prompt="Translate the following English text to ${target_language}. Return ONLY the translation, no explanations or additional text:\n\n${text}"
local escaped_prompt=$(echo "$prompt" | jq -Rs .)
local request_body=$(cat <<EOF
{
"contents": [{
"parts": [{
"text": ${escaped_prompt}
}]
}],
"generationConfig": {
"temperature": 0.3,
"maxOutputTokens": 1000
}
}
EOF
)
# Make API call to Gemini
local api_url="https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${api_key}"
print_color $BLUE " API URL: $api_url" >&2
local response=$(curl -s -X POST "$api_url" \
-H "Content-Type: application/json" \
-d "$request_body" 2>/dev/null)
print_color $BLUE " API Response: $response" >&2
# Extract the translation from response - try multiple parsing approaches
local translation=$(echo "$response" | jq -r '.candidates[0].content.parts[0].text // .text // empty' 2>/dev/null | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
if [[ -z "$translation" ]]; then
print_color $RED " Failed to parse translation. Full response:" >&2
echo "$response" | jq . >&2 2>/dev/null || echo "$response" >&2
echo ""
return 1
fi
print_color $GREEN " Parsed translation: $translation" >&2
echo "$translation"
}
# Function to inject translation into JSON file using jq
inject_translation() {
local json_file=$1
local key_path=$2
local value=$3
# Split key path into array
local -a path_parts
IFS='.' read -ra path_parts <<< "$key_path"
# Build jq path array
local jq_path="["
for i in "${!path_parts[@]}"; do
if [[ $i -gt 0 ]]; then
jq_path+=","
fi
jq_path+="\"${path_parts[$i]}\""
done
jq_path+="]"
# Create a temporary file
local temp_file=$(mktemp)
# Use jq to set the value at the path
jq --argjson path "$jq_path" --arg value "$value" 'setpath($path; $value)' "$json_file" > "$temp_file"
if [[ $? -eq 0 ]]; then
mv "$temp_file" "$json_file"
return 0
else
rm -f "$temp_file"
return 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")"
if $TRANSLATE_MODE; then
echo "Mode: TRANSLATION ENABLED"
fi
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 ""
# Translate missing keys if in translate mode
if $TRANSLATE_MODE; then
print_color $BLUE "Translating missing keys for $lang_name..." >&2
local api_key=$(get_gemini_api_key)
local translated_count=0
local failed_count=0
while IFS= read -r key; do
if [[ -n "$key" ]]; then
# Get English value
local en_value=$(get_json_value "$ref_file_path" "$key")
if [[ -n "$en_value" ]]; then
print_color $YELLOW " Translating: $key" >&2
# Translate the value
local translated_value=$(translate_text "$en_value" "$lang_name" "$api_key")
if [[ -n "$translated_value" ]]; then
# Inject translation into the file
if inject_translation "$lang_file" "$key" "$translated_value"; then
print_color $GREEN " ✓ Translated: $key" >&2
translated_count=$((translated_count + 1))
else
print_color $RED " ✗ Failed to inject: $key" >&2
failed_count=$((failed_count + 1))
fi
else
print_color $RED " ✗ Translation failed: $key" >&2
failed_count=$((failed_count + 1))
fi
# Small delay to avoid rate limiting
sleep 0.5
fi
fi
done <<< "$missing_keys"
echo ""
print_color $GREEN "Translation complete: $translated_count succeeded, $failed_count failed" >&2
echo ""
fi
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
if $TRANSLATE_MODE; then
echo "Translation mode: ENABLED"
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 [--translate] [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 " --translate Enable automatic translation of missing keys using Gemini API" >&2
echo " --list-models List all available Gemini models and exit" >&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 --list-models # List available Gemini models" >&2
echo " $0 --translate # Compare all and translate missing keys" >&2
echo " $0 --translate fr # Translate missing keys for French only" >&2
echo "" >&2
echo "Requirements:" >&2
echo " - jq must be installed" >&2
echo " - curl must be installed (for --translate mode)" >&2
echo " - $REFERENCE_FILE must exist in $FOLDER_PATH" >&2
echo " - Target language file must exist if specified" >&2
echo " - GEMINI_API_KEY environment variable must be set (for --translate mode)" >&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
target_lang=""
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
show_usage
exit 0
;;
--list-models)
list_gemini_models
;;
--translate)
TRANSLATE_MODE=true
shift
;;
*)
if [[ -n "$target_lang" ]]; then
echo "Error: Too many arguments. Only one language code is allowed." >&2
echo "" >&2
show_usage
exit 1
fi
# 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
target_lang="$1"
shift
;;
esac
done
# Run main function
main "$target_lang"

31
Bin/i18n-qml-check.sh Executable file
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
Bin/notifications-test.sh Executable file
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', 'maybe', 'Maybe', 'undecided', 'Undecided']" \
"{}" \
5000

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

365
Commons/I18n.qml Normal file
View File

@@ -0,0 +1,365 @@
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
Logger.log("I18n", `Loaded translations for "${root.langCode}"`)
root.isLoaded = true
root.translationsLoaded()
} 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 fullUserLang = Qt.locale().uiLanguages[i]
// Try full code match (such as zh CN, en US)
if (availableLanguages.includes(fullUserLang)) {
Logger.log("I18n", `Exact match found: "${fullUserLang}"`)
setLanguage(fullUserLang)
return
}
// If full code match fails, try short code matching (such as zh, en)
const shortUserLang = fullUserLang.substring(0, 2)
if (availableLanguages.includes(shortUserLang)) {
Logger.log("I18n", `Short code match found: "${shortUserLang}" from "${fullUserLang}"`)
setLanguage(shortUserLang)
return
}
Logger.log("I18n", `No match for system language: "${fullUserLang}"`)
}
// 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: ${langCode}`)
// 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)
}
}

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")
// 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()
}
}

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,24 +22,351 @@ 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
// -----------------------------------------------------
// -----------------------------------------------------
// 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", 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
// This avoid excessive IO when a variable changes rapidly (ex: sliders)
Timer {
id: saveTimer
running: false
interval: 1000
onTriggered: {
settingsFileView.writeAdapter()
// Write to fallback location if set
if (Quickshell.env("NOCTALIA_SETTINGS_FALLBACK")) {
settingsFallbackFileView.writeAdapter()
}
}
}
FileView {
id: settingsFileView
path: directoriesCreated ? settingsFile : undefined
printErrors: false
watchChanges: true
onFileChanged: reload()
onAdapterUpdated: saveTimer.start()
// Trigger initial load when path changes from empty to actual path
onPathChanged: {
if (path !== undefined) {
reload()
}
}
onLoaded: function () {
if (!isLoaded) {
Logger.log("Settings", "Settings loaded")
upgradeSettingsData()
validateMonitorConfigurations()
isLoaded = true
// Emit the signal
root.settingsLoaded()
}
}
onLoadFailed: function (error) {
if (error.toString().includes("No such file") || error === 2) {
// File doesn't exist, create it with default values
writeAdapter()
// Also write to fallback if set
if (Quickshell.env("NOCTALIA_SETTINGS_FALLBACK")) {
settingsFallbackFileView.writeAdapter()
}
}
}
}
// Fallback FileView for writing settings to alternate location
FileView {
id: settingsFallbackFileView
path: Quickshell.env("NOCTALIA_SETTINGS_FALLBACK") || ""
adapter: Quickshell.env("NOCTALIA_SETTINGS_FALLBACK") ? adapter : null
printErrors: false
watchChanges: false
}
JsonAdapter {
id: adapter
property int settingsVersion: 12
// 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
// 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": "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
property bool animationDisabled: false
}
// 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
property string terminalCommand: "xterm -e"
}
// dock
property JsonObject dock: JsonObject {
property bool autoHide: false
property bool exclusive: false
property real backgroundOpacity: 1.0
property real floatingRatio: 1.0
property bool onlySameOutput: true
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
}
// 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
}
// on-screen display
property JsonObject osd: JsonObject {
property bool enabled: true
property string location: "top_right"
property list<string> monitors: []
property int autoHideMs: 2000
}
// 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 real fontDefaultScale: 1.0
property real fontFixedScale: 1.0
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
property string matugenSchemeType: "scheme-fruit-salad"
}
// 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() {
@@ -71,25 +404,42 @@ Singleton {
// If the settings structure has changed, ensure
// backward compatibility by upgrading the settings
function upgradeSettingsData() {
// Wait for BarWidgetRegistry to be ready
if (!BarWidgetRegistry.widgets || Object.keys(BarWidgetRegistry.widgets).length === 0) {
Logger.warn("Settings", "BarWidgetRegistry not ready, deferring upgrade")
Qt.callLater(upgradeSettingsData)
return
}
const sections = ["left", "center", "right"]
// -----------------
// 1st. check our settings are not super old, when we only had the widget type as a plain string
// 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]
if (typeof widget === "string") {
adapter.bar.widgets[sectionName][i] = {
"id": widget
}
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
var removedWidget = false
for (var s = 0; s < sections.length; s++) {
const sectionName = sections[s]
const widgets = adapter.bar.widgets[sectionName]
@@ -97,14 +447,15 @@ Singleton {
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}`)
widgets.splice(i, 1)
removedWidget = true
}
}
}
// -----------------
// 3nd. migrate global settings to user settings
// 3nd. upgrade widget settings
for (var s = 0; s < sections.length; s++) {
const sectionName = sections[s]
for (var i = 0; i < adapter.bar.widgets[sectionName].length; i++) {
@@ -122,10 +473,29 @@ Singleton {
}
}
// 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++
// -----------------
// 4th. safety check
// if a widget was deleted, ensure we still have a control center
if (removedWidget) {
var gotControlCenter = false
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 (widget.id === "ControlCenter") {
gotControlCenter = true
break
}
}
}
if (!gotControlCenter) {
//const obj = JSON.parse('{"id": "ControlCenter"}');
adapter.bar.widgets["right"].push(({
"id": "ControlCenter"
}))
Logger.warn("Settings", "Added a ControlCenter widget to the right section")
}
}
}
@@ -134,42 +504,20 @@ Singleton {
// Backup the widget definition before altering
const widgetBefore = JSON.stringify(widget)
// Migrate old bar settings to proper per widget settings
switch (widget.id) {
case "ActiveWindow":
widget.showIcon = widget.showIcon !== undefined ? widget.showIcon : adapter.bar.showActiveWindowIcon
break
case "Battery":
widget.alwaysShowPercentage = widget.alwaysShowPercentage !== undefined ? widget.alwaysShowPercentage : adapter.bar.alwaysShowBatteryPercentage
break
case "Clock":
widget.use12HourClock = widget.use12HourClock !== undefined ? widget.use12HourClock : adapter.location.use12HourClock
widget.reverseDayMonth = widget.reverseDayMonth !== undefined ? widget.reverseDayMonth : adapter.location.reverseDayMonth
if (widget.showDate !== undefined) {
widget.displayFormat = "time-date"
} else if (widget.showSeconds) {
widget.displayFormat = "time-seconds"
// 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]
}
delete widget.showDate
delete widget.showSeconds
break
case "MediaMini":
widget.showAlbumArt = widget.showAlbumArt !== undefined ? widget.showAlbumArt : adapter.audio.showMiniplayerAlbumArt
widget.showVisualizer = widget.showVisualizer !== undefined ? widget.showVisualizer : adapter.audio.showMiniplayerCava
break
case "SidePanelToggle":
widget.useDistroLogo = widget.useDistroLogo !== undefined ? widget.useDistroLogo : adapter.bar.useDistroLogo
break
case "SystemMonitor":
widget.showNetworkStats = widget.showNetworkStats !== undefined ? widget.showNetworkStats : adapter.bar.showNetworkStats
break
case "Workspace":
widget.labelMode = widget.labelMode !== undefined ? widget.labelMode : adapter.bar.showWorkspaceLabel
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") {
@@ -185,300 +533,4 @@ Singleton {
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()
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])
// Mark directories as created and trigger file loading
directoriesCreated = true
}
// Don't write settings to disk immediately
// This avoid excessive IO when a variable changes rapidly (ex: sliders)
Timer {
id: saveTimer
running: false
interval: 1000
onTriggered: settingsFileView.writeAdapter()
}
FileView {
id: settingsFileView
path: directoriesCreated ? settingsFile : undefined
printErrors: false
watchChanges: true
onFileChanged: reload()
onAdapterUpdated: saveTimer.start()
// Trigger initial load when path changes from empty to actual path
onPathChanged: {
if (path !== undefined) {
reload()
}
}
onLoaded: function () {
if (!isLoaded) {
Logger.log("Settings", "----------------------------")
Logger.log("Settings", "Settings loaded successfully")
upgradeSettingsData()
validateMonitorConfigurations()
kickOffServices()
isLoaded = true
// Emit the signal
root.settingsLoaded()
}
}
onLoadFailed: function (error) {
if (error.toString().includes("No such file") || error === 2)
// File doesn't exist, create it with default values
writeAdapter()
}
JsonAdapter {
id: adapter
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
// Floating bar settings
property bool floating: false
property real marginVertical: 0.25
property real marginHorizontal: 0.25
property bool showActiveWindowIcon: true // TODO: delete
property bool alwaysShowBatteryPercentage: false // TODO: delete
property bool showNetworkStats: false // TODO: delete
property bool useDistroLogo: false // TODO: delete
property string showWorkspaceLabel: "none" // TODO: delete
// 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
// Animation speed multiplier (0.1x - 2.0x)
property real animationSpeed: 1.0
}
// location
property JsonObject location: JsonObject {
property string name: defaultLocation
property bool useFahrenheit: false
property bool reverseDayMonth: false // TODO: delete
property bool use12HourClock: false // TODO: delete
property bool showDateWithClock: false // TODO: delete
}
// 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 {
// When disabled, Launcher hides clipboard command and ignores cliphist
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
}
// 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 epoch)
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: ""
property bool showMiniplayerAlbumArt: false // TODO: delete
property bool showMiniplayerCava: false // TODO: delete
}
// 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: ""
}
}
}
}

View File

@@ -60,10 +60,10 @@ Singleton {
property real opacityFull: 1.0
// Animation duration (ms)
property int animationFast: Math.round(150 / Settings.data.general.animationSpeed)
property int animationNormal: Math.round(300 / Settings.data.general.animationSpeed)
property int animationSlow: Math.round(450 / Settings.data.general.animationSpeed)
property int animationSlowest: Math.round(750 / Settings.data.general.animationSpeed)
property int animationFast: Settings.data.general.animationDisabled ? 0 : Math.round(150 / Settings.data.general.animationSpeed)
property int animationNormal: Settings.data.general.animationDisabled ? 0 : Math.round(300 / Settings.data.general.animationSpeed)
property int animationSlow: Settings.data.general.animationDisabled ? 0 : Math.round(450 / Settings.data.general.animationSpeed)
property int animationSlowest: Settings.data.general.animationDisabled ? 0 : Math.round(750 / Settings.data.general.animationSpeed)
// Delays
property int tooltipDelay: 300

View File

@@ -26,6 +26,7 @@ Singleton {
"search": "search",
"warning": "exclamation-circle",
"stop": "player-stop-filled",
"busy": "hourglass-empty",
"media-pause": "player-pause-filled",
"media-play": "player-play-filled",
"media-prev": "player-skip-back-filled",
@@ -44,6 +45,7 @@ Singleton {
"keyboard": "keyboard",
"shutdown": "power",
"lock": "lock",
"lock-pause": "lock-pause",
"logout": "logout",
"reboot": "refresh",
"suspend": "player-pause",
@@ -55,6 +57,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",
@@ -66,6 +71,8 @@ Singleton {
"chevron-down": "chevron-down",
"caret-up": "caret-up-filled",
"caret-down": "caret-down-filled",
"star": "star",
"star-off": "star-off",
"battery-exclamation": "battery-exclamation",
"battery-charging": "battery-charging",
"battery-4": "battery-4",
@@ -101,13 +108,14 @@ Singleton {
"settings-display": "device-desktop",
"settings-network": "sitemap",
"settings-brightness": "brightness-up",
"settings-weather": "cloud-sun",
"settings-location": "world-pin",
"settings-color-scheme": "palette",
"settings-wallpaper": "paint",
"settings-wallpaper-selector": "library-photo",
"settings-screen-recorder": "video",
"settings-hooks": "link",
"settings-notification": "bell",
"settings-notifications": "bell",
"settings-osd": "picture-in-picture",
"settings-about": "info-square-rounded",
"bluetooth": "bluetooth",
"bt-device-generic": "bluetooth",
@@ -118,7 +126,33 @@ Singleton {
"bt-device-watch": "device-watch",
"bt-device-speaker": "device-speaker",
"bt-device-tv": "device-tv",
"noctalia": "noctalia"
"noctalia": "noctalia",
"hyprland": "hyprland",
"filepicker-folder": "folder",
"filepicker-refresh": "refresh",
"filepicker-close": "x",
"filepicker-arrow-left": "arrow-left",
"filepicker-arrow-up": "arrow-up",
"filepicker-home": "home",
"filepicker-layout-grid": "layout-grid",
"filepicker-list": "list",
"filepicker-search": "search",
"filepicker-x": "x",
"filepicker-photo": "photo",
"filepicker-check": "check",
"filepicker-file-text": "file-text",
"filepicker-video": "video",
"filepicker-music": "music",
"filepicker-archive": "archive",
"filepicker-table": "table",
"filepicker-presentation": "presentation",
"filepicker-code": "code",
"filepicker-settings": "settings",
"filepicker-file": "file",
"filepicker-text": "file-text",
"filepicker-eye": "eye",
"filepicker-eye-off": "eye-off",
"filepicker-folder-current": "checks"
}
// Fonts Codepoints - do not change!
@@ -3470,6 +3504,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}",
@@ -4302,6 +4337,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}",

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(reverseDayMonth = 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}, ` + (reverseDayMonth ? `${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
Helpers/QtObj2JS.js Normal file
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;
}

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 {
@@ -45,14 +44,27 @@ Variants {
property real fillMode: 1.0
property vector4d fillColor: Qt.vector4d(Settings.data.wallpaper.fillColor.r, Settings.data.wallpaper.fillColor.g, Settings.data.wallpaper.fillColor.b, 1.0)
// On startup assign wallpaper immediately
Component.onCompleted: {
fillMode = WallpaperService.getFillModeUniform()
// On startup, defer assigning wallpaper until the service cache is ready
function _startWallpaperOnceReady() {
if (!modelData) {
Qt.callLater(_startWallpaperOnceReady)
return
}
var path = modelData ? WallpaperService.getWallpaper(modelData.name) : ""
var cacheReady = WallpaperService && WallpaperService.currentWallpapers && Object.keys(WallpaperService.currentWallpapers).length > 0
if (!cacheReady) {
// Try again on the next tick until WallpaperService.init() populates cache
Qt.callLater(_startWallpaperOnceReady)
return
}
fillMode = WallpaperService.getFillModeUniform()
var path = WallpaperService.getWallpaper(modelData.name)
setWallpaperImmediate(path)
}
Component.onCompleted: _startWallpaperOnceReady()
Connections {
target: Settings.data.wallpaper
function onFillModeChanged() {
@@ -231,6 +243,9 @@ Variants {
easing.type: Easing.InOutCubic
onFinished: {
// Swap images after transition completes
if (currentWallpaper.source !== "") {
currentWallpaper.source = ""
}
currentWallpaper.source = nextWallpaper.source
nextWallpaper.source = ""
transitionProgress = 0.0
@@ -243,6 +258,9 @@ Variants {
function setWallpaperImmediate(source) {
transitionAnimation.stop()
transitionProgress = 0.0
if (currentWallpaper.source !== "") {
currentWallpaper.source = ""
}
currentWallpaper.source = source
nextWallpaper.source = ""
}

View File

@@ -1,76 +0,0 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Widgets
Variants {
model: Quickshell.screens
delegate: Loader {
required property ShellScreen modelData
// Dimmer is only active on the screen where the panel is currently open.
active: {
if (Settings.isLoaded && Settings.data.general.dimDesktop && modelData !== undefined && PanelService.openedPanel !== null && PanelService.openedPanel.item !== undefined && PanelService.openedPanel.item !== null) {
return (PanelService.openedPanel.item.screen === modelData)
}
return false
}
sourceComponent: PanelWindow {
id: panel
property real customOpacity: 0
Component.onCompleted: {
if (modelData) {
Logger.log("Dimmer", "Loaded on", modelData.name)
}
// When a NPanel opens it seems it is initialized with the primary screen for a very brief moment
// before the screen actually updates to the proper value. We use a timer to delay the fade in to avoid
// a single frame flicker on the main screen when opening a panel on another screen.
fadeInTimer.start()
}
Connections {
target: PanelService
function onWillClose() {
customOpacity = Style.opacityNone
}
}
Timer {
id: fadeInTimer
interval: 100
onTriggered: customOpacity = Style.opacityHeavy
}
screen: modelData
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
WlrLayershell.namespace: "quickshell-dimmer"
// mask: Region {}
anchors {
top: true
bottom: true
right: true
left: true
}
color: Qt.alpha(Color.mShadow, customOpacity)
Behavior on color {
ColorAnimation {
duration: Style.animationSlow
}
}
}
}
}

View File

@@ -12,7 +12,7 @@ Variants {
delegate: Loader {
required property ShellScreen modelData
active: Settings.isLoaded && CompositorService.isNiri && modelData && Settings.data.wallpaper.enabled
active: CompositorService.isNiri && CompositorService.niriOverviewActive && modelData && Settings.data.wallpaper.enabled
property string wallpaper: ""
@@ -21,6 +21,10 @@ Variants {
if (modelData) {
Logger.log("Overview", "Loading Overview component for Niri on", modelData.name)
}
updateWallpaper()
}
function updateWallpaper() {
wallpaper = modelData ? WallpaperService.getWallpaper(modelData.name) : ""
}
@@ -34,6 +38,15 @@ Variants {
}
}
Connections {
target: WallpaperService
function onIsInitializedChanged() {
if (WallpaperService.isInitialized) {
updateWallpaper()
}
}
}
color: Color.transparent
screen: modelData
WlrLayershell.layer: WlrLayer.Background

View File

@@ -48,10 +48,10 @@ Loader {
margins {
// When bar is floating, corners should be at screen edges (no margins)
// When bar is not floating, respect bar margins as before
top: !Settings.data.bar.floating && ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "top" && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
bottom: !Settings.data.bar.floating && ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "bottom" && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
left: !Settings.data.bar.floating && ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "left" && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
right: !Settings.data.bar.floating && ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "right" && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
top: !Settings.data.bar.floating && BarService.isVisible && ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "top" && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
bottom: !Settings.data.bar.floating && BarService.isVisible && ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "bottom" && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
left: !Settings.data.bar.floating && BarService.isVisible && ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "left" && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
right: !Settings.data.bar.floating && BarService.isVisible && ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "right" && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
}
mask: Region {}

View File

@@ -8,6 +8,7 @@ import qs.Commons
import qs.Services
import qs.Widgets
import qs.Modules.Notification
import qs.Modules.Bar.Extras
Variants {
model: Quickshell.screens
@@ -27,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: BarService.isVisible && modelData && modelData.name ? (Settings.data.bar.monitors.includes(modelData.name) || (Settings.data.bar.monitors.length === 0)) : false
sourceComponent: PanelWindow {
screen: modelData || null
@@ -46,11 +47,18 @@ 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
}
Component.onCompleted: {
if (modelData && modelData.name) {
BarService.registerBar(modelData.name)
}
}
Item {
@@ -68,6 +76,17 @@ Variants {
radius: Settings.data.bar.floating ? Style.radiusL : 0
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: function (mouse) {
if (mouse.button === Qt.RightButton) {
controlCenterPanel.toggle(BarService.lookupWidget("ControlCenter"))
mouse.accepted = true
}
}
}
Loader {
anchors.fill: parent
sourceComponent: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? verticalBarComponent : horizontalBarComponent
@@ -88,7 +107,7 @@ Variants {
Repeater {
model: Settings.data.bar.widgets.left
delegate: NWidgetLoader {
delegate: BarWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
@@ -111,7 +130,7 @@ Variants {
Repeater {
model: Settings.data.bar.widgets.center
delegate: NWidgetLoader {
delegate: BarWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
@@ -135,7 +154,7 @@ Variants {
Repeater {
model: Settings.data.bar.widgets.right
delegate: NWidgetLoader {
delegate: BarWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
@@ -169,7 +188,7 @@ Variants {
Repeater {
model: Settings.data.bar.widgets.left
delegate: NWidgetLoader {
delegate: BarWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
@@ -194,7 +213,7 @@ Variants {
Repeater {
model: Settings.data.bar.widgets.center
delegate: NWidgetLoader {
delegate: BarWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
@@ -220,7 +239,7 @@ Variants {
Repeater {
model: Settings.data.bar.widgets.right
delegate: NWidgetLoader {
delegate: BarWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,

View File

@@ -22,7 +22,7 @@ ColumnLayout {
NText {
text: root.label
font.pointSize: Style.fontSizeL * scaling
pointSize: Style.fontSizeL * scaling
color: Color.mSecondary
font.weight: Style.fontWeightMedium
Layout.fillWidth: true
@@ -67,7 +67,7 @@ ColumnLayout {
// One device BT icon
NIcon {
icon: BluetoothService.getDeviceIcon(modelData)
font.pointSize: Style.fontSizeXXL * scaling
pointSize: Style.fontSizeXXL * scaling
color: getContentColor(Color.mOnSurface)
Layout.alignment: Qt.AlignVCenter
}
@@ -79,7 +79,7 @@ ColumnLayout {
// Device name
NText {
text: modelData.name || modelData.deviceName
font.pointSize: Style.fontSizeM * scaling
pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightMedium
elide: Text.ElideRight
color: getContentColor(Color.mOnSurface)
@@ -90,7 +90,7 @@ ColumnLayout {
NText {
text: BluetoothService.getStatusString(modelData)
visible: text !== ""
font.pointSize: Style.fontSizeXS * scaling
pointSize: Style.fontSizeXS * scaling
color: getContentColor(Color.mOnSurfaceVariant)
}
@@ -103,21 +103,21 @@ ColumnLayout {
// Device signal strength - "Unknown" when not connected
NText {
text: BluetoothService.getSignalStrength(modelData)
font.pointSize: Style.fontSizeXS * scaling
pointSize: Style.fontSizeXS * scaling
color: getContentColor(Color.mOnSurfaceVariant)
}
NIcon {
visible: modelData.signalStrength > 0 && !modelData.pairing && !modelData.blocked
text: BluetoothService.getSignalIcon(modelData)
font.pointSize: Style.fontSizeXS * scaling
pointSize: Style.fontSizeXS * scaling
color: getContentColor(Color.mOnSurface)
}
NText {
visible: modelData.signalStrength > 0 && !modelData.pairing && !modelData.blocked
text: (modelData.signalStrength !== undefined && modelData.signalStrength > 0) ? modelData.signalStrength + "%" : ""
font.pointSize: Style.fontSizeXS * scaling
pointSize: Style.fontSizeXS * scaling
color: getContentColor(Color.mOnSurface)
}
}
@@ -126,7 +126,7 @@ ColumnLayout {
NText {
visible: modelData.batteryAvailable
text: BluetoothService.getBattery(modelData)
font.pointSize: Style.fontSizeXS * scaling
pointSize: Style.fontSizeXS * scaling
color: getContentColor(Color.mOnSurfaceVariant)
}
}
@@ -163,7 +163,7 @@ ColumnLayout {
}
return "Connect"
}
icon: (isBusy ? "hourglass-split" : null)
icon: (isBusy ? "busy" : null)
onClicked: {
if (modelData.connected) {
BluetoothService.disconnectDevice(modelData)
@@ -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)
// }
// }
// }
}
}
}

View File

@@ -30,13 +30,13 @@ NPanel {
NIcon {
icon: "bluetooth"
font.pointSize: Style.fontSizeXXL * scaling
pointSize: Style.fontSizeXXL * scaling
color: Color.mPrimary
}
NText {
text: "Bluetooth"
font.pointSize: Style.fontSizeL * scaling
text: I18n.tr("bluetooth.panel.title")
pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
@@ -44,15 +44,15 @@ NPanel {
NToggle {
id: bluetoothSwitch
checked: Settings.data.network.bluetoothEnabled
checked: BluetoothService.enabled
onToggled: checked => BluetoothService.setBluetoothEnabled(checked)
baseSize: Style.baseWidgetSize * 0.65 * scaling
}
NIconButton {
enabled: Settings.data.network.bluetoothEnabled
enabled: BluetoothService.enabled
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()
@@ -88,21 +88,21 @@ NPanel {
NIcon {
icon: "bluetooth-off"
font.pointSize: 64 * scaling
pointSize: 64 * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Bluetooth is disabled"
font.pointSize: Style.fontSizeL * scaling
text: I18n.tr("bluetooth.panel.disabled")
pointSize: Style.fontSizeL * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Enable Bluetooth to see available devices."
font.pointSize: Style.fontSizeS * scaling
text: I18n.tr("bluetooth.panel.enable-message")
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 []
@@ -186,7 +186,7 @@ NPanel {
NIcon {
icon: "refresh"
font.pointSize: Style.fontSizeXXL * 1.5 * scaling
pointSize: Style.fontSizeXXL * 1.5 * scaling
color: Color.mPrimary
RotationAnimation on rotation {
@@ -199,15 +199,15 @@ NPanel {
}
NText {
text: "Scanning for devices..."
font.pointSize: Style.fontSizeL * scaling
text: I18n.tr("bluetooth.panel.scanning")
pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
}
}
NText {
text: "Make sure your device is in pairing mode."
font.pointSize: Style.fontSizeM * scaling
text: I18n.tr("bluetooth.panel.pairing-mode")
pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}

View File

@@ -0,0 +1,270 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Widgets
NPanel {
id: root
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
Layout.leftMargin: Style.marginM * scaling
Layout.rightMargin: Style.marginM * scaling
spacing: Style.marginS * scaling
NIconButton {
icon: "chevron-left"
tooltipText: I18n.tr("tooltips.previous-month")
onClicked: {
let newDate = new Date(grid.year, grid.month - 1, 1)
grid.year = newDate.getFullYear()
grid.month = newDate.getMonth()
}
}
NText {
text: grid.title
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
}
NIconButton {
icon: "chevron-right"
tooltipText: I18n.tr("tooltips.next-month")
onClicked: {
let newDate = new Date(grid.year, grid.month + 1, 1)
grid.year = newDate.getFullYear()
grid.month = newDate.getMonth()
}
}
}
// Divider between header and weekdays
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginS * scaling
Layout.bottomMargin: Style.marginL * scaling
}
// Columns label (respects locale's first day of week)
RowLayout {
Layout.fillWidth: true
Layout.leftMargin: Style.marginS * scaling // Align with grid
Layout.rightMargin: Style.marginS * scaling
Layout.bottomMargin: Style.marginM * scaling
spacing: 0
// Week header spacer or label (same width as week number column)
Item {
visible: Settings.data.location.showWeekNumberInCalendar
Layout.preferredWidth: visible ? Style.baseWidgetSize * scaling : 0
NText {
anchors.centerIn: parent
text: I18n.tr("calendar.panel.week")
color: Color.mOutline
pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightRegular
horizontalAlignment: Text.AlignHCenter
}
}
// Day name headers - now properly aligned with calendar grid
GridLayout {
Layout.fillWidth: true
Layout.fillHeight: true
columns: 7
rows: 1
columnSpacing: 0
rowSpacing: 0
Repeater {
model: 7
Item {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredWidth: Style.baseWidgetSize * scaling
NText {
anchors.centerIn: parent
text: {
let dayIndex = (content.firstDayOfWeek + index) % 7
return Qt.locale().dayName(dayIndex, Locale.ShortFormat)
}
color: Color.mSecondary
pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
horizontalAlignment: Text.AlignHCenter
}
}
}
}
}
// Grids: days with optional week numbers
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.leftMargin: Style.marginS * scaling
Layout.rightMargin: Style.marginS * scaling
spacing: 0
// Week numbers column (only visible when enabled)
ColumnLayout {
visible: Settings.data.location.showWeekNumberInCalendar
Layout.preferredWidth: visible ? Style.baseWidgetSize * scaling : 0
Layout.fillHeight: true
spacing: 0
Repeater {
model: 6 // Maximum 6 weeks in a month view
Item {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredHeight: Style.baseWidgetSize * scaling
NText {
anchors.centerIn: parent
color: Color.mOutline
pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightBold
text: {
// Calculate the date shown in the first column of this row
// MonthGrid always shows 42 days (6 weeks × 7 days)
// First, find the first day of the month
let firstOfMonth = new Date(grid.year, grid.month, 1)
// 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()
// Calculate offset: how many days before the 1st should the grid start?
let daysBeforeFirst = (firstOfMonthDayOfWeek - firstDayOfWeek + 7) % 7
// 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
}
// 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)}`
}
}
}
}
}
// The actual calendar grid
MonthGrid {
id: grid
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 0
month: Time.date.getMonth()
year: Time.date.getFullYear()
locale: Qt.locale()
delegate: Item {
Rectangle {
width: Style.baseWidgetSize * scaling
height: Style.baseWidgetSize * scaling
radius: width / 2
color: model.today ? Color.mPrimary : Color.transparent
NText {
anchors.centerIn: parent
text: model.day
color: model.today ? Color.mOnPrimary : Color.mOnSurface
opacity: model.month === grid.month ? Style.opacityHeavy : Style.opacityLight
pointSize: Style.fontSizeM * scaling
font.weight: model.today ? Style.fontWeightBold : Style.fontWeightRegular
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
}
}
}
}
// ISO 8601 week number calculation
// This is locale-independent and always uses Monday as first day of week
function getISOWeekNumber(date) {
// 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)
// Get day of week where Monday = 1, Sunday = 7
const dayOfWeek = target.getDay() || 7
// Set to nearest Thursday (which determines the week number)
target.setDate(target.getDate() + 4 - dayOfWeek)
// 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
}
}

View File

@@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
@@ -7,6 +8,8 @@ import qs.Widgets
Item {
id: root
property ShellScreen screen
property string icon: ""
property string text: ""
property string suffix: ""
@@ -43,6 +46,7 @@ Item {
Component {
id: verticalPillComponent
BarPillVertical {
screen: root.screen
icon: root.icon
text: root.text
suffix: root.suffix
@@ -68,6 +72,7 @@ Item {
Component {
id: horizontalPillComponent
BarPillHorizontal {
screen: root.screen
icon: root.icon
text: root.text
suffix: root.suffix

View File

@@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
@@ -7,6 +8,8 @@ import qs.Widgets
Item {
id: root
property ShellScreen screen
property string icon: ""
property string text: ""
property string suffix: ""
@@ -20,7 +23,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
@@ -46,8 +49,18 @@ Item {
width: pillHeight + Math.max(0, pill.width - pillOverlap)
height: pillHeight
Connections {
target: root
function onTooltipTextChanged() {
TooltipService.updateText(root.tooltipText)
}
}
Rectangle {
id: pill
property ShellScreen screen: root.screen
width: revealed ? pillMaxWidth : 1
height: pillHeight
@@ -77,8 +90,8 @@ Item {
return centerX + offset
}
text: root.text + root.suffix
font.family: Settings.data.ui.fontFixed
font.pointSize: textSize
family: Settings.data.ui.fontFixed
pointSize: textSize
font.weight: Style.fontWeightBold
color: forceOpen ? Color.mOnSurface : Color.mPrimary
visible: revealed
@@ -119,7 +132,7 @@ Item {
NIcon {
icon: root.icon
font.pointSize: iconSize
pointSize: iconSize
color: hovered ? Color.mOnTertiary : Color.mOnSurface
// Center horizontally
x: (iconCircle.width - width) / 2
@@ -195,14 +208,6 @@ Item {
}
}
NTooltip {
id: tooltip
positionAbove: Settings.data.bar.position === "bottom"
target: pill
delay: Style.tooltipDelayLong
text: root.tooltipText
}
Timer {
id: showTimer
interval: Style.pillDelay
@@ -220,8 +225,8 @@ Item {
onEntered: {
hovered = true
root.entered()
tooltip.show()
if (disableOpen) {
TooltipService.show(pill, root.tooltipText, BarService.getTooltipDirection(), Style.tooltipDelayLong)
if (disableOpen || forceClose) {
return
}
if (!forceOpen) {
@@ -231,10 +236,10 @@ Item {
onExited: {
hovered = false
root.exited()
if (!forceOpen) {
if (!forceOpen && !forceClose) {
hide()
}
tooltip.hide()
TooltipService.hide()
}
onClicked: function (mouse) {
if (mouse.button === Qt.LeftButton) {

View File

@@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
@@ -7,6 +8,7 @@ import qs.Widgets
Item {
id: root
property ShellScreen screen
property string icon: ""
property string text: ""
property string suffix: ""
@@ -58,8 +60,18 @@ Item {
width: buttonSize
height: revealed ? (buttonSize + maxPillHeight - pillOverlap) : buttonSize
Connections {
target: root
function onTooltipTextChanged() {
TooltipService.updateText(root.tooltipText)
}
}
Rectangle {
id: pill
property ShellScreen screen: root.screen
width: revealed ? maxPillWidth : 1
height: revealed ? maxPillHeight : 1
@@ -91,8 +103,8 @@ Item {
return offset
}
text: root.text + root.suffix
font.family: Settings.data.ui.fontFixed
font.pointSize: textSize
family: Settings.data.ui.fontFixed
pointSize: textSize
font.weight: Style.fontWeightMedium
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
@@ -144,7 +156,7 @@ Item {
NIcon {
icon: root.icon
font.pointSize: iconSize
pointSize: iconSize
color: hovered ? Color.mOnTertiary : Color.mOnSurface
// Center horizontally
x: (iconCircle.width - width) / 2
@@ -236,16 +248,6 @@ Item {
}
}
NTooltip {
id: tooltip
target: pill
text: root.tooltipText
positionLeft: barPosition === "right"
positionRight: barPosition === "left"
positionAbove: Settings.data.bar.position === "bottom"
delay: Style.tooltipDelayLong
}
Timer {
id: showTimer
interval: Style.pillDelay
@@ -263,7 +265,7 @@ Item {
onEntered: {
hovered = true
root.entered()
tooltip.show()
TooltipService.show(pill, root.tooltipText, BarService.getTooltipDirection(), Style.tooltipDelayLong)
if (disableOpen || forceClose) {
return
}
@@ -277,7 +279,7 @@ Item {
if (!forceOpen && !forceClose) {
hide()
}
tooltip.hide()
TooltipService.hide()
}
onClicked: function (mouse) {
if (mouse.button === Qt.LeftButton) {

View File

@@ -8,12 +8,14 @@ Item {
property string widgetId: ""
property var widgetProps: ({})
property bool enabled: true
property string screenName: widgetProps.screen ? widgetProps.screen.name : ""
property string section: widgetProps.section || ""
property int sectionIndex: widgetProps.sectionWidgetIndex || 0
Connections {
target: ScalingService
function onScaleChanged(screenName, scale) {
if (loader.item && loader.item.screen && screenName === loader.item.screen.name) {
function onScaleChanged(aScreenName, scale) {
if (loader.item && loader.item.screen && aScreenName === screenName) {
loader.item['scaling'] = scale
}
}
@@ -27,7 +29,7 @@ Item {
id: loader
anchors.fill: parent
active: Settings.isLoaded && enabled && widgetId !== ""
active: widgetId !== ""
sourceComponent: {
if (!active) {
return null
@@ -45,18 +47,30 @@ Item {
}
}
// Register this widget instance with BarService
if (screenName && section) {
BarService.registerWidget(screenName, section, widgetId, sectionIndex, item)
}
if (item.hasOwnProperty("onLoaded")) {
item.onLoaded()
}
Logger.log("NWidgetLoader", "Loaded", widgetId, "on screen", item.screen.name)
//Logger.log("BarWidgetLoader", "Loaded", widgetId, "on screen", item.screen.name)
}
Component.onDestruction: {
// Unregister when destroyed
if (screenName && section) {
BarService.unregisterWidget(screenName, section, widgetId, sectionIndex)
}
}
}
// Error handling
onWidgetIdChanged: {
if (widgetId && !BarWidgetRegistry.hasWidget(widgetId)) {
Logger.warn("WidgetLoader", "Widget not found in registry:", widgetId)
Logger.warn("BarWidgetLoader", "Widget not found in bar registry:", widgetId)
}
}
}

View File

@@ -160,7 +160,7 @@ PopupWindow {
Layout.fillWidth: true
color: (modelData?.enabled ?? true) ? (mouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface) : Color.mOnSurfaceVariant
text: modelData?.text !== "" ? modelData?.text.replace(/[\n\r]+/g, ' ') : "..."
font.pointSize: Style.fontSizeS * scaling
pointSize: Style.fontSizeS * scaling
verticalAlignment: Text.AlignVCenter
wrapMode: Text.WordWrap
}
@@ -175,7 +175,7 @@ PopupWindow {
NIcon {
icon: modelData?.hasChildren ? "menu" : ""
font.pointSize: Style.fontSizeS * scaling
pointSize: Style.fontSizeS * scaling
verticalAlignment: Text.AlignVCenter
visible: modelData?.hasChildren ?? false
color: (mouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface)
@@ -235,13 +235,13 @@ PopupWindow {
openLeft = false
} else {
// Bar is horizontal (top/bottom) or undefined, use space-based logic
openLeft = (globalPos.x + entry.width + submenuWidth > (screen ? screen.width : Screen.width))
openLeft = (globalPos.x + entry.width + submenuWidth > screen.width)
// Secondary check: ensure we don't open off-screen
if (openLeft && globalPos.x - submenuWidth < 0) {
// Would open off the left edge, force right opening
openLeft = false
} else if (!openLeft && globalPos.x + entry.width + submenuWidth > (screen ? screen.width : Screen.width)) {
} else if (!openLeft && globalPos.x + entry.width + submenuWidth > screen.width) {
// Would open off the right edge, force left opening
openLeft = true
}

View File

@@ -35,13 +35,13 @@ NPanel {
NIcon {
icon: Settings.data.network.wifiEnabled ? "wifi" : "wifi-off"
font.pointSize: Style.fontSizeXXL * scaling
pointSize: Style.fontSizeXXL * scaling
color: Settings.data.network.wifiEnabled ? Color.mPrimary : Color.mOnSurfaceVariant
}
NText {
text: "Wi-Fi"
font.pointSize: Style.fontSizeL * scaling
text: I18n.tr("wifi.panel.title")
pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
@@ -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()
}
@@ -92,14 +92,14 @@ NPanel {
NIcon {
icon: "warning"
font.pointSize: Style.fontSizeL * scaling
pointSize: Style.fontSizeL * scaling
color: Color.mError
}
NText {
text: NetworkService.lastError
color: Color.mError
font.pointSize: Style.fontSizeS * scaling
pointSize: Style.fontSizeS * scaling
wrapMode: Text.Wrap
Layout.fillWidth: true
}
@@ -130,21 +130,21 @@ NPanel {
NIcon {
icon: "wifi-off"
font.pointSize: 64 * scaling
pointSize: 64 * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Wi-Fi is disabled"
font.pointSize: Style.fontSizeL * scaling
text: I18n.tr("wifi.panel.disabled")
pointSize: Style.fontSizeL * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Enable Wi-Fi to see available networks."
font.pointSize: Style.fontSizeS * scaling
text: I18n.tr("wifi.panel.enable-message")
pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
@@ -172,8 +172,8 @@ NPanel {
}
NText {
text: "Searching for nearby networks..."
font.pointSize: Style.fontSizeNormal * scaling
text: I18n.tr("wifi.panel.searching")
pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
@@ -242,7 +242,7 @@ NPanel {
NIcon {
icon: NetworkService.signalIcon(modelData.signal)
font.pointSize: Style.fontSizeXXL * scaling
pointSize: Style.fontSizeXXL * scaling
color: modelData.connected ? Color.mPrimary : Color.mOnSurface
}
@@ -252,7 +252,7 @@ NPanel {
NText {
text: modelData.ssid
font.pointSize: Style.fontSizeNormal * scaling
pointSize: Style.fontSizeNormal * scaling
font.weight: modelData.connected ? Style.fontWeightBold : Style.fontWeightMedium
color: Color.mOnSurface
elide: Text.ElideRight
@@ -263,20 +263,22 @@ NPanel {
spacing: Style.marginXS * scaling
NText {
text: `${modelData.signal}%`
font.pointSize: Style.fontSizeXXS * scaling
text: I18n.tr("system.signal-strength", {
"signal": modelData.signal
})
pointSize: Style.fontSizeXXS * scaling
color: Color.mOnSurfaceVariant
}
NText {
text: "•"
font.pointSize: Style.fontSizeXXS * scaling
pointSize: Style.fontSizeXXS * scaling
color: Color.mOnSurfaceVariant
}
NText {
text: NetworkService.isSecured(modelData.security) ? modelData.security : "Open"
font.pointSize: Style.fontSizeXXS * scaling
pointSize: Style.fontSizeXXS * scaling
color: Color.mOnSurfaceVariant
}
@@ -295,8 +297,8 @@ NPanel {
NText {
id: connectedText
anchors.centerIn: parent
text: "Connected"
font.pointSize: Style.fontSizeXXS * scaling
text: I18n.tr("wifi.panel.connected")
pointSize: Style.fontSizeXXS * scaling
color: Color.mOnPrimary
}
}
@@ -311,8 +313,8 @@ NPanel {
NText {
id: disconnectingText
anchors.centerIn: parent
text: "Disconnecting..."
font.pointSize: Style.fontSizeXXS * scaling
text: I18n.tr("wifi.panel.disconnecting")
pointSize: Style.fontSizeXXS * scaling
color: Color.mOnPrimary
}
}
@@ -327,8 +329,8 @@ NPanel {
NText {
id: forgettingText
anchors.centerIn: parent
text: "Forgetting..."
font.pointSize: Style.fontSizeXXS * scaling
text: I18n.tr("wifi.panel.forgetting")
pointSize: Style.fontSizeXXS * scaling
color: Color.mOnPrimary
}
}
@@ -345,8 +347,8 @@ NPanel {
NText {
id: savedText
anchors.centerIn: parent
text: "Saved"
font.pointSize: Style.fontSizeXXS * scaling
text: I18n.tr("wifi.panel.saved")
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
@@ -437,6 +439,7 @@ NPanel {
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Style.marginS * scaling
text: passwordInput
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS * scaling
color: Color.mOnSurface
echoMode: TextInput.Password
@@ -454,18 +457,18 @@ NPanel {
}
}
Text {
NText {
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
pointSize: Style.fontSizeS * scaling
}
}
}
NButton {
text: "Connect"
text: I18n.tr("wifi.panel.connect")
fontSize: Style.fontSizeXXS * scaling
enabled: passwordInput.length > 0 && !NetworkService.connecting
outlined: true
@@ -506,13 +509,13 @@ NPanel {
RowLayout {
NIcon {
icon: "trash"
font.pointSize: Style.fontSizeL * scaling
pointSize: Style.fontSizeL * scaling
color: Color.mError
}
NText {
text: "Forget this network?"
font.pointSize: Style.fontSizeS * scaling
text: I18n.tr("wifi.panel.forget-network")
pointSize: Style.fontSizeS * scaling
color: Color.mError
Layout.fillWidth: true
}
@@ -520,7 +523,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
@@ -555,20 +558,20 @@ NPanel {
NIcon {
icon: "search"
font.pointSize: 64 * scaling
pointSize: 64 * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "No networks found"
font.pointSize: Style.fontSizeL * scaling
text: I18n.tr("wifi.panel.no-networks")
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()

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(145, 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()
font.pointSize: Style.fontSizeS * scaling
text: windowTitle
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
RowLayout {
spacing: 50 * scaling // Gap between text copies
NText {
id: titleText
text: windowTitle
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,27 +326,16 @@ Item {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton
onEntered: {
if (barPosition === "left" || barPosition === "right") {
tooltip.show()
if ((windowTitle !== "") && (barPosition === "left" || barPosition === "right") || (scrollingMode === "never")) {
TooltipService.show(root, windowTitle, BarService.getTooltipDirection())
}
}
onExited: {
if (barPosition === "left" || barPosition === "right") {
tooltip.hide()
}
TooltipService.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
}
}
}

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
@@ -85,8 +87,9 @@ Item {
BarPill {
id: pill
screen: root.screen
compact: (Settings.data.bar.density === "compact")
rightOpen: BarWidgetRegistry.getPillDirection(root)
rightOpen: BarService.getPillDirection(root)
icon: testMode ? BatteryService.getIcon(testPercent, testCharging, true) : BatteryService.getIcon(percent, charging, isReady)
text: (isReady || testMode) ? Math.round(percent) : "-"
suffix: "%"

View File

@@ -19,9 +19,9 @@ NIconButton {
colorFg: Color.mOnSurface
colorBorder: Color.transparent
colorBorderHover: Color.transparent
icon: Settings.data.network.bluetoothEnabled ? "bluetooth" : "bluetooth-off"
tooltipText: "Bluetooth devices."
tooltipText: I18n.tr("tooltips.bluetooth-devices")
tooltipDirection: BarService.getTooltipDirection()
icon: BluetoothService.enabled ? "bluetooth" : "bluetooth-off"
onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this)
onRightClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this)
}

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
@@ -77,8 +77,9 @@ Item {
BarPill {
id: pill
screen: root.screen
compact: (Settings.data.bar.density === "compact")
rightOpen: BarWidgetRegistry.getPillDirection(root)
rightOpen: BarService.getPillDirection(root)
icon: getIcon()
autoHide: false // Important to be false so we can hover as long as we want
text: {

View File

@@ -29,179 +29,83 @@ 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 var now: Time.date
// Resolve settings: try user settings or defaults from BarWidgetRegistry
readonly property bool use12h: widgetSettings.use12HourClock !== undefined ? widgetSettings.use12HourClock : widgetMetadata.use12HourClock
readonly property bool reverseDayMonth: widgetSettings.reverseDayMonth !== undefined ? widgetSettings.reverseDayMonth : widgetMetadata.reverseDayMonth
readonly property string displayFormat: widgetSettings.displayFormat !== undefined ? widgetSettings.displayFormat : widgetMetadata.displayFormat
readonly property bool usePrimaryColor: widgetSettings.usePrimaryColor !== undefined ? widgetSettings.usePrimaryColor : widgetMetadata.usePrimaryColor
readonly property bool useCustomFont: widgetSettings.useCustomFont !== undefined ? widgetSettings.useCustomFont : widgetMetadata.useCustomFont
readonly property string customFont: widgetSettings.customFont !== undefined ? widgetSettings.customFont : widgetMetadata.customFont
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.locale().toString(now, formatHorizontal.trim()).split("\\n")
NText {
visible: text !== ""
text: modelData
family: useCustomFont && customFont ? customFont : Settings.data.ui.fontDefault
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 += " - " + (reverseDayMonth ? `${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:
// Day
return now.getDate().toString().padStart(2, '0')
case 1:
// Month
return (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 reverseDayMonth ? `${month}/${day}` : `${day}/${month}`
}
// 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(reverseDayMonth)}.`
target: clockContainer
positionAbove: Settings.data.bar.position === "bottom"
// 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.locale().toString(now, formatVertical.trim()).split(" ")
delegate: NText {
visible: text !== ""
text: modelData
family: useCustomFont && customFont ? customFont : Settings.data.ui.fontDefault
pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightBold
color: usePrimaryColor ? Color.mPrimary : Color.mOnSurface
wrapMode: Text.WordWrap
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
}
}
}
}
}
MouseArea {
@@ -211,14 +115,14 @@ Rectangle {
hoverEnabled: true
onEntered: {
if (!PanelService.getPanel("calendarPanel")?.active) {
tooltip.show()
TooltipService.show(root, I18n.tr("clock.tooltip"), BarService.getTooltipDirection())
}
}
onExited: {
tooltip.hide()
TooltipService.hide()
}
onClicked: {
tooltip.hide()
TooltipService.hide()
PanelService.getPanel("calendarPanel")?.toggle(this)
}
}

View File

@@ -29,10 +29,14 @@ 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")
tooltipDirection: BarService.getTooltipDirection()
baseSize: Style.capsuleHeight
compact: (Settings.data.bar.density === "compact")
colorBg: (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
@@ -40,16 +44,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.startsWith("file://") ? customIconPath : "file://" + customIconPath
if (useDistroLogo)
return DistroLogoService.osLogo
return ""
}
visible: source !== ""
smooth: true
asynchronous: true
}

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 {
@@ -47,7 +47,8 @@ Item {
BarPill {
id: pill
rightOpen: BarWidgetRegistry.getPillDirection(root)
screen: root.screen
rightOpen: BarService.getPillDirection(root)
icon: customIcon
text: _dynamicText
compact: (Settings.data.bar.density === "compact")
@@ -57,7 +58,7 @@ Item {
disableOpen: true
tooltipText: {
if (!hasExec) {
return "Custom Button - Configure in settings"
return "Custom button, configure in settings."
} else {
var lines = []
if (leftClickExec !== "") {

View File

@@ -10,7 +10,8 @@ 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")
tooltipDirection: BarService.getTooltipDirection()
compact: (Settings.data.bar.density === "compact")
baseSize: Style.capsuleHeight
colorBg: Settings.data.colorSchemes.darkMode ? (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent) : Color.mPrimary

View File

@@ -14,9 +14,11 @@ 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")
tooltipDirection: BarService.getTooltipDirection()
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()
}

View File

@@ -42,19 +42,21 @@ Item {
BarPill {
id: pill
screen: root.screen
anchors.verticalCenter: parent.verticalCenter
compact: (Settings.data.bar.density === "compact")
rightOpen: BarWidgetRegistry.getPillDirection(root)
rightOpen: BarService.getPillDirection(root)
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.
}
}
}

View File

@@ -33,13 +33,43 @@ 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(145, screen.width * 0.06)
readonly property bool hasActivePlayer: MediaService.currentPlayer !== null && getTitle() !== ""
readonly property string placeholderText: I18n.tr("bar.widget-settings.media-mini.no-active-player")
readonly property string tooltipText: {
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
}
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 || hasActivePlayer || (!hasActivePlayer && !autoHide) ? 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 +79,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 +92,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 +106,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 +121,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 +136,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,23 +151,25 @@ 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")
z: 1 // Above the visualizer
NIcon {
id: windowIcon
icon: MediaService.isPlaying ? "media-pause" : "media-play"
font.pointSize: Style.fontSizeL * scaling
icon: hasActivePlayer ? (MediaService.isPlaying ? "media-pause" : "media-play") : "disc"
color: hasActivePlayer ? Color.mOnSurface : Color.mOnSurfaceVariant
pointSize: Style.fontSizeL * scaling
verticalAlignment: Text.AlignVCenter
Layout.alignment: Qt.AlignVCenter
visible: !showAlbumArt && getTitle() !== "" && !trackArt.visible
visible: !hasActivePlayer || (!showAlbumArt && !trackArt.visible)
}
ColumnLayout {
Layout.alignment: Qt.AlignVCenter
visible: showAlbumArt
visible: showAlbumArt && hasActivePlayer
spacing: 0
Item {
@@ -180,24 +188,143 @@ 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 = (hasActivePlayer && 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: 0
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: {
containerWidth = width
updateScrollingState()
}
Component.onCompleted: {
containerWidth = width
updateScrollingState()
}
Connections {
target: mouseArea
function onContainsMouseChanged() {
titleContainer.updateScrollingState()
}
}
// Scrolling content
Item {
id: scrollContainer
height: parent.height
width: parent.width
property real scrollX: 0
x: scrollX
RowLayout {
spacing: 50 * scaling // Gap between text copies
NText {
id: titleText
text: hasActivePlayer ? getTitle() : placeholderText
pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
verticalAlignment: Text.AlignVCenter
horizontalAlignment: hasActivePlayer ? Text.AlignLeft : Text.AlignHCenter
color: hasActivePlayer ? Color.mOnSurface : Color.mOnSurfaceVariant
}
NText {
text: hasActivePlayer ? getTitle() : placeholderText
font: titleText.font
verticalAlignment: Text.AlignVCenter
horizontalAlignment: hasActivePlayer ? Text.AlignLeft : Text.AlignHCenter
color: hasActivePlayer ? Color.mOnSurface : Color.mOnSurfaceVariant
visible: hasActivePlayer && 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 {
@@ -222,13 +349,13 @@ Item {
width: Style.baseWidgetSize * 0.5 * scaling
height: Style.baseWidgetSize * 0.5 * scaling
anchors.centerIn: parent
visible: getTitle() !== ""
NIcon {
id: mediaIconVertical
anchors.fill: parent
icon: MediaService.isPlaying ? "media-pause" : "media-play"
font.pointSize: Style.fontSizeL * scaling
icon: hasActivePlayer ? (MediaService.isPlaying ? "media-pause" : "media-play") : "disc"
color: hasActivePlayer ? Color.mOnSurface : Color.mOnSurfaceVariant
pointSize: Style.fontSizeL * scaling
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
@@ -240,9 +367,13 @@ Item {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
cursorShape: hasActivePlayer ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: mouse => {
if (!hasActivePlayer || !MediaService.currentPlayer || !MediaService.canPlay) {
return
}
if (mouse.button === Qt.LeftButton) {
MediaService.playPause()
} else if (mouse.button == Qt.RightButton) {
@@ -257,43 +388,15 @@ Item {
}
onEntered: {
if (barPosition === "left" || barPosition === "right") {
tooltip.show()
} else if (tooltip.text !== "") {
tooltip.show()
var textToShow = hasActivePlayer ? tooltipText : placeholderText
if ((textToShow !== "") && (barPosition === "left" || barPosition === "right") || (scrollingMode === "never")) {
TooltipService.show(root, textToShow, BarService.getTooltipDirection())
}
}
onExited: {
if (barPosition === "left" || barPosition === "right") {
tooltip.hide()
} else {
tooltip.hide()
}
TooltipService.hide()
}
}
}
}
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
}
}
target: (barPosition === "left" || barPosition === "right") ? verticalLayout : anchor
positionLeft: barPosition === "right"
positionRight: barPosition === "left"
positionAbove: Settings.data.bar.position === "bottom"
delay: 500
}
}

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
@@ -89,15 +89,19 @@ Item {
BarPill {
id: pill
rightOpen: BarWidgetRegistry.getPillDirection(root)
screen: root.screen
rightOpen: BarService.getPillDirection(root)
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

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,8 +22,15 @@ 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")
tooltipDirection: BarService.getTooltipDirection()
onClicked: {
// Check if wlsunset is available before enabling night light
if (!ProgramCheckerService.wlsunsetAvailable) {
ToastService.showWarning(I18n.tr("settings.display.night-light.section.label"), I18n.tr("toast.night-light.not-installed"))
return
}
if (!Settings.data.nightLight.enabled) {
Settings.data.nightLight.enabled = true
Settings.data.nightLight.forced = false

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,8 @@ 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")
tooltipDirection: BarService.getTooltipDirection()
colorBg: (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
colorFg: Color.mOnSurface
colorBorder: Color.transparent

View File

@@ -11,45 +11,19 @@ NIconButton {
property ShellScreen screen
property real scaling: 1.0
readonly property bool hasPP: PowerProfileService.available
baseSize: Style.capsuleHeight
visible: hasPP
visible: PowerProfileService.available
function profileIcon() {
if (!hasPP)
return "balanced"
if (PowerProfileService.profile === PowerProfile.Performance)
return "performance"
if (PowerProfileService.profile === PowerProfile.Balanced)
return "balanced"
if (PowerProfileService.profile === PowerProfile.PowerSaver)
return "powersaver"
}
function profileName() {
if (!hasPP)
return "Unknown"
if (PowerProfileService.profile === PowerProfile.Performance)
return "Performance"
if (PowerProfileService.profile === PowerProfile.Balanced)
return "Balanced"
if (PowerProfileService.profile === PowerProfile.PowerSaver)
return "Power Saver"
}
function changeProfile() {
if (!hasPP)
return
PowerProfileService.cycleProfile()
}
icon: root.profileIcon()
tooltipText: root.profileName()
icon: PowerProfileService.getIcon()
tooltipText: I18n.tr("tooltips.power-profile", {
"profile": PowerProfileService.getName()
})
tooltipDirection: BarService.getTooltipDirection()
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
colorBorder: Color.transparent
colorBorderHover: Color.transparent
onClicked: root.changeProfile()
onClicked: PowerProfileService.cycleProfile()
}

View File

@@ -0,0 +1,23 @@
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")
tooltipDirection: BarService.getTooltipDirection()
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()
}

View File

@@ -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()
}

View File

@@ -14,10 +14,11 @@ NIconButton {
compact: (Settings.data.bar.density === "compact")
baseSize: Style.capsuleHeight
icon: "power"
tooltipText: "Power Settings"
tooltipText: I18n.tr("tooltips.session-menu")
tooltipDirection: BarService.getTooltipDirection()
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()
}

View File

@@ -39,12 +39,39 @@ Rectangle {
readonly property bool showNetworkStats: (widgetSettings.showNetworkStats !== undefined) ? widgetSettings.showNetworkStats : widgetMetadata.showNetworkStats
readonly property bool showDiskUsage: (widgetSettings.showDiskUsage !== undefined) ? widgetSettings.showDiskUsage : widgetMetadata.showDiskUsage
readonly property real iconSize: textSize * 1.4
readonly property real textSize: {
var base = isVertical ? width * 0.82 : height
return Math.max(1, compact ? base * 0.43 : base * 0.33)
}
readonly property real iconSize: textSize * 1.25
readonly property int percentTextWidth: Math.ceil(percentMetrics.tightBoundingRect.width + 2)
readonly property int tempTextWidth: Math.ceil(tempMetrics.tightBoundingRect.width + 2)
readonly property int memTextWidth: Math.ceil(memMetrics.tightBoundingRect.width + 2)
TextMetrics {
id: percentMetrics
font.family: Settings.data.ui.fontFixed
font.weight: Style.fontWeightMedium
font.pointSize: textSize * Settings.data.ui.fontFixedScale
text: "99%" // Use the longest possible string for measurement
}
TextMetrics {
id: tempMetrics
font.family: Settings.data.ui.fontFixed
font.weight: Style.fontWeightMedium
font.pointSize: textSize * Settings.data.ui.fontFixedScale
text: "99°" // Use the longest possible string for measurement
}
TextMetrics {
id: memMetrics
font.family: Settings.data.ui.fontFixed
font.weight: Style.fontWeightMedium
font.pointSize: textSize * Settings.data.ui.fontFixedScale
text: "99.9K" // Longest value part of network speed
}
anchors.centerIn: parent
implicitWidth: isVertical ? Math.round(Style.capsuleHeight * scaling) : Math.round(mainGrid.implicitWidth + Style.marginM * 2 * scaling)
@@ -55,18 +82,15 @@ Rectangle {
GridLayout {
id: mainGrid
anchors.centerIn: parent
// Dynamic layout based on bar orientation
flow: isVertical ? GridLayout.TopToBottom : GridLayout.LeftToRight
rows: isVertical ? -1 : 1
columns: isVertical ? 1 : -1
rowSpacing: isVertical ? (Style.marginS * scaling) : (Style.marginXS * scaling)
columnSpacing: isVertical ? (Style.marginXS * scaling) : (Style.marginXS * scaling)
rowSpacing: isVertical ? (Style.marginM * scaling) : 0
columnSpacing: isVertical ? 0 : (Style.marginM * scaling)
// CPU Usage Component
Item {
Layout.preferredWidth: cpuUsageContent.implicitWidth
Layout.preferredWidth: isVertical ? root.width : iconSize + percentTextWidth + (Style.marginXXS * scaling)
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
visible: showCpuUsage
@@ -80,32 +104,34 @@ Rectangle {
rowSpacing: Style.marginXXS * scaling
columnSpacing: Style.marginXXS * scaling
NIcon {
icon: "cpu-usage"
pointSize: iconSize
Layout.alignment: Qt.AlignCenter
Layout.row: isVertical ? 1 : 0
Layout.column: 0
}
NText {
text: isVertical ? `${Math.round(SystemStatService.cpuUsage)}%` : `${SystemStatService.cpuUsage}%`
font.family: Settings.data.ui.fontFixed
font.pointSize: textSize
text: `${Math.round(SystemStatService.cpuUsage)}%`
family: Settings.data.ui.fontFixed
pointSize: textSize
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignCenter
horizontalAlignment: Text.AlignHCenter
Layout.preferredWidth: isVertical ? -1 : percentTextWidth
horizontalAlignment: isVertical ? Text.AlignHCenter : Text.AlignRight
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
Layout.row: isVertical ? 0 : 0
Layout.column: isVertical ? 0 : 1
}
NIcon {
icon: "cpu-usage"
font.pointSize: iconSize
Layout.alignment: Qt.AlignCenter
Layout.row: isVertical ? 1 : 0
Layout.column: 0
scale: isVertical ? Math.min(1.0, root.width / implicitWidth) : 1.0
}
}
}
// CPU Temperature Component
Item {
Layout.preferredWidth: cpuTempContent.implicitWidth
Layout.preferredWidth: isVertical ? root.width : (iconSize + tempTextWidth) + (Style.marginXXS * scaling)
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
visible: showCpuTemp
@@ -119,32 +145,34 @@ Rectangle {
rowSpacing: Style.marginXXS * scaling
columnSpacing: Style.marginXXS * scaling
NIcon {
icon: "cpu-temperature"
pointSize: iconSize
Layout.alignment: Qt.AlignCenter
Layout.row: isVertical ? 1 : 0
Layout.column: 0
}
NText {
text: isVertical ? `${SystemStatService.cpuTemp}°` : `${SystemStatService.cpuTemp}°C`
font.family: Settings.data.ui.fontFixed
font.pointSize: textSize
text: `${Math.round(SystemStatService.cpuTemp)}°`
family: Settings.data.ui.fontFixed
pointSize: textSize
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignCenter
horizontalAlignment: Text.AlignHCenter
Layout.preferredWidth: isVertical ? -1 : tempTextWidth
horizontalAlignment: isVertical ? Text.AlignHCenter : Text.AlignRight
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
Layout.row: isVertical ? 0 : 0
Layout.column: isVertical ? 0 : 1
}
NIcon {
icon: "cpu-temperature"
font.pointSize: iconSize
Layout.alignment: Qt.AlignCenter
Layout.row: isVertical ? 1 : 0
Layout.column: 0
scale: isVertical ? Math.min(1.0, root.width / implicitWidth) : 1.0
}
}
}
// Memory Usage Component
Item {
Layout.preferredWidth: memoryContent.implicitWidth
Layout.preferredWidth: isVertical ? root.width : iconSize + (showMemoryAsPercent ? percentTextWidth : memTextWidth) + (Style.marginXXS * scaling)
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
visible: showMemoryUsage
@@ -158,38 +186,34 @@ Rectangle {
rowSpacing: Style.marginXXS * scaling
columnSpacing: Style.marginXXS * scaling
NIcon {
icon: "memory"
pointSize: iconSize
Layout.alignment: Qt.AlignCenter
Layout.row: isVertical ? 1 : 0
Layout.column: 0
}
NText {
text: {
if (showMemoryAsPercent) {
return `${SystemStatService.memPercent}%`
} else {
return isVertical ? `${Math.round(SystemStatService.memGb)}G` : `${SystemStatService.memGb}G`
}
}
font.family: Settings.data.ui.fontFixed
font.pointSize: textSize
text: showMemoryAsPercent ? `${Math.round(SystemStatService.memPercent)}%` : `${SystemStatService.memGb.toFixed(1)}G`
family: Settings.data.ui.fontFixed
pointSize: textSize
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignCenter
horizontalAlignment: Text.AlignHCenter
Layout.preferredWidth: isVertical ? -1 : (showMemoryAsPercent ? percentTextWidth : memTextWidth)
horizontalAlignment: isVertical ? Text.AlignHCenter : Text.AlignRight
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
Layout.row: isVertical ? 0 : 0
Layout.column: isVertical ? 0 : 1
}
NIcon {
icon: "memory"
font.pointSize: iconSize
Layout.alignment: Qt.AlignCenter
Layout.row: isVertical ? 1 : 0
Layout.column: 0
scale: isVertical ? Math.min(1.0, root.width / implicitWidth) : 1.0
}
}
}
// Network Download Speed Component
Item {
Layout.preferredWidth: downloadContent.implicitWidth
Layout.preferredWidth: isVertical ? root.width : iconSize + memTextWidth + (Style.marginXXS * scaling)
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
visible: showNetworkStats
@@ -201,34 +225,36 @@ Rectangle {
rows: isVertical ? 2 : 1
columns: isVertical ? 1 : 2
rowSpacing: Style.marginXXS * scaling
columnSpacing: isVertical ? (Style.marginXXS * scaling) : (Style.marginXS * scaling)
columnSpacing: Style.marginXXS * scaling
NIcon {
icon: "download-speed"
pointSize: iconSize
Layout.alignment: Qt.AlignCenter
Layout.row: isVertical ? 1 : 0
Layout.column: 0
}
NText {
text: isVertical ? SystemStatService.formatCompactSpeed(SystemStatService.rxSpeed) : SystemStatService.formatSpeed(SystemStatService.rxSpeed)
font.family: Settings.data.ui.fontFixed
font.pointSize: textSize
family: Settings.data.ui.fontFixed
pointSize: textSize
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignCenter
horizontalAlignment: Text.AlignHCenter
Layout.preferredWidth: isVertical ? -1 : memTextWidth
horizontalAlignment: isVertical ? Text.AlignHCenter : Text.AlignRight
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
Layout.row: isVertical ? 0 : 0
Layout.column: isVertical ? 0 : 1
}
NIcon {
icon: "download-speed"
font.pointSize: iconSize
Layout.alignment: Qt.AlignCenter
Layout.row: isVertical ? 1 : 0
Layout.column: 0
scale: isVertical ? Math.min(1.0, root.width / implicitWidth) : 1.0
}
}
}
// Network Upload Speed Component
Item {
Layout.preferredWidth: uploadContent.implicitWidth
Layout.preferredWidth: isVertical ? root.width : iconSize + memTextWidth + (Style.marginXXS * scaling)
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
visible: showNetworkStats
@@ -240,34 +266,36 @@ Rectangle {
rows: isVertical ? 2 : 1
columns: isVertical ? 1 : 2
rowSpacing: Style.marginXXS * scaling
columnSpacing: isVertical ? (Style.marginXXS * scaling) : (Style.marginXS * scaling)
columnSpacing: Style.marginXXS * scaling
NIcon {
icon: "upload-speed"
pointSize: iconSize
Layout.alignment: Qt.AlignCenter
Layout.row: isVertical ? 1 : 0
Layout.column: 0
}
NText {
text: isVertical ? SystemStatService.formatCompactSpeed(SystemStatService.txSpeed) : SystemStatService.formatSpeed(SystemStatService.txSpeed)
font.family: Settings.data.ui.fontFixed
font.pointSize: textSize
family: Settings.data.ui.fontFixed
pointSize: textSize
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignCenter
horizontalAlignment: Text.AlignHCenter
Layout.preferredWidth: isVertical ? -1 : memTextWidth
horizontalAlignment: isVertical ? Text.AlignHCenter : Text.AlignRight
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
Layout.row: isVertical ? 0 : 0
Layout.column: isVertical ? 0 : 1
}
NIcon {
icon: "upload-speed"
font.pointSize: iconSize
Layout.alignment: Qt.AlignCenter
Layout.row: isVertical ? 1 : 0
Layout.column: 0
scale: isVertical ? Math.min(1.0, root.width / implicitWidth) : 1.0
}
}
}
// Disk Usage Component (primary drive)
Item {
Layout.preferredWidth: diskContent.implicitWidth
Layout.preferredWidth: isVertical ? root.width : iconSize + percentTextWidth + (Style.marginXXS * scaling)
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
visible: showDiskUsage
@@ -279,27 +307,29 @@ Rectangle {
rows: isVertical ? 2 : 1
columns: isVertical ? 1 : 2
rowSpacing: Style.marginXXS * scaling
columnSpacing: isVertical ? (Style.marginXXS * scaling) : (Style.marginXS * scaling)
columnSpacing: Style.marginXXS * scaling
NIcon {
icon: "storage"
pointSize: iconSize
Layout.alignment: Qt.AlignCenter
Layout.row: isVertical ? 1 : 0
Layout.column: 0
}
NText {
text: `${SystemStatService.diskPercent}%`
font.family: Settings.data.ui.fontFixed
font.pointSize: textSize
family: Settings.data.ui.fontFixed
pointSize: textSize
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignCenter
horizontalAlignment: Text.AlignHCenter
Layout.preferredWidth: isVertical ? -1 : percentTextWidth
horizontalAlignment: isVertical ? Text.AlignHCenter : Text.AlignRight
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
Layout.row: isVertical ? 0 : 0
Layout.column: isVertical ? 0 : 1
}
NIcon {
icon: "storage"
font.pointSize: iconSize
Layout.alignment: Qt.AlignCenter
Layout.row: isVertical ? 1 : 0
Layout.column: 0
scale: isVertical ? Math.min(1.0, root.width / implicitWidth) : 1.0
}
}
}

View File

@@ -13,10 +13,27 @@ Rectangle {
property ShellScreen screen
property real scaling: 1.0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
readonly property bool isVerticalBar: Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
readonly property bool compact: (Settings.data.bar.density === "compact")
readonly property real itemSize: compact ? Style.capsuleHeight * 0.9 * scaling : Style.capsuleHeight * 0.8 * scaling
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
// Always visible when there are toplevels
implicitWidth: isVerticalBar ? Math.round(Style.capsuleHeight * scaling) : taskbarLayout.implicitWidth + Style.marginM * scaling * 2
implicitHeight: isVerticalBar ? taskbarLayout.implicitHeight + Style.marginM * scaling * 2 : Math.round(Style.capsuleHeight * scaling)
@@ -41,36 +58,37 @@ Rectangle {
columnSpacing: isVerticalBar ? 0 : Style.marginXXS * root.scaling
Repeater {
model: ToplevelManager && ToplevelManager.toplevels ? ToplevelManager.toplevels : []
model: CompositorService.windows
delegate: Item {
id: taskbarItem
required property Toplevel modelData
property Toplevel toplevel: modelData
property bool isActive: ToplevelManager.activeToplevel === modelData
required property var modelData
property ShellScreen screen: root.screen
visible: (!widgetSettings.onlySameOutput || modelData.output == screen.name) && (!widgetSettings.onlyActiveWorkspaces || CompositorService.getActiveWorkspaces().map(ws => ws.id).includes(modelData.workspaceId))
Layout.preferredWidth: root.itemSize
Layout.preferredHeight: root.itemSize
Layout.alignment: Qt.AlignCenter
Rectangle {
id: iconBackground
anchors.centerIn: parent
IconImage {
id: appIcon
width: parent.width
height: parent.height
color: taskbarItem.isActive ? Color.mPrimary : root.color
border.width: 0
radius: Math.round(Style.radiusXS * root.scaling)
border.color: "transparent"
z: -1
source: ThemeIcons.iconForAppId(taskbarItem.modelData.appId)
smooth: true
asynchronous: true
opacity: modelData.isFocused ? Style.opacityFull : 0.6
IconImage {
id: appIcon
anchors.centerIn: parent
width: parent.width
height: parent.height
source: AppIcons.iconForAppId(taskbarItem.modelData.appId)
smooth: true
asynchronous: true
Rectangle {
anchors.bottomMargin: -2 * scaling
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
id: iconBackground
width: 4 * scaling
height: 4 * scaling
color: modelData.isFocused ? Color.mPrimary : Color.transparent
radius: width * 0.5
}
}
@@ -86,27 +104,20 @@ Rectangle {
if (mouse.button === Qt.LeftButton) {
try {
taskbarItem.modelData.activate()
CompositorService.focusWindow(taskbarItem.modelData.id)
} catch (error) {
Logger.error("Taskbar", "Failed to activate toplevel: " + error)
}
} else if (mouse.button === Qt.RightButton) {
try {
taskbarItem.modelData.close()
CompositorService.closeWindow(taskbarItem.modelData.id)
} catch (error) {
Logger.error("Taskbar", "Failed to close toplevel: " + error)
}
}
}
onEntered: taskbarTooltip.show()
onExited: taskbarTooltip.hide()
}
NTooltip {
id: taskbarTooltip
text: taskbarItem.modelData.title || taskbarItem.modelData.appId || "Unknown App."
target: taskbarItem
positionAbove: Settings.data.bar.position === "bottom"
onEntered: TooltipService.show(taskbarItem, taskbarItem.modelData.title || taskbarItem.modelData.appId || "Unknown app.", BarService.getTooltipDirection())
onExited: TooltipService.hide()
}
}
}

View File

@@ -53,6 +53,9 @@ Rectangle {
IconImage {
id: trayIcon
property ShellScreen screen: root.screen
anchors.centerIn: parent
width: Style.marginL * scaling
height: Style.marginL * scaling
@@ -102,7 +105,7 @@ Rectangle {
modelData.secondaryActivate && modelData.secondaryActivate()
} else if (mouse.button === Qt.RightButton) {
trayTooltip.hide()
TooltipService.hideImmediately()
// Close the menu if it was visible
if (trayPanel && trayPanel.visible) {
@@ -135,15 +138,11 @@ Rectangle {
}
}
}
onEntered: trayTooltip.show()
onExited: trayTooltip.hide()
}
NTooltip {
id: trayTooltip
target: trayIcon
text: modelData.tooltipTitle || modelData.name || modelData.id || "Tray Item"
positionAbove: Settings.data.bar.position === "bottom"
onEntered: {
trayPanel.close()
TooltipService.show(trayIcon, modelData.tooltipTitle || modelData.name || modelData.id || "Tray Item", BarService.getTooltipDirection())
}
onExited: TooltipService.hide()
}
}
}

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
@@ -75,15 +75,18 @@ Item {
BarPill {
id: pill
screen: root.screen
compact: (Settings.data.bar.density === "compact")
rightOpen: BarWidgetRegistry.getPillDirection(root)
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

View File

@@ -0,0 +1,24 @@
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")
tooltipDirection: BarService.getTooltipDirection()
colorBg: (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
colorFg: Color.mOnSurface
colorBorder: Color.transparent
colorBorderHover: Color.transparent
onClicked: PanelService.getPanel("wallpaperPanel")?.toggle(this)
}

View File

@@ -19,7 +19,8 @@ NIconButton {
colorFg: Color.mOnSurface
colorBorder: Color.transparent
colorBorderHover: Color.transparent
tooltipText: I18n.tr("tooltips.manage-wifi")
tooltipDirection: BarService.getTooltipDirection()
icon: {
try {
if (NetworkService.ethernetConnected) {
@@ -40,7 +41,6 @@ NIconButton {
return "signal_wifi_bad"
}
}
tooltipText: "Manage Wi-Fi."
onClicked: PanelService.getPanel("wifiPanel")?.toggle(this)
onRightClicked: PanelService.getPanel("wifiPanel")?.toggle(this)
}

View File

@@ -7,6 +7,7 @@ import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services
import qs.Widgets
Item {
id: root
@@ -56,6 +57,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 +68,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 +100,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 +134,7 @@ Item {
onHideUnoccupiedChanged: refreshWorkspaces()
Connections {
target: WorkspaceService
target: CompositorService
function onWorkspacesChanged() {
refreshWorkspaces()
}
@@ -120,8 +143,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 +212,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 +266,7 @@ Item {
Item {
id: workspacePillContainer
width: root.getWorkspaceWidth(model)
height: Style.capsuleHeight * root.baseDimensionRatio
height: Style.capsuleHeight * root.baseDimensionRatio * scaling
Rectangle {
id: pill
@@ -212,7 +275,7 @@ Item {
Loader {
active: (labelMode !== "none")
sourceComponent: Component {
Text {
NText {
x: (pill.width - width) / 2
y: (pill.height - height) / 2 + (height - contentHeight) / 2
text: {
@@ -222,9 +285,9 @@ Item {
return model.idx.toString()
}
}
font.pointSize: model.isFocused ? workspacePillContainer.height * 0.45 : workspacePillContainer.height * 0.42
family: Settings.data.ui.fontFixed
pointSize: model.isFocused ? workspacePillContainer.height * 0.45 : workspacePillContainer.height * 0.42
font.capitalization: Font.AllUppercase
font.family: Settings.data.ui.fontFixed
font.weight: Style.fontWeightBold
wrapMode: Text.Wrap
color: {
@@ -235,7 +298,7 @@ Item {
if (model.isActive || model.isOccupied)
return Color.mOnSecondary
return Color.mOnSurface
return Color.mOnSecondary
}
}
}
@@ -250,7 +313,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 +323,7 @@ Item {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
WorkspaceService.switchToWorkspace(model.idx)
CompositorService.switchToWorkspace(model.idx)
}
hoverEnabled: true
}
@@ -346,7 +409,7 @@ Item {
model: localWorkspaces
Item {
id: workspacePillContainerVertical
width: Style.capsuleHeight * root.baseDimensionRatio
width: Style.capsuleHeight * root.baseDimensionRatio * scaling
height: root.getWorkspaceHeight(model)
Rectangle {
@@ -356,7 +419,7 @@ Item {
Loader {
active: (labelMode !== "none")
sourceComponent: Component {
Text {
NText {
x: (pillVertical.width - width) / 2
y: (pillVertical.height - height) / 2 + (height - contentHeight) / 2
text: {
@@ -366,9 +429,9 @@ Item {
return model.idx.toString()
}
}
font.pointSize: model.isFocused ? workspacePillContainerVertical.width * 0.45 : workspacePillContainerVertical.width * 0.42
family: Settings.data.ui.fontFixed
pointSize: model.isFocused ? workspacePillContainerVertical.width * 0.45 : workspacePillContainerVertical.width * 0.42
font.capitalization: Font.AllUppercase
font.family: Settings.data.ui.fontFixed
font.weight: Style.fontWeightBold
wrapMode: Text.Wrap
color: {
@@ -404,7 +467,7 @@ Item {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
WorkspaceService.switchToWorkspace(model.idx)
CompositorService.switchToWorkspace(model.idx)
}
hoverEnabled: true
}

View File

@@ -1,129 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Widgets
NPanel {
id: root
preferredWidth: 340
preferredHeight: 320
// Main Column
panelContent: ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginM * scaling
spacing: Style.marginXS * scaling
// Header: Month/Year with navigation
RowLayout {
Layout.fillWidth: true
Layout.leftMargin: Style.marginM * scaling
Layout.rightMargin: Style.marginM * scaling
spacing: Style.marginS * scaling
NIconButton {
icon: "chevron-left"
tooltipText: "Previous month"
onClicked: {
let newDate = new Date(grid.year, grid.month - 1, 1)
grid.year = newDate.getFullYear()
grid.month = newDate.getMonth()
}
}
NText {
text: grid.title
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
}
NIconButton {
icon: "chevron-right"
tooltipText: "Next month"
onClicked: {
let newDate = new Date(grid.year, grid.month + 1, 1)
grid.year = newDate.getFullYear()
grid.month = newDate.getMonth()
}
}
}
// Divider between header and weekdays
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginS * scaling
Layout.bottomMargin: Style.marginM * scaling
}
// Columns label (respects locale's first day of week)
RowLayout {
Layout.fillWidth: true
Layout.leftMargin: Style.marginS * scaling // Align with grid
Layout.rightMargin: Style.marginS * scaling
spacing: 0
Repeater {
model: 7
NText {
text: {
// Use the locale's first day of week setting
let firstDay = Qt.locale().firstDayOfWeek
let dayIndex = (firstDay + index) % 7
return Qt.locale().dayName(dayIndex, Locale.ShortFormat)
}
color: Color.mSecondary
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
horizontalAlignment: Text.AlignHCenter
Layout.fillWidth: true
Layout.preferredWidth: Style.baseWidgetSize * scaling
}
}
}
// Grids: days
MonthGrid {
id: grid
Layout.fillWidth: true
Layout.fillHeight: true // Take remaining space
Layout.leftMargin: Style.marginS * scaling
Layout.rightMargin: Style.marginS * scaling
spacing: 0
month: Time.date.getMonth()
year: Time.date.getFullYear()
locale: Qt.locale() // Use system locale
delegate: Rectangle {
width: (Style.baseWidgetSize * scaling)
height: (Style.baseWidgetSize * scaling)
radius: Style.radiusS * scaling
color: model.today ? Color.mPrimary : Color.transparent
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
}
}
}
}
}
}

View File

@@ -28,16 +28,16 @@ NBox {
NIcon {
icon: "disc"
font.pointSize: Style.fontSizeXXXL * 3 * scaling
pointSize: Style.fontSizeXXXL * 3 * scaling
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"
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 : ""
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()
}
}
}
}
@@ -167,7 +147,7 @@ NBox {
NIcon {
icon: "disc"
color: Color.mPrimary
font.pointSize: Style.fontSizeXXXL * 3 * scaling
pointSize: Style.fontSizeXXXL * 3 * scaling
visible: !trackArt.visible
anchors.centerIn: parent
}
@@ -182,7 +162,7 @@ NBox {
NText {
visible: MediaService.trackTitle !== ""
text: MediaService.trackTitle
font.pointSize: Style.fontSizeM * scaling
pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
elide: Text.ElideRight
wrapMode: Text.Wrap
@@ -193,8 +173,8 @@ NBox {
NText {
visible: MediaService.trackArtist !== ""
text: MediaService.trackArtist
color: Color.mOnSurface
font.pointSize: Style.fontSizeXS * scaling
color: Color.mPrimary
pointSize: Style.fontSizeXS * scaling
elide: Text.ElideRight
Layout.fillWidth: true
}
@@ -203,7 +183,7 @@ NBox {
visible: MediaService.trackAlbum !== ""
text: MediaService.trackAlbum
color: Color.mOnSurface
font.pointSize: Style.fontSizeXS * scaling
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 {

View File

@@ -25,45 +25,39 @@ NBox {
}
// Performance
NIconButton {
icon: "performance"
tooltipText: "Set performance power profile."
icon: PowerProfileService.getIcon(PowerProfile.Performance)
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
colorFg: (enabled && PowerProfileService.profile === PowerProfile.Performance) ? Color.mOnPrimary : Color.mPrimary
onClicked: {
if (enabled) {
PowerProfileService.setProfile(PowerProfile.Performance)
}
}
onClicked: PowerProfileService.setProfile(PowerProfile.Performance)
}
// Balanced
NIconButton {
icon: "balanced"
tooltipText: "Set balanced power profile."
icon: PowerProfileService.getIcon(PowerProfile.Balanced)
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
colorFg: (enabled && PowerProfileService.profile === PowerProfile.Balanced) ? Color.mOnPrimary : Color.mPrimary
onClicked: {
if (enabled) {
PowerProfileService.setProfile(PowerProfile.Balanced)
}
}
onClicked: PowerProfileService.setProfile(PowerProfile.Balanced)
}
// Eco
NIconButton {
icon: "powersaver"
tooltipText: "Set eco power profile."
icon: PowerProfileService.getIcon(PowerProfile.PowerSaver)
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
colorFg: (enabled && PowerProfileService.profile === PowerProfile.PowerSaver) ? Color.mOnPrimary : Color.mPrimary
onClicked: {
if (enabled) {
PowerProfileService.setProfile(PowerProfile.PowerSaver)
}
}
onClicked: PowerProfileService.setProfile(PowerProfile.PowerSaver)
}
Item {
Layout.fillWidth: true

View File

@@ -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,8 +42,10 @@ NBox {
font.capitalization: Font.Capitalize
}
NText {
text: `System uptime: ${uptimeText}`
font.pointSize: Style.fontSizeS * scaling
text: I18n.tr("system.uptime", {
"uptime": uptimeText
})
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()
}
}
}

View File

@@ -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,15 +54,9 @@ NBox {
NIconButton {
visible: Settings.data.wallpaper.enabled
icon: "wallpaper-selector"
tooltipText: "Left click: Open wallpaper selector.\nRight click: Set random wallpaper."
onClicked: {
var settingsPanel = PanelService.getPanel("settingsPanel")
settingsPanel.requestedTab = SettingsPanel.Tab.WallpaperSelector
settingsPanel.open()
}
onRightClicked: {
WallpaperService.setRandomWallpaper()
}
tooltipText: I18n.tr("tooltips.wallpaper-selector")
onClicked: PanelService.getPanel("wallpaperPanel")?.toggle(this)
onRightClicked: WallpaperService.setRandomWallpaper()
}
Item {

View File

@@ -24,7 +24,7 @@ NBox {
NIcon {
Layout.alignment: Qt.AlignVCenter
icon: weatherReady ? LocationService.weatherSymbolFromCode(LocationService.data.weather.current_weather.weathercode) : ""
font.pointSize: Style.fontSizeXXXL * 1.75 * scaling
pointSize: Style.fontSizeXXXL * 1.75 * scaling
color: Color.mPrimary
}
@@ -36,7 +36,7 @@ NBox {
const chunks = Settings.data.location.name.split(",")
return chunks[0]
}
font.pointSize: Style.fontSizeL * scaling
pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
}
@@ -56,13 +56,13 @@ NBox {
temp = Math.round(temp)
return `${temp}°${suffix}`
}
font.pointSize: Style.fontSizeXL * scaling
pointSize: Style.fontSizeXL * scaling
font.weight: Style.fontWeightBold
}
NText {
text: weatherReady ? `(${LocationService.data.weather.timezone_abbreviation})` : ""
font.pointSize: Style.fontSizeXS * scaling
pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
visible: LocationService.data.weather
}
@@ -88,7 +88,7 @@ NBox {
NText {
text: {
var weatherDate = new Date(LocationService.data.weather.daily.time[index].replace(/-/g, "/"))
return Qt.formatDateTime(weatherDate, "ddd")
return Qt.locale().toString(weatherDate, "ddd")
}
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
@@ -96,7 +96,7 @@ NBox {
NIcon {
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
icon: LocationService.weatherSymbolFromCode(LocationService.data.weather.daily.weathercode[index])
font.pointSize: Style.fontSizeXXL * 1.6 * scaling
pointSize: Style.fontSizeXXL * 1.6 * scaling
color: Color.mPrimary
}
NText {
@@ -112,7 +112,7 @@ NBox {
min = Math.round(min)
return `${max}°/${min}°`
}
font.pointSize: Style.fontSizeXS * scaling
pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
}
}

View File

@@ -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

View File

@@ -13,8 +13,20 @@ Variants {
model: Quickshell.screens
delegate: Item {
id: root
required property ShellScreen modelData
property real scaling: ScalingService.getScreenScale(modelData)
property bool barIsReady: BarService.isBarReady(modelData.name)
Connections {
target: BarService
function onBarReadyChanged(screenName) {
if (screenName === modelData.name) {
barIsReady = true
}
}
}
Connections {
target: ScalingService
@@ -25,6 +37,32 @@ 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()
}
function onOnlySameOutputChanged() {
updateDockApps()
}
}
// Initial update when component is ready
Component.onCompleted: {
if (ToplevelManager) {
updateDockApps()
}
}
// Shared properties between peek and dock windows
readonly property bool autoHide: Settings.data.dock.autoHide
readonly property int hideDelay: 500
@@ -43,12 +81,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 && !(Settings.data.dock.onlySameOutput && toplevel.screens && !toplevel.screens.includes(modelData))) {
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 +157,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
}
@@ -101,7 +193,7 @@ Variants {
// PEEK WINDOW - Always visible when auto-hide is enabled
Loader {
active: Settings.isLoaded && modelData && Settings.data.dock.monitors.includes(modelData.name) && autoHide
active: (barIsReady || !hasBar) && modelData && Settings.data.dock.monitors.includes(modelData.name) && autoHide
sourceComponent: PanelWindow {
id: peekWindow
@@ -137,7 +229,7 @@ Variants {
onExited: {
peekHovered = false
if (!hidden && !dockHovered && !anyAppHovered) {
if (!hidden && !dockHovered && !anyAppHovered && !menuHovered) {
hideTimer.restart()
}
}
@@ -147,7 +239,7 @@ Variants {
// DOCK WINDOW
Loader {
active: Settings.isLoaded && modelData && Settings.data.dock.monitors.includes(modelData.name) && dockLoaded && ToplevelManager && (ToplevelManager.toplevels.values.length > 0)
active: (barIsReady || !hasBar) && modelData && Settings.data.dock.monitors.includes(modelData.name) && dockLoaded && ToplevelManager && (dockApps.length > 0)
sourceComponent: PanelWindow {
id: dockWindow
@@ -235,10 +327,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 +344,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 +357,7 @@ Variants {
anchors.centerIn: parent
Repeater {
model: ToplevelManager ? ToplevelManager.toplevels : null
model: dockApps
delegate: Item {
id: appButton
@@ -268,17 +365,18 @@ 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")
// Individual tooltip for this app
NTooltip {
id: appTooltip
target: appButton
positionAbove: true
visible: false
// Listen for the toplevel being closed
Connections {
target: modelData?.toplevel
function onClosed() {
Qt.callLater(root.updateDockApps)
}
}
Image {
@@ -296,6 +394,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 +406,13 @@ Variants {
easing.overshoot: 1.2
}
}
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutQuad
}
}
}
// Fall back if no icon
@@ -312,8 +420,9 @@ Variants {
anchors.centerIn: parent
visible: !appIcon.visible
icon: "question-mark"
font.pointSize: iconSize * 0.7
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 +432,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,13 +474,13 @@ Variants {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
onEntered: {
anyAppHovered = true
const appName = appButton.appTitle || appButton.appId || "Unknown"
appTooltip.text = appName.length > 40 ? appName.substring(0, 37) + "..." : appName
appTooltip.isVisible = true
const tooltipText = appName.length > 40 ? appName.substring(0, 37) + "..." : appName
TooltipService.show(appButton, tooltipText, "top")
if (autoHide) {
showTimer.stop()
hideTimer.stop()
@@ -346,18 +490,44 @@ Variants {
onExited: {
anyAppHovered = false
appTooltip.hide()
if (autoHide && !dockHovered && !peekHovered) {
TooltipService.hide()
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
TooltipService.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])
}
}
}
}

273
Modules/Dock/DockMenu.qml Normal file
View File

@@ -0,0 +1,273 @@
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
property var onAppClosed: null // Callback function for when an app is closed
// Track which menu item is hovered
property int hoveredItem: -1 // -1: none, 0: focus, 1: pin, 2: close
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
}
// Helper function to determine which menu item is under the mouse
function getHoveredItem(mouseY) {
const itemHeight = 32 * scaling
const startY = Style.marginM * scaling
const relativeY = mouseY - startY
if (relativeY < 0)
return -1
const itemIndex = Math.floor(relativeY / itemHeight)
return itemIndex >= 0 && itemIndex < 3 ? itemIndex : -1
}
// Handle menu item clicks
function handleItemClick(itemIndex) {
switch (itemIndex) {
case 0:
// Focus
if (root.toplevel?.activate) {
root.toplevel.activate()
}
root.requestClose()
break
case 1:
// Pin/Unpin
if (root.toplevel?.appId) {
root.toggleAppPin(root.toplevel.appId)
}
root.requestClose()
break
case 2:
// Close
// 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()
break
}
}
Timer {
id: closeTimer
interval: 500
repeat: false
running: false
onTriggered: {
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)
// Single MouseArea to handle both auto-close and menu interactions
MouseArea {
id: menuMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: root.hoveredItem >= 0 ? Qt.PointingHandCursor : Qt.ArrowCursor
onEntered: {
closeTimer.stop()
}
onExited: {
root.hoveredItem = -1
closeTimer.start()
}
onPositionChanged: mouse => {
root.hoveredItem = root.getHoveredItem(mouse.y)
}
onClicked: mouse => {
const clickedItem = root.getHoveredItem(mouse.y)
if (clickedItem >= 0) {
root.handleItemClick(clickedItem)
}
}
}
ColumnLayout {
id: contextMenuColumn
anchors.fill: parent
anchors.margins: Style.marginM * scaling
spacing: 0
// Focus item
Rectangle {
Layout.fillWidth: true
height: 32 * scaling
color: root.hoveredItem === 0 ? Color.mTertiary : Color.transparent
radius: Style.radiusXS * scaling
RowLayout {
anchors.left: parent.left
anchors.leftMargin: Style.marginS * scaling
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
NIcon {
icon: "eye"
pointSize: Style.fontSizeL * scaling
color: root.hoveredItem === 0 ? Color.mOnTertiary : Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignVCenter
}
NText {
text: I18n.tr("dock.menu.focus")
pointSize: Style.fontSizeS * scaling
color: root.hoveredItem === 0 ? Color.mOnTertiary : Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignVCenter
}
}
}
// Pin/Unpin item
Rectangle {
Layout.fillWidth: true
height: 32 * scaling
color: root.hoveredItem === 1 ? Color.mTertiary : Color.transparent
radius: Style.radiusXS * scaling
RowLayout {
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"
}
pointSize: Style.fontSizeL * scaling
color: root.hoveredItem === 1 ? Color.mOnTertiary : Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignVCenter
}
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")
}
pointSize: Style.fontSizeS * scaling
color: root.hoveredItem === 1 ? Color.mOnTertiary : Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignVCenter
}
}
}
// Close item
Rectangle {
Layout.fillWidth: true
height: 32 * scaling
color: root.hoveredItem === 2 ? Color.mTertiary : Color.transparent
radius: Style.radiusXS * scaling
RowLayout {
anchors.left: parent.left
anchors.leftMargin: Style.marginS * scaling
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
NIcon {
icon: "close"
pointSize: Style.fontSizeL * scaling
color: root.hoveredItem === 2 ? Color.mOnTertiary : Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignVCenter
}
NText {
text: I18n.tr("dock.menu.close")
pointSize: Style.fontSizeS * scaling
color: root.hoveredItem === 2 ? Color.mOnTertiary : Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignVCenter
}
}
}
}
}
}

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
@@ -302,11 +302,34 @@ NPanel {
positionViewAtIndex(currentIndex, ListView.Contain)
}
}
onModelChanged: {
}
delegate: Rectangle {
id: entry
property bool isSelected: mouseArea.containsMouse || (index === selectedIndex)
// Accessor for app id
property string appId: (modelData && modelData.appId) ? String(modelData.appId) : ""
// Pin helpers
function togglePin(appId) {
if (!appId)
return
let arr = (Settings.data.dock.pinnedApps || []).slice()
const idx = arr.indexOf(appId)
if (idx >= 0)
arr.splice(idx, 1)
else
arr.push(appId)
Settings.data.dock.pinnedApps = arr
}
function isPinned(appId) {
const arr = Settings.data.dock.pinnedApps || []
return appId && arr.indexOf(appId) >= 0
}
// Property to reliably track the current item's ID.
// This changes whenever the delegate is recycled for a new item.
@@ -321,7 +344,7 @@ NPanel {
}
width: resultsList.width - Style.marginS * scaling
height: entryHeight
implicitHeight: entryHeight
radius: Style.radiusM * scaling
color: entry.isSelected ? Color.mTertiary : Color.mSurface
@@ -332,136 +355,152 @@ NPanel {
}
}
RowLayout {
ColumnLayout {
id: contentLayout
anchors.fill: parent
anchors.margins: Style.marginM * scaling
spacing: Style.marginM * scaling
// Icon badge or Image preview
Rectangle {
Layout.preferredWidth: badgeSize
Layout.preferredHeight: badgeSize
radius: Style.radiusM * scaling
color: Color.mSurfaceVariant
clip: true
// Top row - Main entry content with pin button
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
// Image preview for clipboard images
NImageRounded {
id: imagePreview
anchors.fill: parent
visible: modelData.isImage
imageRadius: Style.radiusM * scaling
// This property creates a dependency on the service's revision counter
readonly property int _rev: ClipboardService.revision
// Fetches from the service's cache.
// The dependency on `_rev` ensures this binding is re-evaluated when the cache is updated.
imagePath: {
_rev
return ClipboardService.getImageData(modelData.clipboardId) || ""
}
// Loading indicator
Rectangle {
anchors.fill: parent
visible: parent.status === Image.Loading
color: Color.mSurfaceVariant
BusyIndicator {
anchors.centerIn: parent
running: true
width: Style.baseWidgetSize * 0.5 * scaling
height: width
}
}
// Error fallback
onStatusChanged: status => {
if (status === Image.Error) {
iconLoader.visible = true
imagePreview.visible = false
}
}
}
// Icon fallback
Loader {
id: iconLoader
anchors.fill: parent
anchors.margins: Style.marginXS * scaling
visible: !modelData.isImage || imagePreview.status === Image.Error
active: visible
sourceComponent: Component {
IconImage {
anchors.fill: parent
source: modelData.icon ? AppIcons.iconFromName(modelData.icon, "application-x-executable") : ""
visible: modelData.icon && source !== ""
asynchronous: true
}
}
}
// Fallback text if no icon and no image
NText {
anchors.centerIn: parent
visible: !imagePreview.visible && !iconLoader.visible
text: modelData.name ? modelData.name.charAt(0).toUpperCase() : "?"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnPrimary
}
// Image type indicator overlay
// Icon badge or Image preview
Rectangle {
visible: modelData.isImage && imagePreview.visible
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.margins: 2 * scaling
width: formatLabel.width + 6 * scaling
height: formatLabel.height + 2 * scaling
Layout.preferredWidth: badgeSize
Layout.preferredHeight: badgeSize
radius: Style.radiusM * scaling
color: Color.mSurfaceVariant
clip: true
NText {
id: formatLabel
anchors.centerIn: parent
text: {
if (!modelData.isImage)
return ""
const desc = modelData.description || ""
const parts = desc.split(" • ")
return parts[0] || "IMG"
// Image preview for clipboard images
NImageRounded {
id: imagePreview
anchors.fill: parent
visible: modelData.isImage
imageRadius: Style.radiusM * scaling
// This property creates a dependency on the service's revision counter
readonly property int _rev: ClipboardService.revision
// Fetches from the service's cache.
// The dependency on `_rev` ensures this binding is re-evaluated when the cache is updated.
imagePath: {
_rev
return ClipboardService.getImageData(modelData.clipboardId) || ""
}
// Loading indicator
Rectangle {
anchors.fill: parent
visible: parent.status === Image.Loading
color: Color.mSurfaceVariant
BusyIndicator {
anchors.centerIn: parent
running: true
width: Style.baseWidgetSize * 0.5 * scaling
height: width
}
}
// Error fallback
onStatusChanged: status => {
if (status === Image.Error) {
iconLoader.visible = true
imagePreview.visible = false
}
}
}
// Icon fallback
Loader {
id: iconLoader
anchors.fill: parent
anchors.margins: Style.marginXS * scaling
visible: !modelData.isImage || imagePreview.status === Image.Error
active: visible
sourceComponent: Component {
IconImage {
anchors.fill: parent
source: modelData.icon ? ThemeIcons.iconFromName(modelData.icon, "application-x-executable") : ""
visible: modelData.icon && source !== ""
asynchronous: true
}
}
}
// Fallback text if no icon and no image
NText {
anchors.centerIn: parent
visible: !imagePreview.visible && !iconLoader.visible
text: modelData.name ? modelData.name.charAt(0).toUpperCase() : "?"
pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnPrimary
}
// Image type indicator overlay
Rectangle {
visible: modelData.isImage && imagePreview.visible
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.margins: 2 * scaling
width: formatLabel.width + 6 * scaling
height: formatLabel.height + 2 * scaling
radius: Style.radiusM * scaling
color: Color.mSurfaceVariant
NText {
id: formatLabel
anchors.centerIn: parent
text: {
if (!modelData.isImage)
return ""
const desc = modelData.description || ""
const parts = desc.split(" • ")
return parts[0] || "IMG"
}
pointSize: Style.fontSizeXXS * scaling
color: Color.mPrimary
}
font.pointSize: Style.fontSizeXXS * scaling
color: Color.mPrimary
}
}
}
// Text content
ColumnLayout {
Layout.fillWidth: true
spacing: 0 * scaling
NText {
text: modelData.name || "Unknown"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: entry.isSelected ? Color.mOnTertiary : Color.mOnSurface
elide: Text.ElideRight
// Text content
ColumnLayout {
Layout.fillWidth: true
spacing: 0 * scaling
NText {
text: modelData.name || "Unknown"
pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: entry.isSelected ? Color.mOnTertiary : Color.mOnSurface
elide: Text.ElideRight
Layout.fillWidth: true
}
NText {
text: modelData.description || ""
pointSize: Style.fontSizeS * scaling
color: entry.isSelected ? Color.mOnTertiary : Color.mOnSurfaceVariant
elide: Text.ElideRight
Layout.fillWidth: true
visible: text !== ""
}
}
NText {
text: modelData.description || ""
font.pointSize: Style.fontSizeS * scaling
color: entry.isSelected ? Color.mOnTertiary : Color.mOnSurfaceVariant
elide: Text.ElideRight
Layout.fillWidth: true
visible: text !== ""
// Pin/Unpin action icon button
NIconButton {
visible: !!entry.appId && !modelData.isImage && entry.isSelected && (Settings.data.dock.monitors && Settings.data.dock.monitors.length > 0)
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
icon: entry.isPinned(entry.appId) ? "unpin" : "pin"
tooltipText: entry.isPinned(entry.appId) ? I18n.tr("launcher.unpin") : I18n.tr("launcher.pin")
onClicked: entry.togglePin(entry.appId)
}
}
}
@@ -469,12 +508,17 @@ NPanel {
MouseArea {
id: mouseArea
anchors.fill: parent
z: -1
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedIndex = index
ui.activate()
}
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
selectedIndex = index
ui.activate()
mouse.accepted = true
}
}
acceptedButtons: Qt.LeftButton
}
}
}
@@ -492,7 +536,7 @@ NPanel {
const prefix = activePlugin?.name ? `${activePlugin.name}: ` : ""
return prefix + `${results.length} result${results.length !== 1 ? 's' : ''}`
}
font.pointSize: Style.fontSizeXS * scaling
pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
horizontalAlignment: Text.AlignCenter
}

View File

@@ -7,10 +7,42 @@ 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: []
// Persistent usage tracking stored in cacheDir
property string usageFilePath: Settings.cacheDir + "launcher_app_usage.json"
// Debounced saver to avoid excessive IO
Timer {
id: saveTimer
interval: 750
repeat: false
onTriggered: usageFile.writeAdapter()
}
FileView {
id: usageFile
path: usageFilePath
printErrors: false
watchChanges: false
onLoadFailed: function (error) {
if (error.toString().includes("No such file") || error === 2) {
writeAdapter()
}
}
onAdapterUpdated: saveTimer.start()
JsonAdapter {
id: usageAdapter
// key: app id/command, value: integer count
property var counts: ({})
}
}
function init() {
loadApplications()
}
@@ -36,8 +68,32 @@ Item {
return []
if (!query || query.trim() === "") {
// Return all apps alphabetically
return entries.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())).map(app => createResultEntry(app))
// Return all apps, optionally sorted by usage
const favoriteApps = Settings.data.appLauncher.favoriteApps || []
let sorted
if (Settings.data.appLauncher.sortByMostUsed) {
sorted = entries.slice().sort((a, b) => {
// Favorites first
const aFav = favoriteApps.includes(getAppKey(a))
const bFav = favoriteApps.includes(getAppKey(b))
if (aFav !== bFav)
return aFav ? -1 : 1
const ua = getUsageCount(a)
const ub = getUsageCount(b)
if (ub !== ua)
return ub - ua
return (a.name || "").toLowerCase().localeCompare((b.name || "").toLowerCase())
})
} else {
sorted = entries.slice().sort((a, b) => {
const aFav = favoriteApps.includes(getAppKey(a))
const bFav = favoriteApps.includes(getAppKey(b))
if (aFav !== bFav)
return aFav ? -1 : 1
return (a.name || "").toLowerCase().localeCompare((b.name || "").toLowerCase())
})
}
return sorted.map(app => createResultEntry(app))
}
// Use fuzzy search if available, fallback to simple search
@@ -48,7 +104,18 @@ Item {
"limit": 20
})
return fuzzyResults.map(result => createResultEntry(result.obj))
// Sort favorites first within fuzzy results while preserving fuzzysort order otherwise
const favoriteApps = Settings.data.appLauncher.favoriteApps || []
const fav = []
const nonFav = []
for (const r of fuzzyResults) {
const app = r.obj
if (favoriteApps.includes(getAppKey(app)))
fav.push(r)
else
nonFav.push(r)
}
return fav.concat(nonFav).map(result => createResultEntry(result.obj))
} else {
// Fallback to simple search
const searchTerm = query.toLowerCase()
@@ -74,6 +141,7 @@ Item {
function createResultEntry(app) {
return {
"appId": getAppKey(app),
"name": app.name || "Unknown",
"description": app.genericName || app.comment || "",
"icon": app.icon || "application-x-executable",
@@ -84,18 +152,60 @@ Item {
launcher.closeCompleted()
Logger.log("ApplicationsPlugin", `Launching: ${app.name}`)
// Record usage and persist asynchronously
if (Settings.data.appLauncher.sortByMostUsed)
recordUsage(app)
if (Settings.data.appLauncher.useApp2Unit && app.id) {
Logger.log("ApplicationsPlugin", `Using app2unit for: ${app.id}`)
if (app.runInTerminal)
Quickshell.execDetached(["app2unit", "--", app.id + ".desktop"])
else
Quickshell.execDetached(["app2unit", "--"].concat(app.command))
} else if (app.execute) {
app.execute()
} else {
Logger.log("ApplicationsPlugin", `Could not launch: ${app.name}`)
// Fallback logic when app2unit is not used
if (app.runInTerminal) {
// If app.execute() fails for terminal apps, we handle it manually.
Logger.log("ApplicationsPlugin", "Executing terminal app manually: " + app.name)
const terminal = Settings.data.appLauncher.terminalCommand.split(" ")
const command = terminal.concat(app.command)
Quickshell.execDetached(command)
} else if (app.execute) {
// Default execution for GUI apps
app.execute()
} else {
Logger.log("ApplicationsPlugin", `Could not launch: ${app.name}. No valid launch method.`)
}
}
}
}
}
// -------------------------
// Usage tracking helpers
function getAppKey(app) {
if (app && app.id)
return String(app.id)
if (app && app.command && app.command.join)
return app.command.join(" ")
return String(app && app.name ? app.name : "unknown")
}
function getUsageCount(app) {
const key = getAppKey(app)
const m = usageAdapter && usageAdapter.counts ? usageAdapter.counts : null
if (!m)
return 0
const v = m[key]
return typeof v === 'number' && isFinite(v) ? v : 0
}
function recordUsage(app) {
const key = getAppKey(app)
if (!usageAdapter.counts)
usageAdapter.counts = ({})
const current = getUsageCount(app)
usageAdapter.counts[key] = current + 1
// Trigger save via debounced timer
saveTimer.restart()
}
}

View File

@@ -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,

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 () {}

View File

@@ -26,6 +26,15 @@ Loader {
}
}
function formatTime() {
return Settings.data.location.use12hourFormat ? Qt.locale().toString(new Date(), "h:mm A") : Qt.locale().toString(new Date(), "HH:mm")
}
function formatDate() {
// For full text date, day is always before month, so we use this format for everybody: Wednesday, September 17.
return Qt.locale().toString(new Date(), "dddd, MMMM d")
}
function scheduleUnloadAfterUnlock() {
unloadAfterUnlockTimer.start()
}
@@ -137,9 +146,9 @@ Loader {
NText {
id: timeText
text: Qt.formatDateTime(new Date(), "HH:mm")
font.family: Settings.data.ui.fontBillboard
font.pointSize: Style.fontSizeXXXL * 6 * scaling
text: formatTime()
// Smaller time display when using longer 12 hour format
pointSize: Settings.data.location.use12hourFormat ? Style.fontSizeXXXL * 4 * scaling : Style.fontSizeXXXL * 5 * scaling
font.weight: Style.fontWeightBold
font.letterSpacing: -2 * scaling
color: Color.mOnSurface
@@ -163,9 +172,8 @@ Loader {
NText {
id: dateText
text: Qt.formatDateTime(new Date(), "dddd, MMMM d")
font.family: Settings.data.ui.fontBillboard
font.pointSize: Style.fontSizeXXL * scaling
text: formatDate()
pointSize: Style.fontSizeXXL * scaling
font.weight: Font.Light
color: Color.mOnSurface
horizontalAlignment: Text.AlignHCenter
@@ -189,7 +197,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
@@ -218,7 +226,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
@@ -248,7 +256,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
@@ -295,31 +303,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
@@ -354,6 +337,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
@@ -397,10 +381,10 @@ 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
family: Settings.data.ui.fontFixed
pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
Layout.fillWidth: true
}
@@ -410,13 +394,13 @@ Loader {
NText {
text: keyboardLayout.currentLayout
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeM * scaling
family: Settings.data.ui.fontFixed
pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
}
NIcon {
icon: "keyboard"
font.pointSize: Style.fontSizeM * scaling
pointSize: Style.fontSizeM * scaling
color: Color.mOnSurface
}
}
@@ -426,15 +410,15 @@ Loader {
visible: batteryIndicator.batteryVisible
NIcon {
icon: BatteryService.getIcon(batteryIndicator.percent, batteryIndicator.charging, batteryIndicator.isReady)
font.pointSize: Style.fontSizeM * scaling
pointSize: Style.fontSizeM * scaling
color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface
rotation: -90
}
NText {
text: Math.round(batteryIndicator.percent) + "%"
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeM * scaling
family: Settings.data.ui.fontFixed
pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
}
}
@@ -457,8 +441,8 @@ Loader {
NText {
text: Quickshell.env("USER") + "@noctalia:~$"
color: Color.mPrimary
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
family: Settings.data.ui.fontFixed
pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
}
@@ -466,10 +450,12 @@ Loader {
id: welcomeText
text: ""
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
family: Settings.data.ui.fontFixed
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
@@ -494,16 +480,29 @@ Loader {
NText {
text: Quickshell.env("USER") + "@noctalia:~$"
color: Color.mPrimary
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
family: Settings.data.ui.fontFixed
pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
}
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
family: Settings.data.ui.fontFixed
pointSize: Style.fontSizeL * scaling
}
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NText {
text: I18n.tr("lock-screen.password")
color: Color.mPrimary
family: Settings.data.ui.fontFixed
pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
}
TextInput {
@@ -535,48 +534,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
family: Settings.data.ui.fontFixed
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
}
}
}
}
@@ -599,8 +610,7 @@ Loader {
return Color.mError
return Color.transparent
}
font.family: "DejaVu Sans Mono"
font.pointSize: Style.fontSizeL * scaling
pointSize: Style.fontSizeL * scaling
Layout.fillWidth: true
SequentialAnimation on opacity {
@@ -620,6 +630,7 @@ Loader {
RowLayout {
Layout.alignment: Qt.AlignRight
Layout.bottomMargin: -10 * scaling
Layout.fillWidth: true
Rectangle {
Layout.preferredWidth: 120 * scaling
Layout.preferredHeight: 40 * scaling
@@ -633,8 +644,8 @@ Loader {
anchors.centerIn: parent
text: lockContext.unlockInProgress ? "EXECUTING" : "EXECUTE"
color: executeButtonArea.containsMouse ? Color.mOnPrimary : Color.mPrimary
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeM * scaling
family: Settings.data.ui.fontFixed
pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
}
@@ -708,6 +719,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
family: Settings.data.ui.fontFixed
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
@@ -728,7 +882,7 @@ Loader {
id: iconPower
anchors.centerIn: parent
icon: "shutdown"
font.pointSize: Style.fontSizeXXXL * scaling
pointSize: Style.fontSizeXXXL * scaling
color: powerButtonArea.containsMouse ? Color.mOnError : Color.mError
}
@@ -736,8 +890,8 @@ Loader {
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.top
anchors.bottomMargin: 12 * scaling
radius: Style.radiusM * scaling
anchors.bottomMargin: Style.marginM * scaling
radius: Style.radiusS * scaling
color: Color.mSurface
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
@@ -747,8 +901,9 @@ Loader {
id: shutdownTooltipText
anchors.margins: Style.marginM * scaling
anchors.fill: parent
text: "Shut down."
font.pointSize: Style.fontSizeM * scaling
text: I18n.tr("lock-screen.shut-down")
pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
@@ -779,7 +934,7 @@ Loader {
id: iconReboot
anchors.centerIn: parent
icon: "reboot"
font.pointSize: Style.fontSizeXXXL * scaling
pointSize: Style.fontSizeXXXL * scaling
color: restartButtonArea.containsMouse ? Color.mOnPrimary : Color.mPrimary
}
@@ -787,8 +942,8 @@ Loader {
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.top
anchors.bottomMargin: 12 * scaling
radius: Style.radiusM * scaling
anchors.bottomMargin: Style.marginM * scaling
radius: Style.radiusS * scaling
color: Color.mSurface
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
@@ -798,8 +953,9 @@ Loader {
id: restartTooltipText
anchors.margins: Style.marginM * scaling
anchors.fill: parent
text: "Restart."
font.pointSize: Style.fontSizeM * scaling
text: I18n.tr("lock-screen.restart")
pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
@@ -831,7 +987,7 @@ Loader {
id: iconSuspend
anchors.centerIn: parent
icon: "suspend"
font.pointSize: Style.fontSizeXXXL * scaling
pointSize: Style.fontSizeXXXL * scaling
color: suspendButtonArea.containsMouse ? Color.mOnSecondary : Color.mSecondary
}
@@ -839,8 +995,8 @@ Loader {
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.top
anchors.bottomMargin: 12 * scaling
radius: Style.radiusM * scaling
anchors.bottomMargin: Style.marginM * scaling
radius: Style.radiusS * scaling
color: Color.mSurface
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
@@ -850,8 +1006,9 @@ Loader {
id: suspendTooltipText
anchors.margins: Style.marginM * scaling
anchors.fill: parent
text: "Suspend."
font.pointSize: Style.fontSizeM * scaling
text: I18n.tr("lock-screen.suspend")
pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
@@ -877,8 +1034,8 @@ Loader {
running: true
repeat: true
onTriggered: {
timeText.text = Qt.formatDateTime(new Date(), "HH:mm")
dateText.text = Qt.formatDateTime(new Date(), "dddd, MMMM d")
timeText.text = formatTime()
dateText.text = formatDate()
}
}
}

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: 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.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.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: 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,78 +284,118 @@ 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
pointSize: Style.fontSizeXS * scaling
}
Item {
Layout.fillWidth: true
}
}
NText {
text: model.summary || "No summary"
font.pointSize: Style.fontSizeL * scaling
text: model.summary || I18n.tr("general.no-summary")
pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightMedium
color: Color.mOnSurface
textFormat: Text.PlainText
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
Layout.fillWidth: true
maximumLineCount: 3
elide: Text.ElideRight
visible: text.length > 0
}
NText {
text: model.body || ""
font.pointSize: Style.fontSizeM * scaling
pointSize: Style.fontSizeM * scaling
color: Color.mOnSurface
textFormat: Text.PlainText
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
Layout.fillWidth: true
maximumLineCount: 5
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
Flow {
Layout.fillWidth: true
spacing: Style.marginS * scaling
Layout.topMargin: Style.marginM * scaling
property var notificationActions: model.rawNotification ? model.rawNotification.actions : []
flow: Flow.LeftToRight
layoutDirection: Qt.LeftToRight
Repeater {
model: parent.notificationActions
// Store the notification ID for access in button delegates
property string parentNotificationId: notificationId
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
implicitHeight: 24 * scaling
onClicked: {
NotificationService.invokeAction(parent.parentNotificationId, actionData.identifier)
}
}
}
}
}
// 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

View File

@@ -13,7 +13,7 @@ NPanel {
id: root
preferredWidth: 380
preferredHeight: 500
preferredHeight: 480
panelKeyboardFocus: true
panelContent: Rectangle {
@@ -32,13 +32,13 @@ NPanel {
NIcon {
icon: "bell"
font.pointSize: Style.fontSizeXXL * scaling
pointSize: Style.fontSizeXXL * scaling
color: Color.mPrimary
}
NText {
text: "Notification History"
font.pointSize: Style.fontSizeL * scaling
text: I18n.tr("notifications.panel.title")
pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
@@ -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 {
@@ -90,21 +88,21 @@ NPanel {
NIcon {
icon: "bell-off"
font.pointSize: 64 * scaling
pointSize: 64 * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "No notifications"
font.pointSize: Style.fontSizeL * scaling
text: I18n.tr("notifications.panel.no-notifications")
pointSize: Style.fontSizeL * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Your notifications will show up here as they arrive."
font.pointSize: Style.fontSizeS * scaling
text: I18n.tr("notifications.panel.description")
pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
@@ -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,82 +139,121 @@ 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"
pointSize: Style.fontSizeXS * scaling
color: Color.mSecondary
}
NText {
text: Time.formatRelativeTime(model.timestamp)
pointSize: Style.fontSizeXS * scaling
color: Color.mSecondary
}
Item {
Layout.fillWidth: true
}
}
// Summary
NText {
text: (summary || "No summary").substring(0, 100)
font.pointSize: Style.fontSizeM * scaling
text: model.summary || I18n.tr("general.no-summary")
pointSize: Style.fontSizeM * scaling
font.weight: Font.Medium
color: Color.mPrimary
color: Color.mOnSurface
textFormat: Text.PlainText
wrapMode: Text.Wrap
Layout.fillWidth: true
maximumLineCount: 2
elide: Text.ElideRight
}
// Body
NText {
text: (body || "").substring(0, 150)
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurface
text: model.body || ""
pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant
textFormat: Text.PlainText
wrapMode: Text.Wrap
Layout.fillWidth: true
maximumLineCount: 3
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
}
}
}
}

544
Modules/OSD/OSD.qml Normal file
View File

@@ -0,0 +1,544 @@
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
// 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: modelData && (Settings.data.osd.monitors.includes(modelData.name) || (Settings.data.osd.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") {
if (isMuted)
return "0%"
const pct = Math.round(Math.min(1.0, currentVolume) * 100)
return pct + "%"
} else if (currentOSDType === "brightness") {
const pct = Math.round(Math.min(1.0, currentBrightness) * 100)
return pct + "%"
}
return ""
}
// Get progress bar color
function getProgressColor() {
if (currentOSDType === "volume") {
if (isMuted)
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 {
id: panel
screen: modelData
readonly property string location: (Settings.data.osd && Settings.data.osd.location) ? Settings.data.osd.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) || (location === "left")
readonly property bool isRight: (location.indexOf("_right") >= 0) || (location === "right")
readonly property bool isCentered: (location === "top" || location === "bottom")
readonly property bool verticalMode: (location === "left" || location === "right")
readonly property int hWidth: Math.round(320 * root.scaling)
readonly property int hHeight: Math.round(64 * root.scaling)
// Ensure an even width to keep the vertical bar perfectly centered
readonly property int barThickness: (function () {
const base = Math.max(6, Math.round(6 * root.scaling))
return (base % 2 === 0) ? base : base + 1
})()
// 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: {
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: {
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: {
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: {
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: verticalMode ? hHeight : hWidth
implicitHeight: osdItem.height
color: Color.transparent
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
exclusionMode: PanelWindow.ExclusionMode.Ignore
Rectangle {
id: osdItem
width: parent.width
height: panel.verticalMode ? panel.hWidth : Math.round(64 * root.scaling)
radius: Style.radiusL * root.scaling
color: Color.mSurface
border.color: Color.mOutline
border.width: (function () {
const bw = Math.max(2, Math.round(Style.borderM * root.scaling))
return (bw % 2 === 0) ? bw : bw + 1
})()
visible: false
opacity: 0
scale: 0.85
anchors.horizontalCenter: verticalMode ? undefined : parent.horizontalCenter
anchors.verticalCenter: verticalMode ? parent.verticalCenter : undefined
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: Settings.data.osd.autoHideMs
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
}
}
Loader {
id: contentLoader
anchors.fill: parent
active: true
sourceComponent: verticalMode ? verticalContent : horizontalContent
}
Component {
id: horizontalContent
Item {
anchors.fill: parent
RowLayout {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Style.marginM * root.scaling
spacing: Style.marginM * root.scaling
NIcon {
icon: root.getIcon()
color: root.getIconColor()
pointSize: Style.fontSizeXL * root.scaling
Layout.alignment: Qt.AlignVCenter
Behavior on color {
ColorAnimation {
duration: Style.animationNormal
easing.type: Easing.InOutQuad
}
}
}
// Progress bar with calculated width
Rectangle {
Layout.preferredWidth: Math.round(220 * root.scaling)
height: panel.barThickness
radius: Math.round(panel.barThickness / 2)
color: Color.mSurfaceVariant
Layout.alignment: Qt.AlignVCenter
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
}
}
}
}
// Percentage text
NText {
text: root.getDisplayPercentage()
color: Color.mOnSurface
pointSize: Style.fontSizeS * root.scaling
family: Settings.data.ui.fontFixed
Layout.alignment: Qt.AlignVCenter
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
Layout.preferredWidth: Math.round(50 * root.scaling)
}
}
}
}
Component {
id: verticalContent
ColumnLayout {
// Ensure inner padding respects the rounded corners; avoid clipping the icon/text
property int vMargin: (function () {
const styleMargin = Math.round(Style.marginL * root.scaling)
const cornerGuard = Math.round(osdItem.radius)
return Math.max(styleMargin, cornerGuard)
})()
property int vMarginTop: Math.max(Math.round(osdItem.radius), Math.round(Style.marginS * root.scaling))
property int balanceDelta: Math.round(Style.marginS * root.scaling)
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.topMargin: vMarginTop
anchors.bottomMargin: vMargin
width: (function () {
const w = parent.width - (vMargin * 2)
return (w % 2 === 0) ? w : w - 1
})()
spacing: Math.round(Style.marginS * root.scaling)
// Percentage text at top
Item {
Layout.fillWidth: true
Layout.preferredHeight: percentText.implicitHeight
NText {
id: percentText
text: root.getDisplayPercentage()
color: Color.mOnSurface
pointSize: Style.fontSizeS * root.scaling
family: Settings.data.ui.fontFixed
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
// Progress bar
Item {
Layout.fillWidth: true
Layout.fillHeight: true
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.bottom: parent.bottom
width: panel.barThickness
radius: Math.round(panel.barThickness / 2)
color: Color.mSurfaceVariant
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: parent.height * Math.min(1.0, root.getCurrentValue())
radius: parent.radius
color: root.getProgressColor()
Behavior on height {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.InOutQuad
}
}
Behavior on color {
ColorAnimation {
duration: Style.animationNormal
easing.type: Easing.InOutQuad
}
}
}
}
}
// Icon at bottom
NIcon {
icon: root.getIcon()
color: root.getIconColor()
pointSize: Style.fontSizeXL * root.scaling
Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
Layout.bottomMargin: vMargin + Math.round(Style.marginM * root.scaling) + balanceDelta
Behavior on color {
ColorAnimation {
duration: Style.animationNormal
easing.type: Easing.InOutQuad
}
}
}
}
}
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(() => {
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: 500
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.osd.enabled || !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(() => {
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
}
}
}
}

View File

@@ -14,7 +14,7 @@ NPanel {
id: root
preferredWidth: 440
preferredHeight: 410
preferredHeight: 480
panelAnchorHorizontalCenter: true
panelAnchorVerticalCenter: true
panelKeyboardFocus: true
@@ -30,28 +30,33 @@ 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": "lockAndSuspend",
"icon": "lock-pause",
"title": I18n.tr("session-menu.lock-and-suspend"),
"subtitle": I18n.tr("session-menu.lock-and-suspend-subtitle")
}, {
"action": "suspend",
"icon": "suspend",
"title": "Suspend",
"subtitle": "Put the system to sleep"
"title": I18n.tr("session-menu.suspend"),
"subtitle": I18n.tr("session-menu.suspend-subtitle")
}, {
"action": "reboot",
"icon": "reboot",
"title": "Reboot",
"subtitle": "Restart the system"
"title": I18n.tr("session-menu.reboot"),
"subtitle": I18n.tr("session-menu.reboot-subtitle")
}, {
"action": "logout",
"icon": "logout",
"title": "Logout",
"subtitle": "End your session"
"title": I18n.tr("session-menu.logout"),
"subtitle": I18n.tr("session-menu.logout-subtitle")
}, {
"action": "shutdown",
"icon": "shutdown",
"title": "Shutdown",
"subtitle": "Turn off the system",
"title": I18n.tr("session-menu.shutdown"),
"subtitle": I18n.tr("session-menu.shutdown-subtitle"),
"isShutdown": true
}]
@@ -97,6 +102,9 @@ NPanel {
lockScreen.active = true
}
break
case "lockAndSuspend":
CompositorService.lockAndSuspend()
break
case "suspend":
CompositorService.suspend()
break
@@ -263,9 +271,12 @@ 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
pointSize: Style.fontSizeL * scaling
color: timerActive ? Color.mPrimary : Color.mOnSurface
Layout.alignment: Qt.AlignVCenter
verticalAlignment: Text.AlignVCenter
@@ -277,7 +288,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
@@ -374,7 +385,7 @@ NPanel {
return Color.mOnTertiary
return Color.mOnSurface
}
font.pointSize: Style.fontSizeXXXL * scaling
pointSize: Style.fontSizeXXXL * scaling
width: Style.baseWidgetSize * 0.6 * scaling
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
@@ -398,7 +409,7 @@ NPanel {
NText {
text: buttonRoot.title
font.weight: Style.fontWeightMedium
font.pointSize: Style.fontSizeM * scaling
pointSize: Style.fontSizeM * scaling
color: {
if (buttonRoot.pending)
return Color.mPrimary
@@ -419,11 +430,11 @@ NPanel {
NText {
text: {
if (buttonRoot.pending) {
return "Click again to execute immediately"
return I18n.tr("session-menu.click-again")
}
return buttonRoot.subtitle
}
font.pointSize: Style.fontSizeXS * scaling
pointSize: Style.fontSizeXS * scaling
color: {
if (buttonRoot.pending)
return Color.mPrimary
@@ -453,7 +464,7 @@ NPanel {
NText {
anchors.centerIn: parent
text: Math.ceil(timeRemaining / 1000)
font.pointSize: Style.fontSizeS * scaling
pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightBold
color: Color.mOnPrimary
}

View File

@@ -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
@@ -69,7 +70,7 @@ NBox {
NText {
text: sectionName + " Section"
font.pointSize: Style.fontSizeL * scaling
pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.alignment: Qt.AlignVCenter
@@ -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
@@ -165,7 +225,7 @@ NBox {
NText {
text: modelData.id
font.pointSize: Style.fontSizeS * scaling
pointSize: Style.fontSizeS * scaling
color: root.getWidgetColor(modelData)[1]
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
@@ -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
@@ -250,10 +312,10 @@ NBox {
z: 2000
clip: false // Ensure ghost isn't clipped
Text {
NText {
id: ghostText
anchors.centerIn: parent
font.pointSize: Style.fontSizeS * scaling
pointSize: Style.fontSizeS * scaling
color: Color.mOnPrimary
}
}

View File

@@ -5,11 +5,11 @@ 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
@@ -19,56 +19,37 @@ Popup {
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
color: Color.mSurface
radius: Style.radiusL * scaling
border.color: Color.mPrimary
border.width: Math.max(1, Style.borderM * scaling)
}
contentItem: ColumnLayout {
id: content
width: parent.width
spacing: Style.marginM * scaling
@@ -77,8 +58,10 @@ Popup {
Layout.fillWidth: true
NText {
text: `${settingsPopup.widgetId} Settings`
font.pointSize: Style.fontSizeL * scaling
text: I18n.tr("system.widget-settings-title", {
"widget": widgetSettings.widgetId
})
pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
Layout.fillWidth: true
@@ -86,7 +69,8 @@ Popup {
NIconButton {
icon: "close"
onClicked: settingsPopup.close()
tooltipText: "Close"
onClicked: widgetSettings.close()
}
}
@@ -115,22 +99,51 @@ 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",
"Taskbar": "WidgetSettings/TaskbarSettings.qml"
}
const source = widgetSettingsMap[widgetId]
if (source) {
// Use setSource to pass properties at creation time
settingsLoader.setSource(source, {
"widgetData": widgetData,
"widgetMetadata": BarWidgetRegistry.widgetMetadata[widgetId]
})
}
}
}

View File

@@ -0,0 +1,62 @@
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
return settings
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("bar.widget-settings.active-window.auto-hide.label")
description: I18n.tr("bar.widget-settings.active-window.auto-hide.description")
checked: root.valueAutoHide
onToggled: checked => root.valueAutoHide = checked
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("bar.widget-settings.active-window.show-app-icon.label")
description: I18n.tr("bar.widget-settings.active-window.show-app-icon.description")
checked: root.valueShowIcon
onToggled: checked => root.valueShowIcon = checked
}
NComboBox {
label: I18n.tr("bar.widget-settings.active-window.scrolling-mode.label")
description: I18n.tr("bar.widget-settings.active-window.scrolling-mode.description")
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
}
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -0,0 +1,270 @@
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 valueUseCustomFont: widgetData.useCustomFont !== undefined ? widgetData.useCustomFont : widgetMetadata.useCustomFont
property string valueCustomFont: widgetData.customFont !== undefined ? widgetData.customFont : widgetMetadata.customFont
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.useCustomFont = valueUseCustomFont
settings.customFont = valueCustomFont
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-custom-font.label")
description: I18n.tr("bar.widget-settings.clock.use-custom-font.description")
checked: valueUseCustomFont
onToggled: checked => valueUseCustomFont = checked
}
NSearchableComboBox {
Layout.fillWidth: true
visible: valueUseCustomFont
label: I18n.tr("bar.widget-settings.clock.custom-font.label")
description: I18n.tr("bar.widget-settings.clock.custom-font.description")
model: FontService.availableFonts
currentKey: valueCustomFont
placeholder: I18n.tr("bar.widget-settings.clock.custom-font.placeholder")
searchPlaceholder: I18n.tr("bar.widget-settings.clock.custom-font.search-placeholder")
popupHeight: 420 * scaling
minimumWidth: 300 * scaling
onSelected: function (key) {
valueCustomFont = key
}
}
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.locale().toString(now, valueFormatHorizontal.trim()).split("\\n")
delegate: NText {
visible: text !== ""
text: modelData
family: valueUseCustomFont && valueCustomFont ? valueCustomFont : (valueUseMonospacedFont ? Settings.data.ui.fontFixed : Settings.data.ui.fontDefault)
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.locale().toString(now, valueFormatVertical.trim()).split(" ")
delegate: NText {
visible: text !== ""
text: modelData
family: valueUseCustomFont && valueCustomFont ? valueCustomFont : (valueUseMonospacedFont ? Settings.data.ui.fontFixed : Settings.data.ui.fontDefault)
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)
}
}

View File

@@ -0,0 +1,103 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
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.label")
description: I18n.tr("bar.widget-settings.control-center.use-distro-logo.description")
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
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: imagePicker.openFilePicker()
}
}
NIconPicker {
id: iconPicker
initialIcon: valueIcon
onIconSelected: iconName => {
valueIcon = iconName
valueCustomIconPath = ""
}
}
NFilePicker {
id: imagePicker
title: I18n.tr("bar.widget-settings.control-center.select-custom-icon")
selectionMode: "files"
nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.pnm", "*.bmp"]
initialPath: Quickshell.env("HOME")
onAccepted: paths => {
if (paths.length > 0) {
valueCustomIconPath = paths[0] // Use first selected file
}
}
}
}

View File

@@ -0,0 +1,110 @@
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
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.label")
description: I18n.tr("bar.widget-settings.custom-button.left-click.description")
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.label")
description: I18n.tr("bar.widget-settings.custom-button.right-click.description")
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.label")
description: I18n.tr("bar.widget-settings.custom-button.middle-click.description")
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) : ""
}
}

View File

@@ -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
}

View File

@@ -0,0 +1,91 @@
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.label")
description: I18n.tr("bar.widget-settings.media-mini.auto-hide.description")
checked: root.valueAutoHide
onToggled: checked => root.valueAutoHide = checked
}
NToggle {
label: I18n.tr("bar.widget-settings.media-mini.show-album-art.label")
description: I18n.tr("bar.widget-settings.media-mini.show-album-art.description")
checked: valueShowAlbumArt
onToggled: checked => valueShowAlbumArt = checked
}
NToggle {
label: I18n.tr("bar.widget-settings.media-mini.show-visualizer.label")
description: I18n.tr("bar.widget-settings.media-mini.show-visualizer.description")
checked: valueShowVisualizer
onToggled: checked => valueShowVisualizer = checked
}
NComboBox {
visible: valueShowVisualizer
label: I18n.tr("bar.widget-settings.media-mini.visualizer-type.label")
description: I18n.tr("bar.widget-settings.media-mini.visualizer-type.description")
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.label")
description: I18n.tr("bar.widget-settings.media-mini.scrolling-mode.description")
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
}
}

View File

@@ -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
}

View File

@@ -25,13 +25,15 @@ ColumnLayout {
}
NToggle {
label: "Show unread badge"
label: I18n.tr("bar.widget-settings.notification-history.show-unread-badge.label")
description: I18n.tr("bar.widget-settings.notification-history.show-unread-badge.description")
checked: valueShowUnreadBadge
onToggled: checked => valueShowUnreadBadge = checked
}
NToggle {
label: "Hide badge when zero"
label: I18n.tr("bar.widget-settings.notification-history.hide-badge-when-zero.label")
description: I18n.tr("bar.widget-settings.notification-history.hide-badge-when-zero.description")
checked: valueHideWhenZero
onToggled: checked => valueHideWhenZero = checked
}

View File

@@ -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")
}
}

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