Compare commits

...

272 Commits

Author SHA1 Message Date
LemmyCook
9a4317739b SettingsPanel: better var naming 2025-09-01 15:16:28 -04:00
LemmyCook
de32b86f7c SettingsPanel: Improved auto-sizing so it should work well on large and small screens 2025-09-01 15:11:37 -04:00
LemmyCook
5a1faa0fd4 SettingsPanel: ensure we never clip screen height 2025-09-01 15:08:15 -04:00
LemmyCook
57d912efc8 Toast: proper scaling + brought back assignation to WlrLayer.Overlay so its above all. 2025-09-01 15:03:30 -04:00
LemmyCook
87067f7062 TrayMenu: fix dynamic scaling 2025-09-01 14:41:12 -04:00
LemmyCook
210bbac583 ScalingService: 1st pass of the refactoring via signals instead of nested bindings for better efficienty and compatibility with old versions of Qt 2025-09-01 13:52:12 -04:00
LemmyCook
934c8c61b3 WallpaperSelector: current wallpaper border is a real border not a huge colored rectangle. looks better when switching wallpaper 2025-09-01 11:14:19 -04:00
Ly-sec
459bb59dd5 NightLight: moved from DisplayTab to BrightnessTab 2025-09-01 15:37:25 +02:00
LemmyCook
f1c9ed9caa Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-01 09:30:55 -04:00
LemmyCook
5d950b0a5e LightMode: better overview and transparency 2025-09-01 09:30:51 -04:00
Ly-sec
6f78079bc5 UtilitiesCard WallpaperSelector: add right click to choose random
wallpaper
2025-09-01 15:15:59 +02:00
LemmyCook
e3d62388f7 Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-01 09:07:25 -04:00
LemmyCook
5fef9cfe6b WallpaperService: refactored to a simpler signal based approach. 2025-09-01 09:07:23 -04:00
Ly-sec
4a4bec5aec Add support for user based templates (~/.config/matugen/config.toml) as
requested in #185
MatugenService: add logic to scan for the matugen config.toml
ColorSchemeTab: add NCheckbox to toggle user based templates
2025-09-01 14:54:01 +02:00
Ly-sec
4193d3c87c Remove test-microphone.sh 2025-09-01 14:24:59 +02:00
Ly-sec
0fd83498ea Create Microphone widget as requested in #180
Microphone: hook up microphone functionallity to bar widget
2025-09-01 14:22:45 +02:00
Ly-sec
00c94755c5 Replace Mask ScreenCorners with Canvas
ScreenCorners: replace Mask with Canvas, RAM usage seems fine
2025-09-01 14:06:16 +02:00
quadbyte
e8c2042290 Settings: better looking settings panel on 1080p 2025-09-01 00:15:49 -04:00
LemmyCook
6bcb85137b BarTab/NSectionEditor: minor UI improvements 2025-08-31 22:55:51 -04:00
Lemmy
d910f30ed1 Merge pull request #181 from kevindiaz314/main
docs: update README for AUR package changes and improved installation structure
2025-08-31 22:01:19 -04:00
Lemmy
3e8a87c6d6 Merge pull request #182 from nollidnosnhoj/main
disable test mode for battery widget.
2025-08-31 22:00:02 -04:00
LemmyCook
ecb7a9d448 BarWidgets: fixed NPill conditional open left or right that I broke earlier. 2025-08-31 21:57:28 -04:00
LemmyCook
40edc38756 NSectionEditor: Force text width for a more uniform look 2025-08-31 21:42:55 -04:00
LemmyCook
d1f5d301c2 Color animations: more uniform across NWidgets 2025-08-31 21:36:30 -04:00
LemmyCook
102aca0fa0 Settings: less wide + cleanup about 2025-08-31 21:35:41 -04:00
LemmyCook
b0917f5a25 Auto-formatting 2025-08-31 21:35:16 -04:00
Dillon Johnson
5488063490 disable test mode for battery. 2025-08-31 15:16:06 -10:00
Kevin Diaz
ad125d7af9 docs: update README for AUR package changes and improved installation structure
- Update AUR git package description to clarify it "pulls" rather than
"builds" commits
- Change manual installation path to
~/.config/quickshell/noctalia-shell/
- Move installation comments outside bash code blocks so users can
easily copy and paste
- Update AUR/Manual install commands to use "qs -c noctalia-shell ipc
call..." format
- Improve table formatting and command alignment for better readability
2025-08-31 21:12:28 -04:00
LemmyCook
4510762a35 Revert "Wallpaper: attempt to fix wallpaper bindings on Qt 6.8"
This reverts commit c7ee627110.
2025-08-31 18:00:30 -04:00
LemmyCook
c7ee627110 Wallpaper: attempt to fix wallpaper bindings on Qt 6.8 2025-08-31 17:55:08 -04:00
LemmyCook
330eac08cb Wallpaper: back to onClicked for Wallpaper selection 2025-08-31 15:55:12 -04:00
LemmyCook
bb1d56121d Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-08-31 15:45:32 -04:00
LemmyCook
3683d3c29b NPill: allow to open left or right depending on 2025-08-31 15:45:10 -04:00
Lemmy
0736862a2c Merge pull request #179 from lesi-nedo/main
Fix to issue #165
2025-08-31 15:19:38 -04:00
Oleksiy Nedobiychuk
3891c7008a fix(bluetooth): enable disconnect/remove for paired devices #165 2025-08-31 21:00:15 +02:00
Oleksiy Nedobiychuk
5c729b25b4 Merge remote-tracking branch 'upstream/main' 2025-08-31 20:53:26 +02:00
Oleksiy Nedobiychuk
3151b1634c fix to issue #165 2025-08-31 20:51:04 +02:00
LemmyCook
2498f0273d Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-08-31 14:44:24 -04:00
LemmyCook
40579e1b80 NIconButton: better disabled state 2025-08-31 14:44:22 -04:00
Ly-sec
2bd6d23467 Vesktop: small fix 2025-08-31 20:38:16 +02:00
Ly-sec
eb7401d693 Vesktop: add more styling 2025-08-31 20:21:39 +02:00
Ly-sec
0f5bbb961d Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-08-31 20:01:07 +02:00
Ly-sec
fa82dea4d5 Add Vesktop matugen template
vesktop: create matugen template (based on catppuccin)
2025-08-31 20:00:29 +02:00
Oleksiy Nedobiychuk
46ef2b6e53 Merge branch 'fix/ddcutil-hang' 2025-08-31 17:49:53 +02:00
Oleksiy Nedobiychuk
4a799b755f Merge branch 'fix/ddcutil-hang' of https://github.com/lesi-nedo/noctalia-shell into fix/ddcutil-hang 2025-08-31 17:21:55 +02:00
LemmyCook
dda031e73b NightLight: if using autoSchedule, wait for coordinates to be ready 2025-08-31 11:04:09 -04:00
Ly-sec
2f8472f720 Even more ArchUpdater fixes
ArchUpdaterService:properly check for errors
2025-08-31 16:56:52 +02:00
Ly-sec
4520ed3cbf Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-08-31 16:44:43 +02:00
Ly-sec
1ecc3d9744 Fix ArchUpdater to be able to use ghostty
ArchUpdaterService: add separate ghostty command
ArchUpdater: Change color of symbol if no terminal/aur helper is found,
edited tooltip
ArchUpdaterPanel: Add proper error message if TERMINAL or aur helper
was not found
2025-08-31 16:41:52 +02:00
LemmyCook
fcf627c30b BarHeight: more rounding uniformization 2025-08-31 10:36:40 -04:00
LemmyCook
fdf67ab512 ScreenCorners: use the same Math.round() for bar height so corners dont overlap semitransp bar 2025-08-31 10:34:13 -04:00
LemmyCook
b1daf2e8bc Location: Set stable name on load to the user specified name. Until we get a proper weather update 2025-08-31 10:29:25 -04:00
LemmyCook
6ecbdda121 Location: should fix edge case of location data being not ready on time 2025-08-31 10:24:01 -04:00
Ly-sec
3f0374e1f2 ArchUpdater: more selective update debug logs & tests 2025-08-31 15:50:23 +02:00
Ly-sec
cb345c2364 ArchUpdater: Even more debug logs 2025-08-31 15:39:44 +02:00
Ly-sec
d0b957e998 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-08-31 15:29:14 +02:00
Ly-sec
7a2fa4a773 Add debug logs to ArchUpdater 2025-08-31 15:28:54 +02:00
LemmyCook
8a198fd707 Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-08-31 09:21:12 -04:00
LemmyCook
7ba3870c82 SystemStats: no space before unit (to match the others stats) 2025-08-31 09:21:10 -04:00
Ly-sec
ff0c83a04c Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-08-31 15:20:13 +02:00
Ly-sec
9a8046b99f ArchUpdaterService: fix AUR helper detection 2025-08-31 15:19:58 +02:00
LemmyCook
92b37df962 Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-08-31 09:15:17 -04:00
LemmyCook
ab4359b624 ActiveWindo/MediaMini: slight width improvements 2025-08-31 09:15:16 -04:00
Ly-sec
ab5b877dc3 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-08-31 15:00:56 +02:00
Ly-sec
23a41ff3c6 Fix selective update from ArchUpdater
ArchUpdaterService: edit command builder
ArchUpdaterPanel: fix error state display
2025-08-31 15:00:00 +02:00
LemmyCook
68a44b6ef7 Taskbar: small tweaks for better compliance to codebase 2025-08-31 08:53:01 -04:00
Lemmy
51ea837cd0 Merge pull request #174 from JPratama7/feat/app-taskbar
feat: app taskbar
2025-08-31 08:49:26 -04:00
LemmyCook
6f2d5c2752 Tooltip: more lower case 2025-08-31 08:48:34 -04:00
LemmyCook
d912c2a090 ArchUpdate: last console.log 2025-08-31 08:48:07 -04:00
LemmyCook
21876857fc Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-08-31 08:41:54 -04:00
LemmyCook
53405c13af NText: Reverted my change to support Richtext by default + All tooltips are no longer using capital letter at the start of every word 2025-08-31 08:41:51 -04:00
Ly-sec
abb5f385d9 Disable Network stats by default
Settings: set showNetworkStats to false
2025-08-31 14:32:30 +02:00
Ly-sec
4ad851fdd2 Update About Tab to handle long names better
AboutTab: adapt cellWidth/height to accommodate longer names
2025-08-31 14:22:54 +02:00
Ly-sec
8509845381 Update README
README: remove old migration informations
2025-08-31 14:20:15 +02:00
Ly-sec
d7eea7fdae Remove useless debug logs from ArchUpdater
ArchUpdaterService: remove 4 useless debug logs
2025-08-31 13:58:29 +02:00
Ly-sec
8395b2640e Fix ArchUpdaterService error codes (once more)
ArchUpdaterService: Update yay error code (1 also means no updates
available just like in paru)
2025-08-31 13:56:01 +02:00
Ly-sec
1eae0eb3d4 Fix ArchUpdater error codes, revert TrayMenu
TrayMenu: reverted it to the old PopupPanel for ignored
ArchUpdater: paru error code 1 = no updates available
2025-08-31 13:47:06 +02:00
Ly-sec
91ffa4a9fd Reimplement the MediaMini and ActiveWindow fix
Revert ScreenCorner fix (didn't work at all)
2025-08-31 11:18:24 +02:00
Ly-sec
58b93c9d22 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-08-31 11:15:18 +02:00
Ly-sec
6d2f4d51a2 Revert "Possibly fixed #117, format and also change a tiny thing in ActiveWindow"
This reverts commit 1e52e7ca40.
2025-08-31 11:14:23 +02:00
Ly-sec
7b63b6900d Don't dim ScreenCorners
NPanel: add dimOverlay
2025-08-31 10:34:01 +02:00
Ly-sec
1e52e7ca40 Possibly fixed #117, format and also change a tiny thing in ActiveWindow
MediaMini: properly elide and manage width of MediaMini
ActiveWindow: Make it respect width of ActiveWindow title
2025-08-31 09:55:17 +02:00
Ly-sec
2ebdc74f15 Add network stats to SystemMonitor, fix ActiveWindow text display
SystemMonitor: add network up/down stats (also added setting to disable
it in BarTab)
ActiveWindow: add elide if not hovered
2025-08-31 09:22:33 +02:00
Ly-sec
724e55c37d Autoformat 2025-08-31 08:57:00 +02:00
Ly-sec
51f1923e22 Fix TrayMenu crash after display wake. Add checks if screen exists, else set scaling to 1.0
TrayMenu: Replace PopupPanel with NPanel (for better loading & to
prevent QS crash)
Overview, Background etc: add screen checks, if it doesnt exist set
scaling to 1.0
2025-08-31 08:55:20 +02:00
Ly-sec
714f6c058f Small changes for ArchUpdaterService
ArchUpdaterService: remove duplicate AUR helper check and remove any
pacman occurrence
2025-08-31 07:46:28 +02:00
Ly-sec
560f601190 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-08-31 07:35:13 +02:00
Ly-sec
6deb039906 Autoformat 2025-08-31 07:34:59 +02:00
Ly-sec
f19eaf689b Rework ArchUpdater logic, update UI
ArchUpdater: remove pacman poll fully and rely on paru/yay
ArchUpdaterPanel: Remove scrollbar, remove UI blocking
README: Add `TERMINAL` env var info (again), add DiscoCevapi as Donator
2025-08-31 07:33:03 +02:00
quadbyte
80f6570f04 NightLight/Bar: left click toggle, converted to NIconButton
+ Adapted some tooltip to the new richtext NText
2025-08-31 01:26:04 -04:00
JPratama7
d883096971 feat: add little radius 2025-08-31 11:35:24 +07:00
LemmyCook
87f9afbd85 NightLight: reworked settings, defined fade duration and simplified service. 2025-08-31 00:13:40 -04:00
JPratama7
9bb5241e49 refactor: adjust tooltip 2025-08-31 10:54:13 +07:00
JPratama7
65601cb855 refactor: adjust codestyle 2025-08-31 09:44:20 +07:00
Oleksiy
ef86570b24 removed an extra logger call 2025-08-31 01:36:03 +00:00
Oleksiy Nedobiychuk
821c262a93 remove an extra logger 2025-08-31 03:28:18 +02:00
Oleksiy Nedobiychuk
1c323675d1 fix freezing because of ddcutil 2025-08-31 03:16:42 +02:00
LemmyCook
2c9e675ba4 Volume/bar: removed wrong comment 2025-08-30 21:10:54 -04:00
LemmyCook
2d5bebb969 Volume/bar: Right click opens pwvucontrol. 2025-08-30 21:08:32 -04:00
LemmyCook
a97913fd63 MediaMini: added RMB/MMB to control Next/Previous media/song 2025-08-30 15:59:26 -04:00
JPratama7
9264306a36 refactor: remove debug 2025-08-31 00:22:35 +07:00
Jose Chasey Pratama
d2ac174427 Merge branch 'noctalia-dev:main' into local 2025-08-31 00:20:29 +07:00
JPratama7
d5a8a0d72f refactor: add to registry 2025-08-31 00:20:13 +07:00
JPratama7
36bfbe10ab feat: taskbar 2025-08-31 00:20:01 +07:00
LemmyCook
7ace02dd46 BTService: add percent symbol (%) after battery level 2025-08-30 12:44:30 -04:00
LemmyCook
125a3ace08 Wallpaper: made the selection more responsive to clicks + code cleanup 2025-08-30 12:19:38 -04:00
LemmyCook
3c7d03ada9 Wallpaper: added a bash script to compile all shaders
+ code cleanup
2025-08-30 11:22:09 -04:00
LemmyCook
477d38d928 Wallpaper: shaders improvements with more parameters and new Stripes shader 2025-08-30 10:43:33 -04:00
LemmyCook
d36bcb1d4d Autoformatting 2025-08-30 07:58:30 -04:00
Lysec
4c79999a65 Merge pull request #167 from MarkusVolk/main
Add matugen templates for foot an fuzzel
2025-08-30 04:02:51 +02:00
Ly-sec
cdfed0fe94 Replace pkexec with terminal output (with TERMINAL environment var)
ArchUpdater:use terminal thanks to `TERMINAL` environment variable
README: Add explanation for said environment var
2025-08-30 03:57:59 +02:00
LemmyCook
6af915983c Wallpaper: flush nextWallpaper.source when no longer needed in a attempt to save ram 2025-08-29 21:52:16 -04:00
LemmyCook
da266792df Wallpaper: less login 2025-08-29 21:40:43 -04:00
LemmyCook
91afdf7f13 Wallpaper: added disc transition 2025-08-29 21:40:20 -04:00
LemmyCook
26fc6098dc Wallpaper: added random transition + fixed "none" transition 2025-08-29 21:19:17 -04:00
LemmyCook
3496169c68 Revert "Remove need for polkit, launch any ArchUpdater update through terminal"
This reverts commit 299add4a15.
2025-08-29 20:50:28 -04:00
Ly-sec
299add4a15 Remove need for polkit, launch any ArchUpdater update through terminal
ArchUpdater: rely on `TERMINAL` environment variable
README: Add explanation for the `TERMINAL` environment variable
2025-08-30 02:28:48 +02:00
LemmyCook
5ab76c98e5 wallpaper: renamed Swipe => Wipe 2025-08-29 19:10:16 -04:00
LemmyCook
f5b4984295 Wallpaper: swipe left/right/up/down 2025-08-29 19:06:01 -04:00
Lemmy
a38665fa0d Update README.md - screenshots served by github repo 2025-08-29 17:12:19 -04:00
LemmyCook
cf27ff10c0 Github: Added GitHub screenshots 2025-08-29 17:05:01 -04:00
LemmyCook
8f3f520ef4 Merge branch 'advanced-wallpaper' 2025-08-29 17:02:17 -04:00
LemmyCook
c4e4f78336 Wallpaper/Matugen: Matugen always based on the primary screen wallpaper 2025-08-29 17:00:58 -04:00
LemmyCook
2f9eb28596 Wallpaper: On startup set wallpaper without transition 2025-08-29 16:53:43 -04:00
LemmyCook
63e90a5c17 Wallpaper: cool fade in transition via shader 2025-08-29 16:26:48 -04:00
LemmyCook
61d13a6cab Wallpaper: minor fixes for random wallpaper picking 2025-08-29 15:21:10 -04:00
LemmyCook
a2ecc67643 Wallpaper: less intrusive UI when using per monitor directories 2025-08-29 14:57:03 -04:00
LemmyCook
f679999453 Wallpaper: fixed random wallpaper 2025-08-29 14:44:20 -04:00
LemmyCook
5b8d7dbff5 Wallpaper: fixed all edge cases when toggling on/off multi directories support and invalid directory names 2025-08-29 14:38:27 -04:00
LemmyCook
9bbdf5f6f6 Wallpaper: real support for differents folders per monitor \o/ 2025-08-29 14:09:05 -04:00
LemmyCook
812ddf2ebb WallpaperSelector: syntax fix 2025-08-29 13:11:31 -04:00
LemmyCook
db3ea7ed73 Wallpaper: cleanup 2025-08-29 13:04:11 -04:00
LemmyCook
c37ef867a1 Wallpaper: delay service initialization until settings are ready 2025-08-29 12:41:37 -04:00
LemmyCook
7c6c908076 Logger: new callStack() method 2025-08-29 12:40:19 -04:00
Markus Volk
c510afdc28 Add fuzzel matugen template
Signed-off-by: Markus Volk <f_l_k@t-online.de>
2025-08-29 17:57:47 +02:00
LemmyCook
861e207fb6 Wip! 2025-08-29 09:55:47 -04:00
Markus Volk
c601e45436 Add foot matugen template
Signed-off-by: Markus Volk <f_l_k@t-online.de>
2025-08-29 15:41:23 +02:00
LemmyCook
e79c163dd9 Wallpaper rework
- removed swww to the code is easier to maintain
- basic multi monitor wallpaper support
2025-08-29 08:33:40 -04:00
Lysec
c770b97649 Merge pull request #161 from MichaelThomas0721/main
Added ghostty matugen template
2025-08-29 08:20:43 +02:00
Oleksiy Nedobiychuk
5dedf5c1b5 brightness: avoid DDC on internal panels, add timeouts, auto-blacklist bad DDC buses
Signed-off-by: Oleksiy Nedobiychuk <oleksiy12345@live.it>
2025-08-29 00:43:52 +02:00
Michael Thomas
85bd0ed2f8 Merge branch 'noctalia-dev:main' into main 2025-08-28 16:45:50 -04:00
MichaelThomas0721
cd6a183c28 Added ghostty matugen template 2025-08-28 16:39:56 -04:00
LemmyCook
3cc8c8fb03 ArchUpdater: improved the look 2025-08-28 15:55:03 -04:00
LemmyCook
42408572ab Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-08-28 15:01:45 -04:00
LemmyCook
c3956c5894 Bluetooth: revamped a lot of code 2025-08-28 15:01:43 -04:00
LemmyCook
b2e9058a2f Auto-formatting 2025-08-28 15:01:23 -04:00
Lemmy
bc28b11763 Update README.md 2025-08-28 14:33:03 -04:00
Ly-sec
cbd71bec49 Fix ArchUpdater NCheckbox binding
ArchUpdater: Create proper binding, make selective update more robust
2025-08-28 19:48:20 +02:00
Ly-sec
6ac172fe02 ArchUpdaterPanel: Fix typo 2025-08-28 19:17:50 +02:00
LemmyCook
8ebcfa4bc6 Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-08-28 12:22:43 -04:00
LemmyCook
3f4cec1719 NTextInput: improved layout and adapted calling code all over the shell. 2025-08-28 12:22:42 -04:00
Ly-sec
156146fd9a Add audio IPC options
AudioService: add a few functions to AudioService
IPCManager: Add 4 Audio IPC calls
README: Add information about new IPC calls
2025-08-28 17:48:02 +02:00
LemmyCook
e86e7344f3 ArchUpdater: better icons (take2) 2025-08-28 11:10:55 -04:00
Ly-sec
8d9f206c45 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-08-28 17:08:01 +02:00
Ly-sec
39d8d8bcfa Added a check to see if wlsunset is enabled, if it isn't you can change
the NightLight settings.
DisplayTab: add wlsunsetCheck process
2025-08-28 17:06:53 +02:00
LemmyCook
a719db4d0d better comments 2025-08-28 11:06:31 -04:00
Ly-sec
a845067cf0 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-08-28 17:04:34 +02:00
Ly-sec
82d71d65fa Replaced the old checkboxes in ArchUpdaterPanel with NCheckbox
ArchUpdater: use NCheckbox to make things more uniform
2025-08-28 17:03:28 +02:00
Lysec
a699cfb958 Merge pull request #162 from wer-zen/main
Another Readme Fix
2025-08-28 17:01:55 +02:00
wer-zen
92b24c6eb2 readme_fix4 2025-08-28 16:59:36 +02:00
wer-zen
d57092feae readme_fix3 2025-08-28 16:54:08 +02:00
wer-zen
6c4b495a75 readme_fix3 2025-08-28 16:53:59 +02:00
Ly-sec
d0b7ccf302 Autoformat 2025-08-28 15:35:52 +02:00
Ly-sec
e237bd04ff Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-08-28 15:35:37 +02:00
Ly-sec
2a686b55c4 Replace our NightLight solution with wlsunset.
NightLight: add temperature solution
NTextInput: add input hint support
2025-08-28 15:34:47 +02:00
LemmyCook
cdc3b18071 ArchUpdater: better icons 2025-08-28 08:58:24 -04:00
quadbyte
c8860a3a9d Volume/Bar: better touchpad support for volume inc/dec 2025-08-28 08:37:44 -04:00
Ly-sec
57a67bf4df Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-08-28 14:26:29 +02:00
Ly-sec
f932d580af NCheckbox: add scaling 2025-08-28 14:26:13 +02:00
LemmyCook
eadcb3f22b ColorSchemeTab: better presentation 2025-08-28 08:25:43 -04:00
LemmyCook
a6d722f9a9 LocationService + Settings: improved service stability and show geocoding results in the settings 2025-08-28 08:20:17 -04:00
Ly-sec
f10280c8bb Added NCheckbox and used it for Matugen templates
NCheckbox: Added
ColorSchemeTab: replace NToggle with NCheckbox
2025-08-28 14:00:29 +02:00
Ly-sec
a6848be4c2 Create MatugenService, add toggles per template
Matugen: Created Matugen.qml for users to add templates to, add
MatugenService to generate .toml
Notification: possible fix for children null warning
Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell
2025-08-28 13:33:24 +02:00
Ly-sec
f510c1922d Create separate matugen toggles, add MatugenService
Matugen: add Matugen.qml as central place for templates, add
MatugenService to take care of .toml generation
Notification: possible fix for "children of null"
2025-08-28 13:27:49 +02:00
LemmyCook
0562dbbbf9 Settings: more cleanup and conditionnal controls (NightLight)
+ Auto formatting
2025-08-28 06:57:37 -04:00
Ly-sec
85b92d9c6f Added a check if there is any notifications.
Notification: add notificationModel.count check to possibly prevent
unwanted behaviour
2025-08-28 10:51:18 +02:00
Lysec
de465ebcba Merge pull request #158 from MichaelThomas0721/main
Added kitty matugen template
2025-08-28 10:34:40 +02:00
LemmyCook
8302285388 Settings: large cleanup and factorization. Should look much better. 2025-08-27 20:39:50 -04:00
MichaelThomas0721
b502161b11 Added kitty matugen template 2025-08-27 19:22:16 -04:00
LemmyCook
1206be34dc MediaMini: fixed fallback icon 2025-08-27 18:58:21 -04:00
LemmyCook
c6cf5a0fab Bar UI improvements
- better rounding at low scaling, for accurate vertical centering
- use fixed font bar system monitor
- use bold for workspaces name
2025-08-27 14:46:19 -04:00
LemmyCook
d6df496216 ArchUpdater: fixes (part2) 2025-08-27 14:42:15 -04:00
LemmyCook
68874e8680 ArchUpdater: fixes
- replaced all Text by NText for a more streamlined code
- clicking on the icon in the bar should always open the panel even if
there is nothing to update
2025-08-27 14:41:39 -04:00
LemmyCook
67f0c482b3 ActiveWindow+MediaMini: minor adjustments to width and spacing.
- also removed double fallback icon for media mini when no artwork
available
2025-08-27 10:23:43 -04:00
LemmyCook
1ceec16102 MediaCard: AudioVisualizer should not be dependend on tracklength (ex: Twitch) 2025-08-27 10:11:34 -04:00
LemmyCook
9d03006aaa autoformatting 2025-08-27 09:07:36 -04:00
Ly-sec
f7f21f9716 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-08-27 14:54:12 +02:00
Ly-sec
124d9becc6 Add animation speed slider in general tab, always collapse activeWindow
GeneralTab: add animation speed slider
Workspace: set activeWindow to always collapsed except for hover
Misc: replaced a lot of animations with Style.animationXYZ
2025-08-27 14:52:50 +02:00
LemmyCook
50777ef32f MediaPlayer/sidepanel: slightly less thick time slider 2025-08-27 08:51:43 -04:00
Ly-sec
563a151277 Possible fix for MediaCard slider
MediaCard: use proper seek binding
MediaService: add seek binding
autoformat
2025-08-27 14:21:42 +02:00
Ly-sec
6f7528c87a Added issue templates and fixed screenRecorder status symbol
ScreenRecorder: add proper checks for screenRecorder
ISSUE_TEMPLATE: add bug_report and feature_request
2025-08-27 13:21:53 +02:00
Ly-sec
ae0228dc25 Wallpaper: change random wallpaper delay options 2025-08-27 09:54:46 +02:00
Ly-sec
a1f87c50bc ArchUpdater: add AUR support 2025-08-27 09:28:58 +02:00
Ly-sec
74e65d75cb README: add noctalia-shell-git AUR info 2025-08-27 08:47:28 +02:00
Ly-sec
56967d4c0c Compositor: Fix Hyprland activeWindow icon 2025-08-27 08:23:45 +02:00
Ly-sec
2950862e34 README: update Usage section 2025-08-27 08:14:34 +02:00
LemmyCook
f4ecdd5af3 Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-08-26 20:55:47 -04:00
LemmyCook
dd456edf90 Workspace: ShowLabel replaced toggle by NComboBox so we can choose "Name" or "Index" 2025-08-26 20:55:45 -04:00
Lemmy
e1f1addb35 Merge pull request #152 from Drazzy9295/main
small flake.nix fix - providing a proper 'default' and fixing formatter errors
2025-08-26 18:58:20 -04:00
LemmyCook
94e59592f0 Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-08-26 18:49:10 -04:00
LemmyCook
4cd94f0426 NightLight: refactored the code to make simpler
- using intensity instead of warmth
- animated color transition
- removed unecessary bindings and double properties
- using better icons to avoid confusion with brightness
- polished settings UI
2025-08-26 18:48:10 -04:00
Drazzy9295
c99f470e34 small flake.nix fix 2025-08-26 22:27:51 +01:00
Ly-sec
45b0fbeb4a Release: v2.3.0
- add NightLight
- better positioning of panels below their widgets
- quality of life changes/fixes
2025-08-26 20:42:01 +02:00
LemmyCook
76f0368a64 Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-08-26 14:41:29 -04:00
LemmyCook
da80f47921 MediaCard: less thick ouline 2025-08-26 14:41:27 -04:00
Ly-sec
fa7c19d5de README: add small blockquote to optional dependencies 2025-08-26 20:31:59 +02:00
Lysec
74f54b3d76 Merge pull request #148 from kevindiaz314/main
docs: Update README with AUR and Nix instructions
2025-08-26 20:27:10 +02:00
LemmyCook
2f49643e51 NIconButton + NPill: improved vertical centering 2025-08-26 14:25:36 -04:00
LemmyCook
fabdf67da7 Workspace: use font metrics for vertical centering
Another attempt xD
2025-08-26 14:18:38 -04:00
LemmyCook
fc71f61000 Workspace: another attempt at proper text centering 2025-08-26 14:09:54 -04:00
Ly-sec
aa8a72a9d8 Fix named workspace text positioning
Workspace.qml: add slight centerOffset
2025-08-26 20:05:45 +02:00
LemmyCook
a1dcaa2683 Workspace: attempt to fix tiny vertical offset 2025-08-26 13:53:27 -04:00
LemmyCook
f533e2a547 Workspace: bigger text for names, adaptative height. 2025-08-26 13:45:53 -04:00
Kevin Diaz
496ca05b9d Update README.md to move optional packages to required 2025-08-26 13:41:50 -04:00
Kevin Diaz
f399463a99 Update installation instructions for Arch Linux and Nix in README.md 2025-08-26 13:41:50 -04:00
LemmyCook
44c98553dc Workspace: slimmer look 2025-08-26 13:40:35 -04:00
Lysec
57a8f5f0b3 Update README.md 2025-08-26 18:38:47 +02:00
LemmyCook
ab7c5678c6 Workspace: respect the setting 2025-08-26 12:37:46 -04:00
Ly-sec
c323985f03 Small workspace indicator fix 2025-08-26 18:33:28 +02:00
LemmyCook
024f35496c Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-08-26 12:21:58 -04:00
Lysec
4d91096ab9 Update README.md 2025-08-26 18:21:53 +02:00
LemmyCook
8148c0fa29 Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-08-26 12:21:49 -04:00
LemmyCook
620b3e3abc Named workspaces improvements
- renamed settings to showWorkspacesNames (plural)
- improved overall look and readability
2025-08-26 12:21:48 -04:00
LemmyCook
22af8e91cc Autoformatting 2025-08-26 12:20:27 -04:00
Ly-sec
9dcefa4357 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-08-26 18:19:41 +02:00
Ly-sec
634d78456d Add NightLight, update README, format 2025-08-26 18:19:35 +02:00
Lemmy
8140ddc2ff Merge pull request #136 from MichaelThomas0721/main
Added setting for workspace names.
Thanks for your contribution.  I'll most likely rework it a tiny bit to make it more aesthetic later today.
2025-08-26 12:04:01 -04:00
LemmyCook
71cfbc8c0a Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-08-26 10:26:04 -04:00
LemmyCook
6a9dee38ef NPanel fixes 2025-08-26 10:26:02 -04:00
Ly-sec
9ee31e3a6a Add IPC for screenRecorder toggle 2025-08-26 15:48:41 +02:00
Lysec
7379bcb5b6 Merge pull request #147 from wer-zen/main
Nix Part for README
2025-08-26 15:38:40 +02:00
wer-zen
246c475dbe Added Usage for binary and non 2025-08-26 15:36:11 +02:00
wer-zen
864631f967 Fixed qs ipc call stuf 2025-08-26 15:31:02 +02:00
wer-zen
4fe5681917 README.md 2025-08-26 15:13:37 +02:00
wer-zen
fe8e5a0464 README.md 2025-08-26 15:12:42 +02:00
wer-zen
7c914b88a3 README.md 2025-08-26 15:09:19 +02:00
wer-zen
256cd06e5c README.md 2025-08-26 15:08:11 +02:00
LemmyCook
f3ae0101d7 NPanel now can properly be positioned relative to their opener (button) 2025-08-26 08:55:29 -04:00
wer-zen
52efc632f8 README.md 2025-08-26 14:34:01 +02:00
Ly-sec
f3f0f611cb Add AppLauncher opacity, topCenter & bottomCenter 2025-08-26 14:31:59 +02:00
zen
07fcd29842 Update README.md 2025-08-26 14:25:35 +02:00
wer-zen
863107670c README.md 2025-08-26 14:23:02 +02:00
wer-zen
c2ca05b117 flake.nix update 2025-08-26 13:51:12 +02:00
Ly-sec
3c39ea192b Format 2025-08-26 13:12:01 +02:00
Ly-sec
1533b2d3a1 Add MPRIS blacklist 2025-08-26 13:11:49 +02:00
Ly-sec
7bcb227d7b Remove kitty template for now 2025-08-26 12:36:43 +02:00
wer-zen
178ad2ac8a flake.nix update 2025-08-26 12:12:53 +02:00
quadbyte
bdd981e15c Bar Brightness: fixed onClicked to open brightness settings 2025-08-25 23:18:13 -04:00
LemmyCook
a61526543d Settings / Display-Scaling tab: improved display at low scaling + fixed refresh button look 2025-08-25 22:48:18 -04:00
LemmyCook
1ab3463e6d Widgets: renamed SizeMultiplier => SizeRatio. Enforced read-only on size property 2025-08-25 22:43:02 -04:00
LemmyCook
269b2765cd More optims and renaming 2025-08-25 22:17:13 -04:00
LemmyCook
d2563db5a0 OPtimization: Notification History only loaded when necessary 2025-08-25 21:54:03 -04:00
LemmyCook
fcedb65119 Optimization: Dock get loaded only on assigned screens instead of being invisble. 2025-08-25 21:42:27 -04:00
LemmyCook
18b79913bd Settings / Brightness: removed non existing setting/toggle since we moved to modular bar 2025-08-25 21:40:00 -04:00
LemmyCook
9fb4aff635 Optimizations memory/cpu
- Only load bar widgets once the settings are done loading, and the
widget is actually in use.
- Only load bar on screens that request it, instead of hiding it.
2025-08-25 21:18:49 -04:00
LemmyCook
38efdc8f36 Settings: dont add ArchUpdater to the bar by default. 2025-08-25 19:03:21 -04:00
LemmyCook
75700e3309 Avoid one extra Loader per bar widget 2025-08-25 18:33:44 -04:00
LemmyCook
48e57a2122 autoformating 2025-08-25 18:33:25 -04:00
Lysec
d791705afa Merge pull request #129 from ThatOneCalculator/fix/heuristic-lookup
fix: use heuristicLookup for desktop entries if available
2025-08-25 23:31:24 +02:00
Kainoa Kanter
38e3d9909f Merge branch 'noctalia-dev:main' into fix/heuristic-lookup 2025-08-25 14:26:35 -07:00
LemmyCook
2a234f5a88 Bar Clock: Improved current date display. 2025-08-25 15:39:29 -04:00
LemmyCook
5f00266df7 WifiPanel: Improved look and functionalities 2025-08-25 15:28:13 -04:00
MichaelThomas0721
c917d7dccb Added setting for workspace names 2025-08-25 13:47:54 -04:00
LemmyCook
54c39ab8a3 TrayMenu: fixed bottom menu margin 2025-08-25 13:47:02 -04:00
LemmyCook
b19fb316d9 ArchUpdater: fixed CPU hogging 2025-08-25 13:44:23 -04:00
LemmyCook
7a849806fb Minor cleanup 2025-08-25 08:28:27 -04:00
Ly-sec
01ccb771e6 Possible fix for battery % notification 2025-08-25 13:13:56 +02:00
Ly-sec
c6683712a4 Add notification when battery is low, fix some warnings 2025-08-25 12:46:55 +02:00
Kainoa Kanter
81182aa65b Merge branch 'noctalia-dev:main' into fix/heuristic-lookup 2025-08-24 21:34:47 -07:00
Kainoa Kanter
487158739d Merge branch 'noctalia-dev:main' into fix/heuristic-lookup 2025-08-22 12:37:26 -07:00
Kainoa Kanter
7899b124b7 fix: check for correct method 2025-08-22 10:57:37 -07:00
Kainoa Kanter
e1d623be9c fix: use heuristicLookup for desktop entries if available 2025-08-22 09:04:48 -07:00
126 changed files with 8926 additions and 4742 deletions

29
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,29 @@
---
name: Bug Report
about: Report a bug from noctalia-shell
title: "[Bug]: "
labels: bug
assignees: ''
---
### Description
A clear and concise description of the bug.
### Steps to Reproduce
1. Go to '...'
2. Click on '...'
3. See the error.
### Expected Behavior
Explain what you expected to happen.
### Screenshots
Add screenshots if applicable.
### Environment
- Distro [e.g., CachyOS, NixOS, Arch, ...]
- Compositor [ e.g., Hyprland, Niri, ...]
- Version: [e.g., 1.0.0 or `main`]
### Additional Context
Add any other context about the problem here.

12
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
blank_issues_enabled: false
issue_templates:
- name: "Bug Report"
description: "Report a bug in the system."
title: "[Bug]: "
labels: ["bug"]
body: "./ISSUE_TEMPLATE/bug_report.md"
- name: "Feature Request"
description: "Propose a new feature or improvement."
title: "[Feature]: "
labels: ["enhancement"]
body: "./ISSUE_TEMPLATE/feature_request.md"

View File

@@ -0,0 +1,19 @@
---
name: Feature Request
about: Suggest a new feature or improvement
title: "[Feature]: "
labels: enhancement
assignees: ''
---
### Feature Description
What feature would you like to see?
### Why Is This Needed?
Explain the problem or need for this feature.
### Suggested Solutions
Describe how this feature could be implemented.
### Additional Context
Add any relevant screenshots, links, or resources.

View File

@@ -0,0 +1,79 @@
pragma Singleton
import QtQuick
import Quickshell
import qs.Commons
// Central place to define which templates we generate and where they write.
// Users can extend it by dropping additional templates into:
// - Assets/Matugen/templates/
// - ~/.config/matugen/ (when enableUserTemplates is true)
Singleton {
id: root
// Build the base TOML using current settings
function buildConfigToml() {
var lines = []
lines.push("[config]")
// Always include noctalia colors output for the shell
lines.push("[templates.noctalia]")
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/noctalia.json"')
lines.push('output_path = "' + Settings.configDir + 'colors.json"')
if (Settings.data.matugen.gtk4) {
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"')
}
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"')
}
if (Settings.data.matugen.qt6) {
lines.push("\n[templates.qt6]")
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/qtct.conf"')
lines.push('output_path = "~/.config/qt6ct/colors/noctalia.conf"')
}
if (Settings.data.matugen.qt5) {
lines.push("\n[templates.qt5]")
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/qtct.conf"')
lines.push('output_path = "~/.config/qt5ct/colors/noctalia.conf"')
}
if (Settings.data.matugen.kitty) {
lines.push("\n[templates.kitty]")
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/kitty.conf"')
lines.push('output_path = "~/.config/kitty/themes/noctalia.conf"')
lines.push("post_hook = 'kitty +kitten themes --reload-in=all noctalia'")
}
if (Settings.data.matugen.ghostty) {
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\"")
}
if (Settings.data.matugen.foot) {
lines.push("\n[templates.foot]")
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/foot.conf"')
lines.push('output_path = "~/.config/foot/themes/noctalia"')
lines.push(
'post_hook = "sed -i /themes/d ~/.config/foot/foot.ini && echo include=~/.config/foot/themes/noctalia >> ~/.config/foot/foot.ini"')
}
if (Settings.data.matugen.fuzzel) {
lines.push("\n[templates.fuzzel]")
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/fuzzel.conf"')
lines.push('output_path = "~/.config/fuzzel/themes/noctalia"')
lines.push(
'post_hook = "sed -i /themes/d ~/.config/fuzzel/fuzzel.ini && echo include=~/.config/fuzzel/themes/noctalia >> ~/.config/fuzzel/fuzzel.ini"')
}
if (Settings.data.matugen.vesktop) {
lines.push("\n[templates.vesktop]")
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/vesktop.css"')
lines.push('output_path = "~/.config/vesktop/themes/noctalia.theme.css"')
}
return lines.join("\n") + "\n"
}
}

View File

@@ -1,6 +0,0 @@
# Base: only write Noctalia colors.json for the shell
[config]
[templates.noctalia]
input_path = "templates/noctalia.json"
output_path = "~/.config/noctalia/colors.json"

View File

@@ -1,30 +0,0 @@
# This file configures how matugen generates colors from wallpapers for Noctalia
[config]
[templates.noctalia]
input_path = "templates/noctalia.json"
output_path = "~/.config/noctalia/colors.json"
# GTK 4 (libadwaita) variables override
[templates.gtk4]
input_path = "templates/gtk4.css"
output_path = "~/.config/gtk-4.0/gtk.css"
# GTK 3 named-colors fallback for legacy apps
[templates.gtk3]
input_path = "templates/gtk3.css"
output_path = "~/.config/gtk-3.0/gtk.css"
# Qt6ct color scheme (can also be used by qt5ct in many distros)
[templates.qt6]
input_path = "templates/qtct.conf"
output_path = "~/.config/qt6ct/colors/noctalia.conf"
[templates.qt5]
input_path = "templates/qtct.conf"
output_path = "~/.config/qt5ct/colors/noctalia.conf"
[templates.kitty]
input_path = "templates/kitty.conf"
output_path = "~/.config/kitty/noctalia.conf"

View File

@@ -0,0 +1,30 @@
[colors]
background={{ colors.background.default.hex_stripped }}
foreground={{ colors.on_surface.default.hex_stripped }}
regular0={{ colors.surface.default.hex_stripped }}
regular1={{ colors.error.default.hex_stripped }}
regular2={{ colors.primary.default.hex_stripped }}
regular3={{ colors.tertiary.default.hex_stripped }}
regular4={{ colors.on_primary_container.default.hex_stripped }}
regular5={{ colors.on_secondary_container.default.hex_stripped }}
regular6={{ colors.secondary.default.hex_stripped }}
regular7={{ colors.on_surface.default.hex_stripped }}
bright0={{ colors.surface_bright.default.hex_stripped }}
bright1={{ colors.error.default.hex_stripped }}
bright2={{ colors.primary.default.hex_stripped }}
bright3={{ colors.tertiary.default.hex_stripped }}
bright4={{ colors.on_primary_container.default.hex_stripped }}
bright5={{ colors.on_secondary_container.default.hex_stripped }}
bright6={{ colors.secondary.default.hex_stripped }}
bright7={{ colors.on_surface.default.hex_stripped }}
dim0=45475A
dim1=F38BA8
dim2=A6E3A1
dim3=F9E2AF
dim4=89B4FA
dim5=F5C2E7
dim6=94E2D5
dim7=BAC2DE
selection-foreground={{ colors.primary.default.hex_stripped }}
selection-background={{ colors.on_primary.default.hex_stripped }}
cursor={{ colors.surface_variant.default.hex_stripped }} {{ colors.on_surface.default.hex_stripped }}

View File

@@ -0,0 +1,15 @@
# Fuzzel Colors
# Generated with Matugen
[colors]
background={{colors.background.default.hex_stripped}}CC
text={{colors.on_surface.default.hex_stripped}}ff
prompt={{colors.secondary.default.hex_stripped}}ff
placeholder={{colors.tertiary.default.hex_stripped}}ff
input={{colors.primary.default.hex_stripped}}ff
match={{colors.tertiary.default.hex_stripped}}ff
selection={{colors.primary.default.hex_stripped}}80
selection-text={{colors.on_surface.default.hex_stripped}}ff
selection-match={{colors.on_primary.default.hex_stripped}}ff
counter={{colors.secondary.default.hex_stripped}}ff
border={{colors.primary.default.hex_stripped}}ff

View File

@@ -0,0 +1,23 @@
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}}
cursor-color = {{colors.primary.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}}

View File

@@ -1,23 +1,25 @@
background {{colors.surface.default.hex}}
foreground {{colors.on_surface.default.hex}}
cursor_color {{colors.primary.default.hex}}
selection_background {{colors.surface_container.default.hex}}
selection_foreground {{colors.on_surface.default.hex}}
url_color {{colors.primary.default.hex}}
color0 {{colors.surface.default.hex}}
color1 {{colors.error.default.hex}}
color2 {{colors.tertiary.default.hex}}
color3 {{colors.secondary.default.hex}}
color4 {{colors.primary.default.hex}}
color5 {{colors.surface_container_highest.default.hex}}
color5 {{colors.primary.default.hex}}
color6 {{colors.secondary.default.hex}}
color7 {{colors.on_background.default.hex}}
color8 {{colors.outline.default.hex}}
color9 {{colors.error_container.default.hex}}
color9 {{colors.secondary_fixed_dim.default.hex}}
color10 {{colors.tertiary_container.default.hex}}
color11 {{colors.surface_container.default.hex}}
color12 {{colors.primary_container.default.hex}}
color13 {{colors.on_primary_container.default.hex}}
color14 {{colors.surface_variant.default.hex}}
color15 {{colors.on_background.default.hex}}
cursor {{colors.primary.default.hex}}
cursor_text_color {{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}}
url_color {{colors.primary.default.hex}}

View File

@@ -0,0 +1,572 @@
/*
* Vesktop Theme
* Generated with Matugen
* Base was taken from https://github.com/catppuccin/discord <3
*/
/* Dark Theme */
.visual-refresh.theme-dark,
.visual-refresh .theme-dark {
/* Brand Colors */
--brand-experiment: {{colors.primary.default.hex}};
--bg-brand: {{colors.primary.default.hex}};
--brand-500: {{colors.primary.default.hex}} !important;
--text-link: {{colors.primary.default.hex}} !important;
--text-brand: {{colors.primary.default.hex}};
--control-brand-foreground: {{colors.primary.default.hex}};
--control-brand-foreground-new: {{colors.primary.default.hex}};
--mention-foreground: {{colors.primary.default.hex}};
--mention-background: {{colors.primary.default.hex}}20;
--focus-primary: {{colors.primary.default.hex}};
--logo-primary: {{colors.on_surface.default.hex}};
--badge-brand-bg: {{colors.primary.default.hex}};
--badge-brand-text: {{colors.on_primary.default.hex}};
/* Text Colors */
--header-primary: {{colors.on_surface.default.hex}} !important;
--header-secondary: {{colors.on_surface_variant.default.hex}} !important;
--text-normal: {{colors.on_surface.default.hex}} !important;
--text-default: {{colors.on_surface.default.hex}};
--text-muted: {{colors.on_surface_variant.default.hex}} !important;
--text-primary: {{colors.on_surface.default.hex}};
--text-secondary: {{colors.on_surface_variant.default.hex}};
--text-tertiary: {{colors.on_surface_variant.default.hex}} !important;
--interactive-normal: {{colors.on_surface.default.hex}} !important;
--interactive-muted: {{colors.on_surface_variant.default.hex}};
--interactive-hover: {{colors.on_surface.default.hex}};
--interactive-active: {{colors.on_surface.default.hex}};
/* Main Background Colors - Bar color (mSurface) colors.surface.default.hex*/
--background-primary: {{colors.surface_variant.default.hex}} !important;
--background-floating: {{colors.surface_variant.default.hex}} !important;
--background-surface-high: {{colors.surface_variant.default.hex}} !important;
--modal-background: {{colors.surface_variant.default.hex}} !important;
--app-background-frame: {{colors.surface_variant.default.hex}} !important;
--home-background: {{colors.surface_variant.default.hex}} !important;
--chat-background: {{colors.surface_variant.default.hex}} !important;
--chat-background-default: {{colors.surface_variant.default.hex}} !important;
--chat-input-container-background: {{colors.surface_container.default.hex}} !important;
/* Secondary Background Colors - Workspace color (mSurfaceVariant) */
--background-secondary: {{colors.surface.default.hex}} !important;
--background-secondary-alt: {{colors.surface.default.hex}} !important;
--background-surface-higher: {{colors.surface.default.hex}} !important;
--background-base-low: {{colors.surface.default.hex}} !important;
--background-base-lower: {{colors.surface.default.hex}} !important;
--channeltextarea-background: {{colors.surface_container.default.hex}} !important;
--modal-footer-background: {{colors.surface.default.hex}} !important;
/* New Messages Banner */
--background-mentioned: {{colors.primary.default.hex}}15 !important;
--background-mentioned-hover: {{colors.primary.default.hex}}20 !important;
--text-mentioned: {{colors.on_surface.default.hex}} !important;
--text-mentioned-hover: {{colors.on_surface.default.hex}} !important;
--text-mentioned-link: {{colors.primary.default.hex}} !important;
/* Additional Discord-specific variables for new messages banner */
--background-message-automod: {{colors.primary.default.hex}}15 !important;
--background-message-automod-hover: {{colors.primary.default.hex}}20 !important;
--background-message-highlight: {{colors.primary.default.hex}}15 !important;
--background-message-highlight-hover: {{colors.primary.default.hex}}20 !important;
/* Discord unread messages banner specific variables */
--background-mentioned: {{colors.primary.default.hex}}15 !important;
--background-mentioned-hover: {{colors.primary.default.hex}}20 !important;
--text-mentioned: {{colors.on_surface.default.hex}} !important;
--text-mentioned-hover: {{colors.on_surface.default.hex}} !important;
--text-mentioned-link: {{colors.primary.default.hex}} !important;
/* Additional Discord banner text variables */
--text-normal: {{colors.on_surface.default.hex}} !important;
--text-default: {{colors.on_surface.default.hex}} !important;
--text-primary: {{colors.on_surface.default.hex}} !important;
--text-secondary: {{colors.on_surface_variant.default.hex}} !important;
--text-tertiary: {{colors.on_surface_variant.default.hex}} !important;
--text-muted: {{colors.on_surface_variant.default.hex}} !important;
--interactive-normal: {{colors.on_surface.default.hex}} !important;
--interactive-muted: {{colors.on_surface_variant.default.hex}} !important;
/* Additional Discord banner variables */
--background-message-automod: {{colors.primary.default.hex}}15 !important;
--background-message-automod-hover: {{colors.primary.default.hex}}20 !important;
--background-message-highlight: {{colors.primary.default.hex}}15 !important;
--background-message-highlight-hover: {{colors.primary.default.hex}}20 !important;
--background-message-hover: {{colors.surface_variant.default.hex}}50 !important;
--background-modifier-hover: {{colors.surface_variant.default.hex}}80 !important;
--background-modifier-selected: {{colors.primary.default.hex}}20 !important;
--background-modifier-accent: {{colors.primary.default.hex}}30 !important;
--background-modifier-active: {{colors.primary.default.hex}}25 !important;
/* Chat Input Improvements */
--text-input-background: {{colors.surface_container.default.hex}} !important;
--text-input-border: {{colors.outline.default.hex}} !important;
--text-input-border-hover: {{colors.primary.default.hex}} !important;
/* Additional Discord-specific input variables */
--deprecated-text-input-bg: {{colors.surface_container.default.hex}} !important;
--deprecated-text-input-border: {{colors.outline.default.hex}} !important;
--deprecated-text-input-border-hover: {{colors.primary.default.hex}} !important;
--input-background: {{colors.surface_container.default.hex}} !important;
--input-border: {{colors.outline.default.hex}} !important;
--input-placeholder-text: {{colors.on_surface_variant.default.hex}} !important;
/* Elevated/Container Backgrounds */
--background-tertiary: {{colors.surface_container.default.hex}} !important;
--background-accent: {{colors.surface_container.default.hex}} !important;
--background-surface-highest: {{colors.surface_container_high.default.hex}} !important;
--background-base-lowest: {{colors.surface_container.default.hex}} !important;
/* Border Colors */
--border-faint: {{colors.outline_variant.default.hex}};
--border-strong: {{colors.surface_container.default.hex}};
--border-normal: {{colors.surface_container_high.default.hex}};
--border-subtle: {{colors.surface.default.hex}} !important;
--chat-border: {{colors.surface_container_high.default.hex}};
/* Status Colors */
--status-positive: {{colors.tertiary.default.hex}};
--status-positive-background: {{colors.tertiary.default.hex}};
--status-positive-text: {{colors.on_tertiary.default.hex}};
--text-positive: {{colors.tertiary.default.hex}};
--text-feedback-positive: {{colors.tertiary.default.hex}};
--background-feedback-positive: {{colors.tertiary.default.hex}}20;
--info-positive-background: {{colors.tertiary.default.hex}}20;
--info-positive-foreground: {{colors.tertiary.default.hex}};
--info-positive-text: {{colors.on_surface.default.hex}};
--status-warning: {{colors.secondary.default.hex}};
--status-warning-background: {{colors.secondary.default.hex}};
--status-warning-text: {{colors.on_secondary.default.hex}};
--text-warning: {{colors.secondary.default.hex}};
--text-feedback-warning: {{colors.secondary.default.hex}};
--background-feedback-warning: {{colors.secondary.default.hex}}20;
--info-warning-background: {{colors.secondary.default.hex}}20;
--info-warning-foreground: {{colors.secondary.default.hex}};
--info-warning-text: {{colors.on_surface.default.hex}};
--status-danger: {{colors.error.default.hex}};
--status-danger-background: {{colors.error.default.hex}};
--status-danger-text: {{colors.on_error.default.hex}};
--text-danger: {{colors.error.default.hex}};
--text-feedback-critical: {{colors.error.default.hex}};
--background-feedback-critical: {{colors.error.default.hex}}20;
--info-danger-background: {{colors.error.default.hex}}20;
--info-danger-foreground: {{colors.error.default.hex}};
--info-danger-text: {{colors.on_surface.default.hex}};
/* Button Colors */
--button-secondary-background: {{colors.surface_variant.default.hex}} !important;
--button-secondary-background-hover: {{colors.surface_container.default.hex}};
--button-secondary-background-active: {{colors.surface_container.default.hex}};
--button-secondary-background-disabled: {{colors.surface_variant.default.hex}};
--button-secondary-text: {{colors.on_surface.default.hex}} !important;
--button-filled-brand-text: {{colors.on_primary.default.hex}};
--button-filled-brand-background: {{colors.primary.default.hex}};
--button-filled-brand-background-hover: {{colors.primary.default.hex}};
--button-filled-brand-background-active: {{colors.primary.default.hex}};
/* Input Colors */
--input-background: {{colors.surface_container.default.hex}};
--input-border: {{colors.outline.default.hex}};
--input-placeholder-text: {{colors.on_surface_variant.default.hex}};
/* Scrollbar Colors */
--scrollbar-thin-thumb: {{colors.primary.default.hex}};
--scrollbar-thin-track: transparent;
--scrollbar-auto-thumb: {{colors.primary.default.hex}};
--scrollbar-auto-track: {{colors.surface_container_high.default.hex}};
--scrollbar-auto-scrollbar-color-thumb: {{colors.primary.default.hex}};
--scrollbar-auto-scrollbar-color-track: {{colors.surface_container_high.default.hex}};
/* Icon Colors */
--icon-muted: {{colors.on_surface_variant.default.hex}};
--icon-default: {{colors.on_surface.default.hex}};
--icon-primary: {{colors.on_surface.default.hex}};
--icon-secondary: {{colors.on_surface_variant.default.hex}};
--icon-tertiary: {{colors.on_surface_variant.default.hex}} !important;
/* Channel Colors */
--channels-default: {{colors.on_surface_variant.default.hex}} !important;
--channel-icon: {{colors.on_surface_variant.default.hex}} !important;
--channel-text-area-placeholder: {{colors.on_surface.default.hex}}80;
/* Selection and Hover States */
--background-modifier-hover: {{colors.surface_variant.default.hex}}80;
--background-modifier-selected: {{colors.primary.default.hex}}20 !important;
--background-modifier-accent: {{colors.primary.default.hex}}30;
--background-modifier-active: {{colors.primary.default.hex}}25 !important;
--background-message-hover: {{colors.surface_variant.default.hex}}50 !important;
--background-message-highlight: {{colors.primary.default.hex}}15;
--background-message-highlight-hover: {{colors.primary.default.hex}}20;
/* Code Block - Use workspace background */
--background-code: {{colors.surface_container.default.hex}};
--textbox-markdown-syntax: {{colors.on_surface_variant.default.hex}};
/* Spoiler */
--spoiler-revealed-background: {{colors.surface_container.default.hex}};
--spoiler-hidden-background: {{colors.surface_variant.default.hex}};
/* White/Black Overrides */
--white: {{colors.on_surface.default.hex}};
--white-400: {{colors.on_surface.default.hex}};
--white-500: {{colors.on_surface.default.hex}};
--white-600: {{colors.on_surface_variant.default.hex}};
--white-700: {{colors.on_surface_variant.default.hex}};
--black-500: {{colors.surface_container_high.default.hex}};
/* Force styling for Discord unread messages banner */
--unread-bar-background: {{colors.primary.default.hex}}15 !important;
--unread-bar-text: {{colors.on_surface.default.hex}} !important;
--unread-bar-hover: {{colors.primary.default.hex}}20 !important;
/* Additional Discord unread bar variables */
--background-mentioned: {{colors.primary.default.hex}}15 !important;
--background-mentioned-hover: {{colors.primary.default.hex}}20 !important;
--text-mentioned: {{colors.on_surface.default.hex}} !important;
--text-mentioned-hover: {{colors.on_surface.default.hex}} !important;
--text-mentioned-link: {{colors.primary.default.hex}} !important;
/* Discord banner specific variables */
--background-message-automod: {{colors.primary.default.hex}}15 !important;
--background-message-automod-hover: {{colors.primary.default.hex}}20 !important;
--background-message-highlight: {{colors.primary.default.hex}}15 !important;
--background-message-highlight-hover: {{colors.primary.default.hex}}20 !important;
/* Discord unread bar specific variables */
--background-mentioned: {{colors.primary.default.hex}}15 !important;
--background-mentioned-hover: {{colors.primary.default.hex}}20 !important;
--text-mentioned: {{colors.on_surface.default.hex}} !important;
--text-mentioned-hover: {{colors.on_surface.default.hex}} !important;
--text-mentioned-link: {{colors.primary.default.hex}} !important;
/* Additional Discord text variables that might affect the banner */
--text-normal: {{colors.on_surface.default.hex}} !important;
--text-default: {{colors.on_surface.default.hex}} !important;
--text-primary: {{colors.on_surface.default.hex}} !important;
--text-secondary: {{colors.on_surface_variant.default.hex}} !important;
--text-tertiary: {{colors.on_surface_variant.default.hex}} !important;
--text-muted: {{colors.on_surface_variant.default.hex}} !important;
--interactive-normal: {{colors.on_surface.default.hex}} !important;
--interactive-muted: {{colors.on_surface_variant.default.hex}} !important;
/* Force styling for Discord chat input */
--chat-input-background: {{colors.surface_container.default.hex}} !important;
--chat-input-placeholder: {{colors.on_surface_variant.default.hex}} !important;
/* Discord unread messages banner specific variables */
--new-messages-bar-background: {{colors.surface_container.default.hex}} !important;
--new-messages-bar-text: {{colors.on_surface.default.hex}} !important;
--new-messages-bar-hover: {{colors.surface_container_high.default.hex}} !important;
--bar-button-background: {{colors.surface_container.default.hex}} !important;
--bar-button-text: {{colors.on_surface.default.hex}} !important;
--bar-button-hover: {{colors.surface_container_high.default.hex}} !important;
}
.visual-refresh.theme-dark ::selection,
.visual-refresh .theme-dark ::selection {
background-color: {{colors.primary.default.hex}};
}
/* Force Discord unread messages banner styling */
.visual-refresh.theme-dark .newMessagesBar__0f481,
.visual-refresh.theme-dark .barButtonMain__0f481,
.visual-refresh.theme-dark .barButtonBase__0f481,
.visual-refresh.theme-dark .span__0f481 {
background-color: {{colors.surface_container.default.hex}} !important;
color: {{colors.on_surface.default.hex}} !important;
}
.visual-refresh.theme-dark .newMessagesBar__0f481:hover,
.visual-refresh.theme-dark .barButtonMain__0f481:hover,
.visual-refresh.theme-dark .barButtonBase__0f481:hover {
background-color: {{colors.surface_container_high.default.hex}} !important;
}
/* Force Discord chat input styling */
.visual-refresh.theme-dark .channelTextArea-rNsIhG,
.visual-refresh.theme-dark .channelTextArea-rNsIhG *,
.visual-refresh.theme-dark .scrollableContainer-2NUZem,
.visual-refresh.theme-dark [data-slate-editor="true"] {
background-color: {{colors.surface_container.default.hex}} !important;
}
.visual-refresh.theme-dark [data-slate-editor="true"]::placeholder,
.visual-refresh.theme-dark .channelTextArea-rNsIhG [data-slate-editor="true"]::placeholder {
color: {{colors.on_surface_variant.default.hex}} !important;
}
/* Discord Emoji Picker Theming */
.visual-refresh.theme-dark .contentWrapper__08434,
.visual-refresh.theme-dark .emojiPicker_c0e32c,
.visual-refresh.theme-dark .wrapper_c0e32c {
background-color: {{colors.surface.default.hex}} !important;
}
.visual-refresh.theme-dark .nav__08434,
.visual-refresh.theme-dark .navList__08434 {
background-color: {{colors.surface.default.hex}} !important;
}
.visual-refresh.theme-dark .navButton__08434 {
background-color: {{colors.surface.default.hex}} !important;
color: {{colors.on_surface_variant.default.hex}} !important;
}
.visual-refresh.theme-dark .navButtonActive__08434 {
background-color: {{colors.surface.default.hex}} !important;
color: {{colors.on_surface.default.hex}} !important;
}
.visual-refresh.theme-dark .searchBar_c0e32c,
.visual-refresh.theme-dark .input_a45028 {
background-color: {{colors.surface.default.hex}} !important;
color: {{colors.on_surface.default.hex}} !important;
}
.visual-refresh.theme-dark .input_a45028::placeholder {
color: {{colors.on_surface_variant.default.hex}} !important;
}
.visual-refresh.theme-dark .header_c656ac,
.visual-refresh.theme-dark .header__14245,
.visual-refresh.theme-dark .wrapper__14245 {
background-color: {{colors.surface_variant.default.hex}} !important;
color: {{colors.on_surface.default.hex}} !important;
}
.visual-refresh.theme-dark .headerLabel__14245 {
color: {{colors.on_surface.default.hex}} !important;
}
.visual-refresh.theme-dark .interactive__14245 {
color: {{colors.on_surface.default.hex}} !important;
}
.visual-refresh.theme-dark .header__14245 {
color: {{colors.on_surface.default.hex}} !important;
}
.visual-refresh.theme-dark .header__14245 * {
color: {{colors.on_surface.default.hex}} !important;
}
.visual-refresh.theme-dark .headerIcon__14245 svg,
.visual-refresh.theme-dark .headerCollapseIcon__14245 svg {
color: {{colors.on_surface.default.hex}} !important;
fill: {{colors.on_surface.default.hex}} !important;
}
.visual-refresh.theme-dark .emojiItem_fc7141 {
background-color: transparent !important;
}
.visual-refresh.theme-dark .emojiItem_fc7141:hover {
background-color: {{colors.surface_container.default.hex}} !important;
}
.visual-refresh.theme-dark .emojiItemSelected_fc7141 {
background-color: {{colors.primary.default.hex}}20 !important;
}
.visual-refresh.theme-dark .inspector_aeaaeb {
background-color: {{colors.surface_container.default.hex}} !important;
color: {{colors.on_surface.default.hex}} !important;
}
.visual-refresh.theme-dark .categoryList_c0e32c {
background-color: {{colors.surface.default.hex}} !important;
}
.visual-refresh.theme-dark .categoryItem_b9ee0c {
background-color: transparent !important;
}
.visual-refresh.theme-dark .categoryItem_b9ee0c:hover {
background-color: {{colors.surface_variant.default.hex}} !important;
}
.visual-refresh.theme-dark .categoryItemDefaultCategorySelected_b9ee0c {
background-color: {{colors.surface_variant.default.hex}} !important;
}
/* Additional Discord emoji picker elements */
.visual-refresh.theme-dark .navItem__08434 {
background-color: {{colors.surface_variant.default.hex}} !important;
color: {{colors.on_surface_variant.default.hex}} !important;
}
.visual-refresh.theme-dark .navItem__08434:hover {
background-color: {{colors.surface_container.default.hex}} !important;
}
.visual-refresh.theme-dark .stickersNavItem__08434 {
color: {{colors.on_surface_variant.default.hex}} !important;
}
.visual-refresh.theme-dark .wrapper__14245 {
background-color: {{colors.surface_variant.default.hex}} !important;
}
.visual-refresh.theme-dark .headerLabel__14245 {
color: {{colors.on_surface.default.hex}} !important;
}
.visual-refresh.theme-dark .headerIcon__14245 svg,
.visual-refresh.theme-dark .headerCollapseIcon__14245 svg {
color: {{colors.on_surface_variant.default.hex}} !important;
}
.visual-refresh.theme-dark .interactive__14245:hover {
background-color: {{colors.surface_container.default.hex}} !important;
}
/* Chat input styling */
.visual-refresh.theme-dark .scrollableContainer__74017,
.visual-refresh.theme-dark .themedBackground__74017,
.visual-refresh.theme-dark .inner__74017,
.visual-refresh.theme-dark .textArea__74017,
.visual-refresh.theme-dark .slateContainer_ec4baf,
.visual-refresh.theme-dark .markup__75297,
.visual-refresh.theme-dark .editor__1b31f,
.visual-refresh.theme-dark .slateTextArea_ec4baf {
background-color: {{colors.surface_container.default.hex}} !important;
color: {{colors.on_surface.default.hex}} !important;
}
.visual-refresh.theme-dark .emptyText__1464f {
color: {{colors.on_surface_variant.default.hex}} !important;
}
.visual-refresh.theme-dark .placeholder__1b31f {
color: {{colors.on_surface_variant.default.hex}} !important;
}
/* Message content styling */
.visual-refresh.theme-dark .messageContent_c19a55 {
color: {{colors.on_surface.default.hex}} !important;
background-color: {{colors.surface.default.hex}} !important;
}
.visual-refresh.theme-dark .messageContent_c19a55 .markup__75297 {
color: {{colors.on_surface.default.hex}} !important;
background-color: {{colors.surface.default.hex}} !important;
}
/* Message background styling */
.visual-refresh.theme-dark .message__5126c,
.visual-refresh.theme-dark .cozyMessage__5126c,
.visual-refresh.theme-dark .wrapper_c19a55,
.visual-refresh.theme-dark .contents_c19a55 {
background-color: {{colors.surface.default.hex}} !important;
}
/* Message hover effects */
.visual-refresh.theme-dark .message__5126c:hover {
background-color: {{colors.surface_variant.default.hex}} !important;
}
.visual-refresh.theme-dark .message__5126c:hover * {
color: {{colors.on_surface.default.hex}} !important;
}
/* Remove Discord's native quote/reply bar */
.visual-refresh.theme-dark .message__5126c::before {
display: none !important;
}
.visual-refresh.theme-dark .message__5126c.hasReply_c19a55::before {
display: none !important;
}
/* Channel styling - darker text for read channels */
.visual-refresh.theme-dark .link__2ea32 .name__2ea32 {
color: {{colors.outline.default.hex}} !important;
}
/* Unread channels keep normal color */
.visual-refresh.theme-dark .link__2ea32[aria-label*="unread"] .name__2ea32 {
color: {{colors.on_surface.default.hex}} !important;
}
/* Search input styling */
.visual-refresh.theme-dark .inner_a45028 {
background-color: {{colors.surface_variant.default.hex}} !important;
}
.visual-refresh.theme-dark .input_a45028 {
background-color: transparent !important;
}
.visual-refresh.theme-dark .input_a45028::placeholder {
color: {{colors.on_surface_variant.default.hex}} !important;
}
/* Chat input placeholder styling */
.visual-refresh.theme-dark .emptyText__1464f {
color: {{colors.on_surface_variant.default.hex}} !important;
}
.visual-refresh.theme-dark .slateTextArea_ec4baf > div:first-child .emptyText__1464f::before {
content: "Message #general" !important;
color: {{colors.on_surface_variant.default.hex}} !important;
}
/* Hide placeholder when input is focused */
.visual-refresh.theme-dark .slateTextArea_ec4baf:focus .emptyText__1464f::before,
.visual-refresh.theme-dark .markup__75297:focus .emptyText__1464f::before {
display: none !important;
}
.visual-refresh.theme-dark .message__5126c:hover .messageContent_c19a55,
.visual-refresh.theme-dark .message__5126c:hover .markup__75297,
.visual-refresh.theme-dark .message__5126c:hover .header_c19a55,
.visual-refresh.theme-dark .message__5126c:hover .headerText_c19a55,
.visual-refresh.theme-dark .message__5126c:hover .username_c19a55,
.visual-refresh.theme-dark .message__5126c:hover .timestamp_c19a55 {
background-color: {{colors.surface_variant.default.hex}} !important;
}
.visual-refresh.theme-dark .categoryIcon_b9ee0c svg {
color: {{colors.on_surface_variant.default.hex}} !important;
}
.visual-refresh.theme-dark .unicodeShortcut_b9ee0c {
background-color: {{colors.surface_container.default.hex}} !important;
color: {{colors.on_surface.default.hex}} !important;
}
.visual-refresh.theme-dark .unicodeShortcut_b9ee0c:hover {
background-color: {{colors.surface_container_high.default.hex}} !important;
}
.visual-refresh.theme-dark .unicodeShortcut_b9ee0c svg {
color: {{colors.on_surface.default.hex}} !important;
}
/* Number badge styling */
.visual-refresh.theme-dark .numberBadge__2b1f5 {
color: {{colors.surface.default.hex}} !important;
background-color: {{colors.primary.default.hex}} !important;
}
/* New badge styling */
.visual-refresh.theme-dark .newBadge__4ed1a {
color: {{colors.surface.default.hex}} !important;
background-color: {{colors.primary.default.hex}} !important;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 373 KiB

36
Bin/shaders-compile.sh Executable file
View File

@@ -0,0 +1,36 @@
#!/bin/bash
# Directory containing the source shaders.
SOURCE_DIR="Shaders/frag/"
# Directory where the compiled shaders will be saved.
DEST_DIR="Shaders/qsb/"
# Check if the source directory exists.
if [ ! -d "$SOURCE_DIR" ]; then
echo "Source directory $SOURCE_DIR not found!"
exit 1
fi
# Create the destination directory if it doesn't exist.
mkdir -p "$DEST_DIR"
# Loop through all files in the source directory ending with .frag
for shader in "$SOURCE_DIR"*.frag; do
# Check if a file was found (to handle the case of no .frag files).
if [ -f "$shader" ]; then
# Get the base name of the file (e.g., wp_fade).
shader_name=$(basename "$shader" .frag)
# Construct the output path for the compiled shader.
output_path="$DEST_DIR$shader_name.frag.qsb"
# Construct and run the qsb command.
qsb --qt6 -o "$output_path" "$shader"
# Print a message to confirm compilation.
echo "Compiled $shader to $output_path"
fi
done
echo "Shader compilation complete."

View File

@@ -24,6 +24,11 @@ fi
TEMP_SENSOR_PATH=""
TEMP_SENSOR_TYPE=""
# Network speed monitoring variables
PREV_RX_BYTES=0
PREV_TX_BYTES=0
PREV_TIME=0
# --- Data Collection Functions ---
#
@@ -194,6 +199,8 @@ get_cpu_temp() {
fi
}
# --- Main Loop ---
# This loop runs indefinitely, gathering and printing stats.
while true; do
@@ -205,14 +212,58 @@ while true; do
disk_per=$(get_disk_usage)
cpu_usage=$(get_cpu_usage)
cpu_temp=$(get_cpu_temp)
# Get network speeds
current_time=$(date +%s.%N)
total_rx=0
total_tx=0
# Read total bytes from /proc/net/dev for all interfaces
while IFS=: read -r interface stats; do
# Skip only loopback interface, allow other interfaces
if [[ "$interface" =~ ^lo[[:space:]]*$ ]]; then
continue
fi
# Extract rx and tx bytes (fields 1 and 9 in the stats part)
rx_bytes=$(echo "$stats" | awk '{print $1}')
tx_bytes=$(echo "$stats" | awk '{print $9}')
# Add to totals if they are valid numbers
if [[ "$rx_bytes" =~ ^[0-9]+$ ]] && [[ "$tx_bytes" =~ ^[0-9]+$ ]]; then
total_rx=$((total_rx + rx_bytes))
total_tx=$((total_tx + tx_bytes))
fi
done < <(tail -n +3 /proc/net/dev)
# Calculate speeds if we have previous data
rx_speed=0
tx_speed=0
if [[ "$PREV_TIME" != "0" ]]; then
time_diff=$(awk -v current="$current_time" -v prev="$PREV_TIME" 'BEGIN { printf "%.3f", current - prev }')
rx_diff=$((total_rx - PREV_RX_BYTES))
tx_diff=$((total_tx - PREV_TX_BYTES))
# Calculate speeds in bytes per second using awk
rx_speed=$(awk -v rx="$rx_diff" -v time="$time_diff" 'BEGIN { printf "%.0f", rx / time }')
tx_speed=$(awk -v tx="$tx_diff" -v time="$time_diff" 'BEGIN { printf "%.0f", tx / time }')
fi
# Update previous values for next iteration
PREV_RX_BYTES=$total_rx
PREV_TX_BYTES=$total_tx
PREV_TIME=$current_time
# Use printf to format the final JSON output string, adding the mem_mb key.
printf '{"cpu": "%s", "cputemp": "%s", "memgb":"%s", "memper": "%s", "diskper": "%s"}\n' \
printf '{"cpu": "%s", "cputemp": "%s", "memgb":"%s", "memper": "%s", "diskper": "%s", "rx_speed": "%s", "tx_speed": "%s"}\n' \
"$cpu_usage" \
"$cpu_temp" \
"$mem_gb" \
"$mem_per" \
"$disk_per"
"$disk_per" \
"$rx_speed" \
"$tx_speed"
# Wait for the specified duration before the next update.
sleep "$SLEEP_DURATION"

View File

@@ -2,6 +2,7 @@ pragma Singleton
import QtQuick
import Quickshell
import qs.Services
Singleton {
id: icons
@@ -33,11 +34,21 @@ Singleton {
try {
if (typeof DesktopEntries === 'undefined' || !DesktopEntries.byId)
return iconFromName(fallback, fallback)
const entry = DesktopEntries.byId(appId)
const entry = (DesktopEntries.heuristicLookup) ? DesktopEntries.heuristicLookup(
appId) : DesktopEntries.byId(appId)
const name = entry && entry.icon ? entry.icon : ""
return iconFromName(name || fallback, fallback)
} catch (e) {
return iconFromName(fallback, fallback)
}
}
// Distro logo helper (absolute path or empty string)
function distroLogoPath() {
try {
return (typeof OSInfo !== 'undefined' && OSInfo.distroIconPath) ? OSInfo.distroIconPath : ""
} catch (e) {
return ""
}
}
}

View File

@@ -17,6 +17,14 @@ Singleton {
}
}
function _getStackTrace() {
try {
throw new Error("Stack trace")
} catch (e) {
return e.stack
}
}
function log(...args) {
var msg = _formatMessage(...args)
console.log(msg)
@@ -31,4 +39,20 @@ Singleton {
var msg = _formatMessage(...args)
console.error(msg)
}
function callStack() {
var stack = _getStackTrace()
Logger.log("Debug", "--------------------------")
Logger.log("Debug", "Current call stack")
// Split the stack into lines and log each one
var stackLines = stack.split('\n')
for (var i = 0; i < stackLines.length; i++) {
var line = stackLines[i].trim() // Remove leading/trailing whitespace
if (line.length > 0) {
// Only log non-empty lines
Logger.log("Debug", `- ${line}`)
}
}
Logger.log("Debug", "--------------------------")
}
}

View File

@@ -26,10 +26,12 @@ Singleton {
property string defaultAvatar: Quickshell.env("HOME") + "/.face"
// Used to access via Settings.data.xxx.yyy
property alias data: adapter
readonly property alias data: adapter
// Flag to prevent unnecessary wallpaper calls during reloads
property bool isInitialLoad: true
property bool isLoaded: false
// Signal emitted when settings are loaded after startupcale changes
signal settingsLoaded
// Function to validate monitor configurations
function validateMonitorConfigurations() {
@@ -90,22 +92,19 @@ Singleton {
reload()
}
onLoaded: function () {
Qt.callLater(function () {
if (isInitialLoad) {
Logger.log("Settings", "OnLoaded")
// Only set wallpaper on initial load, not on reloads
if (adapter.wallpaper.current !== "") {
Logger.log("Settings", "Set current wallpaper", adapter.wallpaper.current)
WallpaperService.setCurrentWallpaper(adapter.wallpaper.current, true)
}
if (!isLoaded) {
Logger.log("Settings", "----------------------------")
Logger.log("Settings", "Settings loaded successfully")
isLoaded = true
// Validate monitor configurations, only once
// if none of the configured monitors exist, clear the lists
// Emit the signal
root.settingsLoaded()
Qt.callLater(function () {
// Some stuff like settings validation should just be executed once on startup and not on every reload
validateMonitorConfigurations()
}
isInitialLoad = false
})
})
}
}
onLoadFailed: function (error) {
if (error.toString().includes("No such file") || error === 2)
@@ -121,7 +120,9 @@ Singleton {
property string position: "top" // Possible values: "top", "bottom"
property bool showActiveWindowIcon: true
property bool alwaysShowBatteryPercentage: false
property bool showNetworkStats: false
property real backgroundOpacity: 1.0
property string showWorkspaceLabel: "none"
property list<string> monitors: []
// Widget configuration for modular bar system
@@ -129,7 +130,7 @@ Singleton {
widgets: JsonObject {
property list<string> left: ["SystemMonitor", "ActiveWindow", "MediaMini"]
property list<string> center: ["Workspace"]
property list<string> right: ["ScreenRecorderIndicator", "Tray", "ArchUpdater", "NotificationHistory", "WiFi", "Bluetooth", "Battery", "Volume", "Brightness", "Clock", "SidePanelToggle"]
property list<string> right: ["ScreenRecorderIndicator", "Tray", "NotificationHistory", "WiFi", "Bluetooth", "Battery", "Volume", "Brightness", "NightLight", "Clock", "SidePanelToggle"]
}
}
@@ -139,6 +140,10 @@ Singleton {
property bool dimDesktop: false
property bool showScreenCorners: false
property real radiusRatio: 1.0
// Animation speed multiplier (0.1x - 2.0x)
property real animationSpeed: 1.0
// Replace sidepanel toggle with distro logo (shown in bar and/or side panel)
property bool useDistroLogoForSidepanel: false
}
// location
@@ -166,30 +171,23 @@ Singleton {
// wallpaper
property JsonObject wallpaper: JsonObject {
property string directory: "/usr/share/wallpapers"
property string current: ""
property bool isRandom: false
property int randomInterval: 300
property JsonObject swww
onDirectoryChanged: WallpaperService.listWallpapers()
onIsRandomChanged: WallpaperService.toggleRandomWallpaper()
onRandomIntervalChanged: WallpaperService.restartRandomWallpaperTimer()
swww: JsonObject {
property bool enabled: false
property string resizeMethod: "crop"
property int transitionFps: 60
property string transitionType: "random"
property real transitionDuration: 1.1
}
property bool enableMultiMonitorDirectories: false
property bool setWallpaperOnAllMonitors: true
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: true
// Position: center, top_left, top_right, bottom_left, bottom_right
// 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: []
}
@@ -218,6 +216,9 @@ Singleton {
property string visualizerType: "linear"
property int volumeStep: 5
property int cavaFrameRate: 60
// MPRIS controls
property list<string> mprisBlacklist: []
property string preferredPlayer: ""
}
// ui
@@ -225,19 +226,10 @@ Singleton {
property string fontDefault: "Roboto" // Default font for all text
property string fontFixed: "DejaVu Sans Mono" // Fixed width font for terminal
property string fontBillboard: "Inter" // Large bold font for clocks and prominent displays
// Legacy compatibility
property string fontFamily: fontDefault // Keep for backward compatibility
// Idle inhibitor state
property list<var> monitorsScaling: []
property bool idleInhibitorEnabled: false
}
// Scaling (not stored inside JsonObject, or it crashes)
property var monitorsScaling: {
}
// brightness
property JsonObject brightness: JsonObject {
property int brightnessStep: 5
@@ -247,8 +239,31 @@ Singleton {
property bool useWallpaperColors: false
property string predefinedScheme: ""
property bool darkMode: true
// External app theming (GTK & Qt)
property bool themeApps: false
}
// 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 enableUserTemplates: false
}
// night light
property JsonObject nightLight: JsonObject {
property bool enabled: false
property bool autoSchedule: true
property string nightTemp: "4000"
property string dayTemp: "6500"
property string manualSunrise: "06:30"
property string manualSunset: "18:30"
}
}
}

View File

@@ -13,6 +13,7 @@ Singleton {
*/
// Font size
property real fontSizeXXS: 8
property real fontSizeXS: 9
property real fontSizeS: 10
property real fontSizeM: 11
@@ -55,9 +56,9 @@ Singleton {
property real opacityFull: 1.0
// Animation duration (ms)
property int animationFast: 150
property int animationNormal: 300
property int animationSlow: 450
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)
// Dimensions
property int barHeight: 36

View File

@@ -18,7 +18,8 @@ Singleton {
dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1)
let day = date.getDate()
let month = date.toLocaleDateString(Qt.locale(), "MMM")
return timeString + " - " + dayName + ", " + day + " " + month
return timeString + " - " + (Settings.data.location.reverseDayMonth ? `${dayName}, ${month} ${day}` : `${dayName}, ${day} ${month}`)
}
return timeString

View File

@@ -13,21 +13,6 @@ NPanel {
panelHeight: 500 * scaling
panelAnchorRight: true
// Auto-refresh when service updates
Connections {
target: ArchUpdaterService
function onUpdatePackagesChanged() {
// Force UI update when packages change
if (root.visible) {
// Small delay to ensure data is fully updated
Qt.callLater(() => {
// Force a UI update by triggering a property change
ArchUpdaterService.updatePackages = ArchUpdaterService.updatePackages
}, 100)
}
}
}
panelContent: Rectangle {
color: Color.mSurface
radius: Style.radiusL * scaling
@@ -43,24 +28,36 @@ NPanel {
spacing: Style.marginM * scaling
NIcon {
text: "system_update"
text: "system_update_alt"
font.pointSize: Style.fontSizeXXL * scaling
color: Color.mPrimary
}
Text {
NText {
text: "System Updates"
font.pointSize: Style.fontSizeL * scaling
font.family: Settings.data.ui.fontDefault
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
}
// Reset button (only show if update failed)
NIconButton {
visible: ArchUpdaterService.updateFailed
icon: "refresh"
tooltipText: "Reset update state"
sizeRatio: 0.8
colorBg: Color.mError
colorFg: Color.mOnError
onClicked: {
ArchUpdaterService.resetUpdateState()
}
}
NIconButton {
icon: "close"
tooltipText: "Close"
sizeMultiplier: 0.8
sizeRatio: 0.8
onClicked: root.close()
}
}
@@ -69,79 +66,365 @@ NPanel {
Layout.fillWidth: true
}
// Update summary
Text {
text: ArchUpdaterService.updatePackages.length + " package" + (ArchUpdaterService.updatePackages.length
!== 1 ? "s" : "") + " can be updated"
// Update summary (only show when packages are available and terminal is configured)
NText {
visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed
&& !ArchUpdaterService.checkFailed && !ArchUpdaterService.aurBusy
&& ArchUpdaterService.totalUpdates > 0 && ArchUpdaterService.terminalAvailable
&& ArchUpdaterService.aurHelperAvailable
text: ArchUpdaterService.totalUpdates + " package" + (ArchUpdaterService.totalUpdates !== 1 ? "s" : "") + " can be updated"
font.pointSize: Style.fontSizeL * scaling
font.family: Settings.data.ui.fontDefault
font.weight: Style.fontWeightMedium
color: Color.mOnSurface
Layout.fillWidth: true
}
// Package selection info
Text {
text: ArchUpdaterService.selectedPackagesCount + " of " + ArchUpdaterService.updatePackages.length + " packages selected"
// Package selection info (only show when not updating and have packages and terminal is configured)
NText {
visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed
&& !ArchUpdaterService.checkFailed && !ArchUpdaterService.aurBusy
&& ArchUpdaterService.totalUpdates > 0 && ArchUpdaterService.terminalAvailable
&& ArchUpdaterService.aurHelperAvailable
text: ArchUpdaterService.selectedPackagesCount + " of " + ArchUpdaterService.totalUpdates + " packages selected"
font.pointSize: Style.fontSizeS * scaling
font.family: Settings.data.ui.fontDefault
color: Color.mOnSurfaceVariant
Layout.fillWidth: true
}
// Package list
Rectangle {
// Update in progress state
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
color: Color.mSurfaceVariant
radius: Style.radiusM * scaling
visible: ArchUpdaterService.updateInProgress
spacing: Style.marginM * scaling
Item {
Layout.fillHeight: true
} // Spacer
NIcon {
text: "hourglass_empty"
font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mPrimary
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Update in progress"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Please check your terminal window for update progress and prompts."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
Layout.maximumWidth: 280 * scaling
}
Item {
Layout.fillHeight: true
} // Spacer
}
// Terminal not available state
Item {
Layout.fillWidth: true
Layout.fillHeight: true
visible: !ArchUpdaterService.terminalAvailable && !ArchUpdaterService.updateInProgress
&& !ArchUpdaterService.updateFailed
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginM * scaling
NIcon {
text: "terminal"
font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mError
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Terminal not configured"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "The TERMINAL environment variable is not set. Please set it to your preferred terminal (e.g., kitty, alacritty, foot) in your shell configuration."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
Layout.maximumWidth: 280 * scaling
}
}
}
// AUR helper not available state
Item {
Layout.fillWidth: true
Layout.fillHeight: true
visible: ArchUpdaterService.terminalAvailable && !ArchUpdaterService.aurHelperAvailable
&& !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed
&& !ArchUpdaterService.checkFailed && !ArchUpdaterService.aurBusy
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginM * scaling
NIcon {
text: "package"
font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mError
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "AUR helper not found"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "No AUR helper (yay or paru) is installed. Please install either yay or paru to manage AUR packages. yay is recommended."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
Layout.maximumWidth: 280 * scaling
}
}
}
// Check failed state (AUR down, network issues, etc.)
Item {
Layout.fillWidth: true
Layout.fillHeight: true
visible: ArchUpdaterService.checkFailed && !ArchUpdaterService.updateInProgress
&& !ArchUpdaterService.updateFailed && ArchUpdaterService.terminalAvailable
&& ArchUpdaterService.aurHelperAvailable
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginM * scaling
NIcon {
text: "error"
font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mError
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Cannot check for updates"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: ArchUpdaterService.lastCheckError
|| "AUR helper is unavailable or network connection failed. This could be due to AUR being down, network issues, or missing AUR helper (yay/paru)."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
Layout.maximumWidth: 280 * scaling
}
// Prominent refresh button
NIconButton {
icon: "refresh"
tooltipText: "Try checking again"
sizeRatio: 1.2
colorBg: Color.mPrimary
colorFg: Color.mOnPrimary
onClicked: {
ArchUpdaterService.forceRefresh()
}
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: Style.marginL * scaling
}
}
}
// Update failed state
Item {
Layout.fillWidth: true
Layout.fillHeight: true
visible: ArchUpdaterService.updateFailed
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginM * scaling
NIcon {
text: "error_outline"
font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mError
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Update failed"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Check your terminal for error details and try again."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
Layout.maximumWidth: 280 * scaling
}
// Prominent refresh button
NIconButton {
icon: "refresh"
tooltipText: "Refresh and try again"
sizeRatio: 1.2
colorBg: Color.mPrimary
colorFg: Color.mOnPrimary
onClicked: {
ArchUpdaterService.resetUpdateState()
}
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: Style.marginL * scaling
}
}
}
// No updates available state
Item {
Layout.fillWidth: true
Layout.fillHeight: true
visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed
&& !ArchUpdaterService.checkFailed && !ArchUpdaterService.aurBusy
&& ArchUpdaterService.totalUpdates === 0 && ArchUpdaterService.terminalAvailable
&& ArchUpdaterService.aurHelperAvailable
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginM * scaling
NIcon {
text: "check_circle"
font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mPrimary
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "System is up to date"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "All packages are current. Check back later for updates."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
Layout.maximumWidth: 280 * scaling
}
}
}
// Checking for updates state
Item {
Layout.fillWidth: true
Layout.fillHeight: true
visible: ArchUpdaterService.aurBusy && !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed
&& ArchUpdaterService.terminalAvailable && ArchUpdaterService.aurHelperAvailable
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginM * scaling
NBusyIndicator {
Layout.alignment: Qt.AlignHCenter
size: Style.fontSizeXXXL * scaling
color: Color.mPrimary
}
NText {
text: "Checking for updates"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Scanning package databases for available updates..."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
Layout.maximumWidth: 280 * scaling
}
}
}
// Package list (only show when not in any special state)
NBox {
visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed
&& !ArchUpdaterService.checkFailed && !ArchUpdaterService.aurBusy
&& ArchUpdaterService.totalUpdates > 0 && ArchUpdaterService.terminalAvailable
&& ArchUpdaterService.aurHelperAvailable
Layout.fillWidth: true
Layout.fillHeight: true
// Combine repo and AUR lists in order: repos first, then AUR
property var items: (ArchUpdaterService.repoPackages || []).concat(ArchUpdaterService.aurPackages || [])
ListView {
id: packageListView
id: unifiedList
anchors.fill: parent
anchors.margins: Style.marginS * scaling
anchors.margins: Style.marginM * scaling
cacheBuffer: Math.round(300 * scaling)
clip: true
model: ArchUpdaterService.updatePackages
spacing: Style.marginXS * scaling
model: parent.items
delegate: Rectangle {
width: packageListView.width
height: 50 * scaling
width: unifiedList.width
height: 44 * scaling
color: Color.transparent
radius: Style.radiusS * scaling
RowLayout {
anchors.fill: parent
anchors.margins: Style.marginS * scaling
spacing: Style.marginS * scaling
// Checkbox for selection
NIconButton {
NCheckbox {
id: checkbox
icon: "check_box_outline_blank"
onClicked: {
const isSelected = ArchUpdaterService.isPackageSelected(modelData.name)
if (isSelected) {
ArchUpdaterService.togglePackageSelection(modelData.name)
icon = "check_box_outline_blank"
colorFg = Color.mOnSurfaceVariant
} else {
ArchUpdaterService.togglePackageSelection(modelData.name)
icon = "check_box"
colorFg = Color.mPrimary
}
}
colorBg: Color.transparent
colorFg: Color.mOnSurfaceVariant
Layout.preferredWidth: 30 * scaling
Layout.preferredHeight: 30 * scaling
Component.onCompleted: {
// Set initial state
if (ArchUpdaterService.isPackageSelected(modelData.name)) {
icon = "check_box"
colorFg = Color.mPrimary
}
label: ""
description: ""
checked: ArchUpdaterService.isPackageSelected(modelData.name)
baseSize: Math.max(Style.baseWidgetSize * 0.7, 14)
onToggled: function (checked) {
ArchUpdaterService.togglePackageSelection(modelData.name)
// Force refresh of the checked property
checkbox.checked = ArchUpdaterService.isPackageSelected(modelData.name)
}
}
@@ -150,79 +433,93 @@ NPanel {
Layout.fillWidth: true
spacing: Style.marginXXS * scaling
Text {
NText {
text: modelData.name
font.pointSize: Style.fontSizeM * scaling
font.family: Settings.data.ui.fontDefault
font.weight: Style.fontWeightMedium
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
}
Text {
NText {
text: modelData.oldVersion + " → " + modelData.newVersion
font.pointSize: Style.fontSizeS * scaling
font.family: Settings.data.ui.fontDefault
font.pointSize: Style.fontSizeXXS * scaling
color: Color.mOnSurfaceVariant
Layout.fillWidth: true
}
}
}
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
// Source tag (AUR vs PAC)
Rectangle {
visible: !!modelData.source
radius: width * 0.5
color: modelData.source === "aur" ? Color.mTertiary : Color.mSecondary
Layout.alignment: Qt.AlignVCenter
implicitHeight: Style.fontSizeS * 1.8 * scaling
// Width based on label content + horizontal padding
implicitWidth: badgeText.implicitWidth + Math.max(12 * scaling, Style.marginS * scaling)
NText {
id: badgeText
anchors.centerIn: parent
text: modelData.source === "aur" ? "AUR" : "PAC"
font.pointSize: Style.fontSizeXXS * scaling
font.weight: Style.fontWeightBold
color: modelData.source === "aur" ? Color.mOnTertiary : Color.mOnSecondary
}
}
}
}
}
}
// Action buttons
// Action buttons (only show when not updating)
RowLayout {
visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed
&& !ArchUpdaterService.checkFailed && ArchUpdaterService.terminalAvailable
&& ArchUpdaterService.aurHelperAvailable
Layout.fillWidth: true
spacing: Style.marginS * scaling
spacing: Style.marginL * scaling
NIconButton {
icon: "refresh"
tooltipText: "Check for updates"
tooltipText: ArchUpdaterService.aurBusy ? "Checking for updates..." : (!ArchUpdaterService.canPoll ? "Refresh available soon" : "Refresh package lists")
onClicked: {
ArchUpdaterService.doPoll()
ArchUpdaterService.forceRefresh()
}
colorBg: Color.mSurfaceVariant
colorFg: Color.mOnSurface
Layout.fillWidth: true
Layout.preferredHeight: 35 * scaling
enabled: !ArchUpdaterService.aurBusy
}
NIconButton {
icon: ArchUpdaterService.updateInProgress ? "hourglass_empty" : "system_update"
tooltipText: ArchUpdaterService.updateInProgress ? "Update in progress..." : "Update all packages"
enabled: !ArchUpdaterService.updateInProgress
icon: "system_update_alt"
tooltipText: "Update all packages"
enabled: ArchUpdaterService.totalUpdates > 0
onClicked: {
ArchUpdaterService.runUpdate()
root.close()
}
colorBg: ArchUpdaterService.updateInProgress ? Color.mSurfaceVariant : Color.mPrimary
colorFg: ArchUpdaterService.updateInProgress ? Color.mOnSurfaceVariant : Color.mOnPrimary
colorBg: ArchUpdaterService.totalUpdates > 0 ? Color.mPrimary : Color.mSurfaceVariant
colorFg: ArchUpdaterService.totalUpdates > 0 ? Color.mOnPrimary : Color.mOnSurfaceVariant
Layout.fillWidth: true
Layout.preferredHeight: 35 * scaling
}
NIconButton {
icon: ArchUpdaterService.updateInProgress ? "hourglass_empty" : "settings"
tooltipText: ArchUpdaterService.updateInProgress ? "Update in progress..." : "Update selected packages"
enabled: !ArchUpdaterService.updateInProgress && ArchUpdaterService.selectedPackagesCount > 0
icon: "check_box"
tooltipText: "Update selected packages"
enabled: ArchUpdaterService.selectedPackagesCount > 0
onClicked: {
if (ArchUpdaterService.selectedPackagesCount > 0) {
ArchUpdaterService.runSelectiveUpdate()
root.close()
}
}
colorBg: ArchUpdaterService.updateInProgress ? Color.mSurfaceVariant : (ArchUpdaterService.selectedPackagesCount
> 0 ? Color.mSecondary : Color.mSurfaceVariant)
colorFg: ArchUpdaterService.updateInProgress ? Color.mOnSurfaceVariant : (ArchUpdaterService.selectedPackagesCount
> 0 ? Color.mOnSecondary : Color.mOnSurfaceVariant)
colorBg: ArchUpdaterService.selectedPackagesCount > 0 ? Color.mPrimary : Color.mSurfaceVariant
colorFg: ArchUpdaterService.selectedPackagesCount > 0 ? Color.mOnPrimary : Color.mOnSurfaceVariant
Layout.fillWidth: true
Layout.preferredHeight: 35 * scaling
}
}
}

View File

@@ -4,27 +4,61 @@ import Quickshell.Wayland
import qs.Commons
import qs.Services
Loader {
active: !Settings.data.wallpaper.swww.enabled
Variants {
id: backgroundVariants
model: Quickshell.screens
sourceComponent: Variants {
model: Quickshell.screens
delegate: Loader {
delegate: PanelWindow {
required property ShellScreen modelData
property string wallpaperSource: WallpaperService.currentWallpaper !== ""
&& !Settings.data.wallpaper.swww.enabled ? WallpaperService.currentWallpaper : ""
required property ShellScreen modelData
visible: wallpaperSource !== "" && !Settings.data.wallpaper.swww.enabled
active: Settings.isLoaded && modelData
// Force update when SWWW setting changes
onVisibleChanged: {
if (visible) {
sourceComponent: PanelWindow {
id: root
} else {
// Internal state management
property string transitionType: "fade"
property real transitionProgress: 0
readonly property real edgeSmoothness: Settings.data.wallpaper.transitionEdgeSmoothness
readonly property var allTransitions: WallpaperService.allTransitions
readonly property bool transitioning: transitionAnimation.running
// Wipe direction: 0=left, 1=right, 2=up, 3=down
property real wipeDirection: 0
// Disc
property real discCenterX: 0.5
property real discCenterY: 0.5
// Stripe
property real stripesCount: 16
property real stripesAngle: 0
// Used to debounce wallpaper changes
property string futureWallpaper: ""
// On startup assign wallpaper immediately
Component.onCompleted: {
var path = modelData ? WallpaperService.getWallpaper(modelData.name) : ""
setWallpaperImmediate(path)
}
// External state management
Connections {
target: WallpaperService
function onWallpaperChanged(screenName, path) {
if (screenName === modelData.name) {
// Update wallpaper display
// Set wallpaper immediately on startup
futureWallpaper = path
debounceTimer.restart()
}
}
}
color: Color.transparent
screen: modelData
WlrLayershell.layer: WlrLayer.Background
@@ -38,18 +72,188 @@ Loader {
left: true
}
margins {
top: 0
Timer {
id: debounceTimer
interval: 333
running: false
repeat: false
onTriggered: {
changeWallpaper()
}
}
Image {
id: currentWallpaper
anchors.fill: parent
fillMode: Image.PreserveAspectCrop
source: wallpaperSource
visible: wallpaperSource !== ""
cache: true
source: ""
smooth: true
mipmap: false
visible: false
cache: false
// currentWallpaper should not be asynchronous to avoid flickering when swapping next to current.
asynchronous: false
}
Image {
id: nextWallpaper
anchors.fill: parent
fillMode: Image.PreserveAspectCrop
source: ""
smooth: true
mipmap: false
visible: false
cache: false
asynchronous: true
}
// Fade or None transition shader
ShaderEffect {
id: fadeShader
anchors.fill: parent
visible: transitionType === "fade" || transitionType === "none"
property variant source1: currentWallpaper
property variant source2: nextWallpaper
property real progress: root.transitionProgress
fragmentShader: Qt.resolvedUrl("../../Shaders/qsb/wp_fade.frag.qsb")
}
// Wipe transition shader
ShaderEffect {
id: wipeShader
anchors.fill: parent
visible: transitionType === "wipe"
property variant source1: currentWallpaper
property variant source2: nextWallpaper
property real progress: root.transitionProgress
property real smoothness: root.edgeSmoothness
property real direction: root.wipeDirection
fragmentShader: Qt.resolvedUrl("../../Shaders/qsb/wp_wipe.frag.qsb")
}
// Disc reveal transition shader
ShaderEffect {
id: discShader
anchors.fill: parent
visible: transitionType === "disc"
property variant source1: currentWallpaper
property variant source2: nextWallpaper
property real progress: root.transitionProgress
property real smoothness: root.edgeSmoothness
property real aspectRatio: root.width / root.height
property real centerX: root.discCenterX
property real centerY: root.discCenterY
fragmentShader: Qt.resolvedUrl("../../Shaders/qsb/wp_disc.frag.qsb")
}
// Diagonal stripes transition shader
ShaderEffect {
id: stripesShader
anchors.fill: parent
visible: transitionType === "stripes"
property variant source1: currentWallpaper
property variant source2: nextWallpaper
property real progress: root.transitionProgress
property real smoothness: root.edgeSmoothness
property real aspectRatio: root.width / root.height
property real stripeCount: root.stripesCount
property real angle: root.stripesAngle
fragmentShader: Qt.resolvedUrl("../../Shaders/qsb/wp_stripes.frag.qsb")
}
// Animation for the transition progress
NumberAnimation {
id: transitionAnimation
target: root
property: "transitionProgress"
from: 0.0
to: 1.0
// The stripes shader feels faster visually, we make it a bit slower here.
duration: transitionType == "stripes" ? Settings.data.wallpaper.transitionDuration
* 1.6 : Settings.data.wallpaper.transitionDuration
easing.type: Easing.InOutCubic
onFinished: {
// Swap images after transition completes
currentWallpaper.source = nextWallpaper.source
nextWallpaper.source = ""
transitionProgress = 0.0
}
}
function startTransition() {
if (!transitioning && nextWallpaper.source != currentWallpaper.source) {
transitionAnimation.start()
}
}
function setWallpaperImmediate(source) {
transitionAnimation.stop()
transitionProgress = 0.0
currentWallpaper.source = source
nextWallpaper.source = ""
}
function setWallpaperWithTransition(source) {
if (source != currentWallpaper.source) {
if (transitioning) {
// We are interrupting a transition
transitionAnimation.stop()
transitionProgress = 0
currentWallpaper.source = nextWallpaper.source
nextWallpaper.source = ""
}
nextWallpaper.source = source
startTransition()
}
}
// Main method that actually trigger the wallpaper change
function changeWallpaper() {
// Get the transitionType from the settings
transitionType = Settings.data.wallpaper.transitionType
if (transitionType == "random") {
var index = Math.floor(Math.random() * allTransitions.length)
transitionType = allTransitions[index]
}
// Ensure the transition type really exists
if (transitionType !== "none" && !allTransitions.includes(transitionType)) {
transitionType = "fade"
}
//Logger.log("Background", "New wallpaper: ", futureWallpaper, "On:", modelData.name, "Transition:", transitionType)
switch (transitionType) {
case "none":
setWallpaperImmediate(futureWallpaper)
break
case "wipe":
wipeDirection = Math.random() * 4
setWallpaperWithTransition(futureWallpaper)
break
case "disc":
discCenterX = Math.random()
discCenterY = Math.random()
setWallpaperWithTransition(futureWallpaper)
break
case "stripes":
stripesCount = Math.round(Math.random() * 20 + 4)
stripesAngle = Math.random() * 360
setWallpaperWithTransition(futureWallpaper)
break
default:
setWallpaperWithTransition(futureWallpaper)
break
}
}
}
}

View File

@@ -6,24 +6,34 @@ import qs.Commons
import qs.Services
import qs.Widgets
Loader {
active: CompositorService.isNiri
Variants {
model: Quickshell.screens
Component.onCompleted: {
if (CompositorService.isNiri) {
Logger.log("Overview", "Loading Overview component for Niri")
}
}
delegate: Loader {
required property ShellScreen modelData
sourceComponent: Variants {
model: Quickshell.screens
active: Settings.isLoaded && CompositorService.isNiri && modelData
delegate: PanelWindow {
required property ShellScreen modelData
property string wallpaperSource: WallpaperService.currentWallpaper !== ""
&& !Settings.data.wallpaper.swww.enabled ? WallpaperService.currentWallpaper : ""
property string wallpaper: ""
sourceComponent: PanelWindow {
Component.onCompleted: {
if (modelData) {
Logger.log("Overview", "Loading Overview component for Niri on", modelData.name)
}
wallpaper = modelData ? WallpaperService.getWallpaper(modelData.name) : ""
}
// External state management
Connections {
target: WallpaperService
function onWallpaperChanged(screenName, path) {
if (screenName === modelData.name) {
wallpaper = path
}
}
}
visible: wallpaperSource !== "" && !Settings.data.wallpaper.swww.enabled
color: Color.transparent
screen: modelData
WlrLayershell.layer: WlrLayer.Background
@@ -39,19 +49,15 @@ Loader {
Image {
id: bgImage
anchors.fill: parent
fillMode: Image.PreserveAspectCrop
source: wallpaperSource
cache: true
source: wallpaper
smooth: true
mipmap: false
visible: wallpaperSource !== ""
cache: false
}
MultiEffect {
id: overviewBgBlur
anchors.fill: parent
source: bgImage
blurEnabled: true
@@ -59,9 +65,12 @@ Loader {
blurMax: 128
}
// Make the overview darker
Rectangle {
anchors.fill: parent
color: Qt.rgba(Color.mSurface.r, Color.mSurface.g, Color.mSurface.b, 0.5)
color: Settings.data.colorSchemes.darkMode ? Qt.rgba(Color.mSurface.r, Color.mSurface.g, Color.mSurface.b,
0.5) : Qt.rgba(Color.mOnSurface.r, Color.mOnSurface.g,
Color.mOnSurface.b, 0.5)
}
}
}

View File

@@ -16,23 +16,27 @@ Loader {
id: root
required property ShellScreen modelData
readonly property real scaling: ScalingService.scale(screen)
property real scaling: ScalingService.getScreenScale(screen)
screen: modelData
// Visible color
property color ringColor: Qt.rgba(Color.mSurface.r, Color.mSurface.g, Color.mSurface.b,
Settings.data.bar.backgroundOpacity)
// The amount subtracted from full size for the inner cutout
// Inner size = full size - borderWidth (per axis)
property int borderWidth: Style.borderM
// Rounded radius for the inner cutout
property int innerRadius: 20
property color cornerColor: Qt.rgba(Color.mSurface.r, Color.mSurface.g, Color.mSurface.b,
Settings.data.bar.backgroundOpacity)
property real cornerRadius: 20 * scaling
property real cornerSize: 20 * scaling
Connections {
target: ScalingService
function onScaleChanged(screenName, scale) {
if (screenName === screen.name) {
scaling = scale
}
}
}
color: Color.transparent
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "quickshell-corner"
// Do not take keyboard focus and make the surface click-through
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
anchors {
@@ -44,107 +48,201 @@ Loader {
margins {
top: ((modelData && Settings.data.bar.monitors.includes(modelData.name))
|| (Settings.data.bar.monitors.length === 0))
&& Settings.data.bar.position === "top" ? Math.floor(Style.barHeight * scaling) : 0
|| (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "top"
&& Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
bottom: ((modelData && Settings.data.bar.monitors.includes(modelData.name))
|| (Settings.data.bar.monitors.length === 0))
&& Settings.data.bar.position === "bottom" ? Math.floor(Style.barHeight * scaling) : 0
|| (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "bottom"
&& Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
}
// Source we want to show only as a ring
Rectangle {
id: overlaySource
anchors.fill: parent
color: root.ringColor
}
// Texture for overlaySource
ShaderEffectSource {
id: overlayTexture
anchors.fill: parent
sourceItem: overlaySource
hideSource: true
live: true
visible: false
}
// Mask via Canvas: paint opaque white, then punch rounded inner hole
// Top-left concave corner
Canvas {
id: maskSource
anchors.fill: parent
id: topLeftCorner
anchors.top: parent.top
anchors.left: parent.left
width: cornerSize
height: cornerSize
antialiasing: true
renderTarget: Canvas.FramebufferObject
smooth: false
onPaint: {
const ctx = getContext("2d")
if (!ctx)
return
ctx.reset()
ctx.clearRect(0, 0, width, height)
// Solid white base (alpha=1)
ctx.globalCompositeOperation = "source-over"
ctx.fillStyle = "#ffffffff"
// Fill the entire area with the corner color
ctx.fillStyle = Qt.rgba(root.cornerColor.r, root.cornerColor.g, root.cornerColor.b, root.cornerColor.a)
ctx.fillRect(0, 0, width, height)
// Punch hole using destination-out with rounded rect path
const x = Math.round(root.borderWidth / 2)
const y = Math.round(root.borderWidth / 2)
const w = Math.max(0, width - root.borderWidth)
const h = Math.max(0, height - root.borderWidth)
const r = Math.max(0, Math.min(root.innerRadius, Math.min(w, h) / 2))
// Cut out the rounded corner using destination-out
ctx.globalCompositeOperation = "destination-out"
ctx.fillStyle = "#ffffffff"
ctx.fillStyle = "#ffffff"
ctx.beginPath()
// rounded rectangle path using arcTo
ctx.moveTo(x + r, y)
ctx.lineTo(x + w - r, y)
ctx.arcTo(x + w, y, x + w, y + r, r)
ctx.lineTo(x + w, y + h - r)
ctx.arcTo(x + w, y + h, x + w - r, y + h, r)
ctx.lineTo(x + r, y + h)
ctx.arcTo(x, y + h, x, y + h - r, r)
ctx.lineTo(x, y + r)
ctx.arcTo(x, y, x + r, y, r)
ctx.closePath()
ctx.arc(width, height, root.cornerRadius, 0, 2 * Math.PI)
ctx.fill()
}
onWidthChanged: requestPaint()
onHeightChanged: requestPaint()
onWidthChanged: if (available)
requestPaint()
onHeightChanged: if (available)
requestPaint()
Connections {
target: root
function onCornerColorChanged() {
if (topLeftCorner.available)
topLeftCorner.requestPaint()
}
function onCornerRadiusChanged() {
if (topLeftCorner.available)
topLeftCorner.requestPaint()
}
}
}
// Repaint mask when properties change
Connections {
function onBorderWidthChanged() {
maskSource.requestPaint()
// Top-right concave corner
Canvas {
id: topRightCorner
anchors.top: parent.top
anchors.right: parent.right
width: cornerSize
height: cornerSize
antialiasing: true
renderTarget: Canvas.FramebufferObject
smooth: true
onPaint: {
const ctx = getContext("2d")
if (!ctx)
return
ctx.reset()
ctx.clearRect(0, 0, width, height)
ctx.fillStyle = Qt.rgba(root.cornerColor.r, root.cornerColor.g, root.cornerColor.b, root.cornerColor.a)
ctx.fillRect(0, 0, width, height)
ctx.globalCompositeOperation = "destination-out"
ctx.fillStyle = "#ffffff"
ctx.beginPath()
ctx.arc(0, height, root.cornerRadius, 0, 2 * Math.PI)
ctx.fill()
}
function onRingColorChanged() {}
onWidthChanged: if (available)
requestPaint()
onHeightChanged: if (available)
requestPaint()
function onInnerRadiusChanged() {
maskSource.requestPaint()
Connections {
target: root
function onCornerColorChanged() {
if (topRightCorner.available)
topRightCorner.requestPaint()
}
function onCornerRadiusChanged() {
if (topRightCorner.available)
topRightCorner.requestPaint()
}
}
}
// Bottom-left concave corner
Canvas {
id: bottomLeftCorner
anchors.bottom: parent.bottom
anchors.left: parent.left
width: cornerSize
height: cornerSize
antialiasing: true
renderTarget: Canvas.FramebufferObject
smooth: true
onPaint: {
const ctx = getContext("2d")
if (!ctx)
return
ctx.reset()
ctx.clearRect(0, 0, width, height)
ctx.fillStyle = Qt.rgba(root.cornerColor.r, root.cornerColor.g, root.cornerColor.b, root.cornerColor.a)
ctx.fillRect(0, 0, width, height)
ctx.globalCompositeOperation = "destination-out"
ctx.fillStyle = "#ffffff"
ctx.beginPath()
ctx.arc(width, 0, root.cornerRadius, 0, 2 * Math.PI)
ctx.fill()
}
target: root
onWidthChanged: if (available)
requestPaint()
onHeightChanged: if (available)
requestPaint()
Connections {
target: root
function onCornerColorChanged() {
if (bottomLeftCorner.available)
bottomLeftCorner.requestPaint()
}
function onCornerRadiusChanged() {
if (bottomLeftCorner.available)
bottomLeftCorner.requestPaint()
}
}
}
// Texture for maskSource; hides the original
ShaderEffectSource {
id: maskTexture
// Bottom-right concave corner
Canvas {
id: bottomRightCorner
anchors.bottom: parent.bottom
anchors.right: parent.right
width: cornerSize
height: cornerSize
antialiasing: true
renderTarget: Canvas.FramebufferObject
smooth: true
anchors.fill: parent
sourceItem: maskSource
hideSource: true
live: true
visible: false
}
onPaint: {
const ctx = getContext("2d")
if (!ctx)
return
// Apply mask to show only the ring area
MultiEffect {
anchors.fill: parent
source: overlayTexture
maskEnabled: true
maskSource: maskTexture
maskInverted: false
maskSpreadAtMax: 0.75
ctx.reset()
ctx.clearRect(0, 0, width, height)
ctx.fillStyle = Qt.rgba(root.cornerColor.r, root.cornerColor.g, root.cornerColor.b, root.cornerColor.a)
ctx.fillRect(0, 0, width, height)
ctx.globalCompositeOperation = "destination-out"
ctx.fillStyle = "#ffffff"
ctx.beginPath()
ctx.arc(0, 0, root.cornerRadius, 0, 2 * Math.PI)
ctx.fill()
}
onWidthChanged: if (available)
requestPaint()
onHeightChanged: if (available)
requestPaint()
Connections {
target: root
function onCornerColorChanged() {
if (bottomRightCorner.available)
bottomRightCorner.requestPaint()
}
function onCornerRadiusChanged() {
if (bottomRightCorner.available)
bottomRightCorner.requestPaint()
}
}
}
mask: Region {}

View File

@@ -12,115 +12,133 @@ import qs.Modules.Notification
Variants {
model: Quickshell.screens
delegate: PanelWindow {
delegate: Loader {
id: root
required property ShellScreen modelData
readonly property real scaling: ScalingService.scale(screen)
screen: modelData
property real scaling: ScalingService.getScreenScale(modelData)
WlrLayershell.namespace: "noctalia-bar"
implicitHeight: Style.barHeight * scaling
color: Color.transparent
// If no bar activated in settings, then show them all
visible: modelData ? (Settings.data.bar.monitors.includes(modelData.name)
|| (Settings.data.bar.monitors.length === 0)) : false
anchors {
top: Settings.data.bar.position === "top"
bottom: Settings.data.bar.position === "bottom"
left: true
right: true
Connections {
target: ScalingService
function onScaleChanged(screenName, scale) {
if ((modelData !== null) && (screenName === modelData.name)) {
scaling = scale
}
}
}
Item {
anchors.fill: parent
clip: true
active: Settings.isLoaded && modelData && modelData.name ? (Settings.data.bar.monitors.includes(modelData.name)
|| (Settings.data.bar.monitors.length === 0)) : false
// Background fill
Rectangle {
id: bar
sourceComponent: PanelWindow {
screen: modelData || null
WlrLayershell.namespace: "noctalia-bar"
implicitHeight: Math.round(Style.barHeight * scaling)
color: Color.transparent
anchors {
top: Settings.data.bar.position === "top"
bottom: Settings.data.bar.position === "bottom"
left: true
right: true
}
Item {
anchors.fill: parent
color: Qt.rgba(Color.mSurface.r, Color.mSurface.g, Color.mSurface.b, Settings.data.bar.backgroundOpacity)
layer.enabled: true
}
clip: true
// ------------------------------
// Left Section - Dynamic Widgets
Row {
id: leftSection
// Background fill
Rectangle {
id: bar
height: parent.height
anchors.left: parent.left
anchors.leftMargin: Style.marginS * scaling
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
anchors.fill: parent
color: Qt.rgba(Color.mSurface.r, Color.mSurface.g, Color.mSurface.b, Settings.data.bar.backgroundOpacity)
layer.enabled: true
}
Repeater {
model: Settings.data.bar.widgets.left
delegate: Loader {
active: true
sourceComponent: NWidgetLoader {
// ------------------------------
// Left Section - Dynamic Widgets
Row {
id: leftSection
objectName: "leftSection"
height: parent.height
anchors.left: parent.left
anchors.leftMargin: Style.marginS * scaling
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
Repeater {
model: Settings.data.bar.widgets.left
delegate: NWidgetLoader {
widgetName: modelData
widgetProps: {
"screen": screen
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen),
"barSection": parent.objectName,
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.left.length
}
anchors.verticalCenter: parent.verticalCenter
}
anchors.verticalCenter: parent.verticalCenter
}
}
}
// ------------------------------
// Center Section - Dynamic Widgets
Row {
id: centerSection
// ------------------------------
// Center Section - Dynamic Widgets
Row {
id: centerSection
objectName: "centerSection"
height: parent.height
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
height: parent.height
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
Repeater {
model: Settings.data.bar.widgets.center
delegate: NWidgetLoader {
Repeater {
model: Settings.data.bar.widgets.center
delegate: Loader {
active: true
sourceComponent: NWidgetLoader {
widgetName: modelData
widgetProps: {
"screen": screen
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen),
"barSection": parent.objectName,
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.center.length
}
anchors.verticalCenter: parent.verticalCenter
}
anchors.verticalCenter: parent.verticalCenter
}
}
}
// ------------------------------
// Right Section - Dynamic Widgets
Row {
id: rightSection
// ------------------------------
// Right Section - Dynamic Widgets
Row {
id: rightSection
objectName: "rightSection"
height: parent.height
anchors.right: bar.right
anchors.rightMargin: Style.marginS * scaling
anchors.verticalCenter: bar.verticalCenter
spacing: Style.marginS * scaling
height: parent.height
anchors.right: bar.right
anchors.rightMargin: Style.marginS * scaling
anchors.verticalCenter: bar.verticalCenter
spacing: Style.marginS * scaling
Repeater {
model: Settings.data.bar.widgets.right
delegate: Loader {
active: true
sourceComponent: NWidgetLoader {
Repeater {
model: Settings.data.bar.widgets.right
delegate: NWidgetLoader {
widgetName: modelData
widgetProps: {
"screen": screen
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen),
"barSection": parent.objectName,
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.right.length
}
anchors.verticalCenter: parent.verticalCenter
}
anchors.verticalCenter: parent.verticalCenter
}
}
}

View File

@@ -14,13 +14,25 @@ PopupWindow {
property real anchorY
property bool isSubMenu: false
property bool isHovered: rootMouseArea.containsMouse
property ShellScreen screen
property real scaling: ScalingService.getScreenScale(screen)
Connections {
target: ScalingService
function onScaleChanged(screenName, scale) {
if ((screen != null) && (screenName === screen.name)) {
scaling = scale
}
}
}
readonly property int menuWidth: 180
implicitWidth: menuWidth * scaling
// Use the content height of the Flickable for implicit height
implicitHeight: Math.min(Screen.height * 0.9, flickable.contentHeight + (Style.marginM * 2 * scaling))
implicitHeight: Math.min(screen ? screen.height * 0.9 : Screen.height * 0.9,
flickable.contentHeight + (Style.marginS * 2 * scaling))
visible: false
color: Color.transparent
anchor.item: anchorItem
@@ -212,7 +224,7 @@ PopupWindow {
// Check if there's enough space on the right
const globalPos = entry.mapToGlobal(0, 0)
const openLeft = (globalPos.x + entry.width + submenuWidth > Screen.width)
const openLeft = (globalPos.x + entry.width + submenuWidth > (screen ? screen.width : Screen.width))
// Position with overlap
const anchorX = openLeft ? -submenuWidth + overlap : entry.width - overlap
@@ -223,7 +235,8 @@ PopupWindow {
"anchorItem": entry,
"anchorX": anchorX,
"anchorY": 0,
"isSubMenu": true
"isSubMenu": true,
"screen": screen
})
if (entry.subMenu) {

View File

@@ -11,37 +11,14 @@ Row {
id: root
property ShellScreen screen
property real scaling: ScalingService.scale(screen)
property bool showingFullTitle: false
property int lastWindowIndex: -1
property real scaling: 1.0
readonly property real minWidth: 160
readonly property real maxWidth: 400
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
visible: getTitle() !== ""
// Timer to hide full title after window switch
Timer {
id: fullTitleTimer
interval: 2000
repeat: false
onTriggered: {
showingFullTitle = false
}
}
// Update text when window changes
Connections {
target: CompositorService
function onActiveWindowChanged() {
// Check if window actually changed
if (CompositorService.focusedWindowIndex !== lastWindowIndex) {
lastWindowIndex = CompositorService.focusedWindowIndex
showingFullTitle = true
fullTitleTimer.restart()
}
}
}
function getTitle() {
// Use the service's focusedWindowTitle property which is updated immediately
// when WindowOpenedOrChanged events are received
@@ -60,14 +37,15 @@ Row {
NText {
id: fullTitleMetrics
visible: false
text: titleText.text
font: titleText.font
text: getTitle()
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
}
Rectangle {
// Let the Rectangle size itself based on its content (the Row)
visible: root.visible
width: row.width + Style.marginM * scaling * 2
width: row.width + Style.marginM * 2 * scaling
height: Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant
@@ -79,11 +57,12 @@ Row {
anchors.fill: parent
anchors.leftMargin: Style.marginS * scaling
anchors.rightMargin: Style.marginS * scaling
clip: true
Row {
id: row
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginXS * scaling
spacing: Style.marginS * scaling
// Window icon
Item {
@@ -105,18 +84,23 @@ Row {
NText {
id: titleText
// If hovered or just switched window, show up to 400 pixels
// If not hovered show up to 150 pixels
width: (showingFullTitle || mouseArea.containsMouse) ? Math.min(fullTitleMetrics.contentWidth,
400 * scaling) : Math.min(
fullTitleMetrics.contentWidth, 150 * scaling)
// For short titles, show full. For long titles, truncate and expand on hover
width: {
if (mouseArea.containsMouse) {
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling))
} else {
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.minWidth * scaling))
}
}
horizontalAlignment: Text.AlignLeft
text: getTitle()
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
elide: Text.ElideRight
elide: mouseArea.containsMouse ? Text.ElideNone : Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
color: Color.mSecondary
clip: true
Behavior on width {
NumberAnimation {

View File

@@ -10,66 +10,71 @@ NIconButton {
id: root
property ShellScreen screen
property real scaling: ScalingService.scale(screen)
property real scaling: 1.0
sizeMultiplier: 0.8
sizeRatio: 0.8
colorBg: Color.mSurfaceVariant
colorFg: Color.mOnSurface
colorBorder: Color.transparent
colorBorderHover: Color.transparent
colorFg: {
if (!ArchUpdaterService.terminalAvailable || !ArchUpdaterService.aurHelperAvailable) {
return Color.mError
}
return (ArchUpdaterService.totalUpdates === 0) ? Color.mOnSurface : Color.mPrimary
}
// Enhanced icon states with better visual feedback
// Icon states
icon: {
if (ArchUpdaterService.busy)
if (!ArchUpdaterService.terminalAvailable) {
return "terminal"
}
if (!ArchUpdaterService.aurHelperAvailable) {
return "package"
}
if (ArchUpdaterService.aurBusy) {
return "sync"
if (ArchUpdaterService.updatePackages.length > 0) {
// Show different icons based on update count
const count = ArchUpdaterService.updatePackages.length
if (count > 50)
return "system_update_alt" // Many updates
if (count > 10)
return "system_update" // Moderate updates
return "system_update" // Few updates
}
if (ArchUpdaterService.totalUpdates > 0) {
return "system_update_alt"
}
return "task_alt"
}
// Enhanced tooltip with more information
// Tooltip with repo vs AUR breakdown and sample lists
tooltipText: {
if (ArchUpdaterService.busy)
return "Checking for updates…"
var count = ArchUpdaterService.updatePackages.length
if (count === 0)
return "System is up to date ✓"
var header = count === 1 ? "One package can be upgraded:" : (count + " packages can be upgraded:")
var list = ArchUpdaterService.updatePackages || []
var s = ""
var limit = Math.min(list.length, 8)
// Reduced to 8 for better readability
for (var i = 0; i < limit; ++i) {
var p = list[i]
s += (i ? "\n" : "") + (p.name + ": " + p.oldVersion + " → " + p.newVersion)
if (!ArchUpdaterService.terminalAvailable) {
return "Terminal not configured\nSet TERMINAL environment variable"
}
if (!ArchUpdaterService.aurHelperAvailable) {
return "AUR helper not found\nInstall yay or paru"
}
if (ArchUpdaterService.aurBusy) {
return "Checking for updates…"
}
if (list.length > 8)
s += "\n… and " + (list.length - 8) + " more"
return header + "\n\n" + s + "\n\nClick to update system"
const total = ArchUpdaterService.totalUpdates
if (total === 0) {
return "System is up to date ✓"
}
let header = (total === 1) ? "1 package can be updated" : (total + " packages can be updated")
const pacCount = ArchUpdaterService.updates
const aurCount = ArchUpdaterService.aurUpdates
const pacmanTooltip = (pacCount > 0) ? ((pacCount === 1) ? "1 system package" : pacCount + " system packages") : ""
const aurTooltip = (aurCount > 0) ? ((aurCount === 1) ? "1 AUR package" : aurCount + " AUR packages") : ""
let tooltip = header
if (pacmanTooltip !== "") {
tooltip += "\n" + pacmanTooltip
}
if (aurTooltip !== "") {
tooltip += "\n" + aurTooltip
}
return tooltip
}
// Enhanced click behavior with confirmation
onClicked: {
if (ArchUpdaterService.busy)
return
if (ArchUpdaterService.updatePackages.length > 0) {
// Show confirmation dialog for updates
PanelService.getPanel("archUpdaterPanel").toggle(screen)
} else {
// Just refresh if no updates available
ArchUpdaterService.doPoll()
}
// Always allow panel to open, never block
PanelService.getPanel("archUpdaterPanel").toggle(screen, this)
}
}

View File

@@ -10,19 +10,63 @@ Item {
id: root
property ShellScreen screen
property real scaling: ScalingService.scale(screen)
property real scaling: 1.0
property string barSection: ""
property int sectionWidgetIndex: 0
property int sectionWidgetsCount: 0
// Track if we've already notified to avoid spam
property bool hasNotifiedLowBattery: false
implicitWidth: pill.width
implicitHeight: pill.height
// Helper to evaluate and possibly notify
function maybeNotify(percent, charging) {
const p = Math.round(percent)
// Only notify exactly at 15%, not at 0% or any other percentage
if (!charging && p === 15 && !root.hasNotifiedLowBattery) {
Quickshell.execDetached(
["notify-send", "-u", "critical", "-i", "battery-caution", "Low Battery", `Battery is at ${p}%. Please connect charger.`])
root.hasNotifiedLowBattery = true
}
// Reset when charging starts or when battery recovers above 20%
if (charging || p > 20) {
root.hasNotifiedLowBattery = false
}
}
// Watch for battery changes
Connections {
target: UPower.displayDevice
function onPercentageChanged() {
let battery = UPower.displayDevice
let isReady = battery && battery.ready && battery.isLaptopBattery && battery.isPresent
let percent = isReady ? (battery.percentage * 100) : 0
let charging = isReady ? battery.state === UPowerDeviceState.Charging : false
root.maybeNotify(percent, charging)
}
function onStateChanged() {
let battery = UPower.displayDevice
let isReady = battery && battery.ready && battery.isLaptopBattery && battery.isPresent
let charging = isReady ? battery.state === UPowerDeviceState.Charging : false
// Reset notification flag when charging starts
if (charging) {
root.hasNotifiedLowBattery = false
}
}
}
NPill {
id: pill
// Test mode
property bool testMode: false
property int testPercent: 49
property int testPercent: 20
property bool testCharging: false
property var battery: UPower.displayDevice
property bool isReady: testMode ? true : (battery && battery.ready && battery.isLaptopBattery && battery.isPresent)
property real percent: testMode ? testPercent : (isReady ? (battery.percentage * 100) : 0)
@@ -30,16 +74,12 @@ Item {
// Choose icon based on charge and charging state
function batteryIcon() {
if (!isReady || !battery.isLaptopBattery)
return "battery_android_alert"
if (charging)
return "battery_android_bolt"
if (percent >= 95)
return "battery_android_full"
// Hardcoded battery symbols
if (percent >= 85)
return "battery_android_6"
@@ -57,45 +97,43 @@ Item {
return "battery_android_0"
}
rightOpen: BarWidgetRegistry.getNPillDirection(root)
icon: batteryIcon()
text: (isReady && battery.isLaptopBattery) ? Math.round(percent) + "%" : "-"
text: ((isReady && battery.isLaptopBattery) || testMode) ? Math.round(percent) + "%" : "-"
textColor: charging ? Color.mPrimary : Color.mOnSurface
forceOpen: isReady && battery.isLaptopBattery && Settings.data.bar.alwaysShowBatteryPercentage
disableOpen: (!isReady || !battery.isLaptopBattery)
iconCircleColor: Color.mPrimary
collapsedIconColor: Color.mOnSurface
autoHide: false
forceOpen: isReady && (testMode || battery.isLaptopBattery) && Settings.data.bar.alwaysShowBatteryPercentage
disableOpen: (!isReady || (!testMode && !battery.isLaptopBattery))
tooltipText: {
let lines = []
if (testMode) {
lines.push("Time Left: " + Time.formatVagueHumanReadableDuration(12345))
lines.push("Time left: " + Time.formatVagueHumanReadableDuration(12345))
return lines.join("\n")
}
if (!isReady || !battery.isLaptopBattery) {
return "No Battery Detected"
return "No battery detected"
}
if (battery.timeToEmpty > 0) {
lines.push("Time Left: " + Time.formatVagueHumanReadableDuration(battery.timeToEmpty))
lines.push("Time left: " + Time.formatVagueHumanReadableDuration(battery.timeToEmpty))
}
if (battery.timeToFull > 0) {
lines.push("Time Until Full: " + Time.formatVagueHumanReadableDuration(battery.timeToFull))
lines.push("Time until full: " + Time.formatVagueHumanReadableDuration(battery.timeToFull))
}
if (battery.changeRate !== undefined) {
const rate = battery.changeRate
if (rate > 0) {
lines.push(charging ? "Charging Rate: " + rate.toFixed(2) + " W" : "Discharging Rate: " + rate.toFixed(
lines.push(charging ? "Charging rate: " + rate.toFixed(2) + " W" : "Discharging rate: " + rate.toFixed(
2) + " W")
} else if (rate < 0) {
lines.push("Discharging Rate: " + Math.abs(rate).toFixed(2) + " W")
lines.push("Discharging rate: " + Math.abs(rate).toFixed(2) + " W")
} else {
lines.push("Estimating...")
}
} else {
lines.push(charging ? "Charging" : "Discharging")
}
if (battery.healthPercentage !== undefined && battery.healthPercentage > 0) {
lines.push("Health: " + Math.round(battery.healthPercentage) + "%")
}

View File

@@ -11,25 +11,16 @@ NIconButton {
id: root
property ShellScreen screen
property real scaling: ScalingService.scale(screen)
property real scaling: 1.0
visible: Settings.data.network.bluetoothEnabled
sizeMultiplier: 0.8
sizeRatio: 0.8
colorBg: Color.mSurfaceVariant
colorFg: Color.mOnSurface
colorBorder: Color.transparent
colorBorderHover: Color.transparent
icon: {
// Show different icons based on connection status
if (BluetoothService.pairedDevices.length > 0) {
return "bluetooth_connected"
} else if (BluetoothService.discovering) {
return "bluetooth_searching"
} else {
return "bluetooth"
}
}
tooltipText: "Bluetooth Devices"
onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(screen)
icon: "bluetooth"
tooltipText: "Bluetooth devices"
onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(screen, this)
}

View File

@@ -9,7 +9,10 @@ Item {
id: root
property ShellScreen screen
property real scaling: ScalingService.scale(screen)
property real scaling: 1.0
property string barSection: ""
property int sectionWidgetIndex: 0
property int sectionWidgetsCount: 0
// Used to avoid opening the pill on Quickshell startup
property bool firstBrightnessReceived: false
@@ -58,6 +61,8 @@ Item {
NPill {
id: pill
rightOpen: BarWidgetRegistry.getNPillDirection(root)
icon: getIcon()
iconCircleColor: Color.mPrimary
collapsedIconColor: Color.mOnSurface
@@ -86,6 +91,7 @@ Item {
}
onClicked: {
var settingsPanel = PanelService.getPanel("settingsPanel")
settingsPanel.requestedTab = SettingsPanel.Tab.Brightness
settingsPanel.open(screen)
}

View File

@@ -8,7 +8,7 @@ Rectangle {
id: root
property ShellScreen screen
property real scaling: ScalingService.scale(screen)
property real scaling: 1.0
implicitWidth: clock.width + Style.marginM * 2 * scaling
implicitHeight: Math.round(Style.capsuleHeight * scaling)
@@ -38,7 +38,7 @@ Rectangle {
}
onClicked: {
tooltip.hide()
PanelService.getPanel("calendarPanel")?.toggle(screen)
PanelService.getPanel("calendarPanel")?.toggle(screen, this)
}
}
}

View File

@@ -10,7 +10,10 @@ Row {
id: root
property ShellScreen screen
property real scaling: ScalingService.scale(screen)
property real scaling: 1.0
property string barSection: ""
property int sectionWidgetIndex: 0
property int sectionWidgetsCount: 0
// Use the shared service for keyboard layout
property string currentLayout: KeyboardLayoutService.currentLayout
@@ -20,12 +23,14 @@ Row {
NPill {
id: pill
rightOpen: BarWidgetRegistry.getNPillDirection(root)
icon: "keyboard_alt"
iconCircleColor: Color.mPrimary
collapsedIconColor: Color.mOnSurface
autoHide: false // Important to be false so we can hover as long as we want
text: currentLayout
tooltipText: "Keyboard Layout: " + currentLayout
tooltipText: "Keyboard layout: " + currentLayout
onClicked: {

View File

@@ -11,7 +11,9 @@ Row {
id: root
property ShellScreen screen
property real scaling: ScalingService.scale(screen)
property real scaling: 1.0
readonly property real minWidth: 160
readonly property real maxWidth: 400
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
@@ -31,8 +33,10 @@ Row {
}
Rectangle {
id: mediaMini
// Let the Rectangle size itself based on its content (the Row)
width: row.width + Style.marginM * scaling * 2
width: row.width + Style.marginM * 2 * scaling
height: Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling)
@@ -40,6 +44,13 @@ Row {
anchors.verticalCenter: parent.verticalCenter
// Used to anchor the tooltip, so the tooltip does not move when the content expands
Item {
id: anchor
height: parent.height
width: 200 * scaling
}
Item {
id: mainContainer
anchors.fill: parent
@@ -50,7 +61,7 @@ Row {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "linear"
&& MediaService.isPlaying
&& MediaService.isPlaying && MediaService.trackLength > 0
z: 0
sourceComponent: LinearSpectrum {
@@ -65,7 +76,7 @@ Row {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "mirrored"
&& MediaService.isPlaying
&& MediaService.isPlaying && MediaService.trackLength > 0
z: 0
sourceComponent: MirroredSpectrum {
@@ -81,7 +92,7 @@ Row {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "wave"
&& MediaService.isPlaying
&& MediaService.isPlaying && MediaService.trackLength > 0
z: 0
sourceComponent: WaveSpectrum {
@@ -97,7 +108,7 @@ Row {
Row {
id: row
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginXS * scaling
spacing: Style.marginS * scaling
z: 1 // Above the visualizer
NIcon {
@@ -113,46 +124,32 @@ Row {
anchors.verticalCenter: parent.verticalCenter
visible: Settings.data.audio.showMiniplayerAlbumArt
Rectangle {
width: 18 * scaling
height: 18 * scaling
radius: width * 0.5
color: Color.transparent
antialiasing: true
clip: true
Item {
width: Math.round(18 * scaling)
height: Math.round(18 * scaling)
NImageCircled {
id: trackArt
visible: MediaService.trackArtUrl.toString() !== ""
anchors.fill: parent
anchors.verticalCenter: parent.verticalCenter
anchors.margins: scaling
imagePath: MediaService.trackArtUrl
fallbackIcon: MediaService.isPlaying ? "pause" : "play_arrow"
borderWidth: 0
border.color: Color.transparent
}
// Fallback icon when no album art available
NIcon {
id: windowIconFallback
text: MediaService.isPlaying ? "pause" : "play_arrow"
font.pointSize: Style.fontSizeL * scaling
verticalAlignment: Text.AlignVCenter
anchors.verticalCenter: parent.verticalCenter
visible: getTitle() !== "" && !trackArt.visible
}
}
}
NText {
id: titleText
// If hovered or just switched window, show up to 400 pixels
// If not hovered show up to 150 pixels
width: (mouseArea.containsMouse) ? Math.min(fullTitleMetrics.contentWidth,
400 * scaling) : Math.min(fullTitleMetrics.contentWidth,
150 * scaling)
// For short titles, show full. For long titles, truncate and expand on hover
width: {
if (mouseArea.containsMouse) {
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling))
} else {
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.minWidth * scaling))
}
}
text: getTitle()
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
@@ -176,8 +173,46 @@ Row {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: MediaService.playPause()
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
MediaService.playPause()
} else if (mouse.button == Qt.RightButton) {
MediaService.next()
// Need to hide the tooltip instantly
tooltip.visible = false
} else if (mouse.button == Qt.MiddleButton) {
MediaService.previous()
// Need to hide the tooltip instantly
tooltip.visible = false
}
}
onEntered: {
if (tooltip.text !== "") {
tooltip.show()
}
}
onExited: {
tooltip.hide()
}
}
}
}
NTooltip {
id: tooltip
text: {
var str = ""
if (MediaService.canGoNext) {
str += "Right click for next\n"
}
if (MediaService.canGoPrevious) {
str += "Middle click for previous\n"
}
return str
}
target: anchor
positionAbove: Settings.data.bar.position === "bottom"
}
}

View File

@@ -0,0 +1,109 @@
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Services.Pipewire
import qs.Commons
import qs.Modules.SettingsPanel
import qs.Services
import qs.Widgets
Item {
id: root
property ShellScreen screen
property real scaling: 1.0
property string barSection: ""
property int sectionWidgetIndex: 0
property int sectionWidgetsCount: 0
// Used to avoid opening the pill on Quickshell startup
property bool firstInputVolumeReceived: false
property int wheelAccumulator: 0
implicitWidth: pill.width
implicitHeight: pill.height
function getIcon() {
if (AudioService.inputMuted) {
return "mic_off"
}
return AudioService.inputVolume <= Number.EPSILON ? "mic_off" : (AudioService.inputVolume < 0.33 ? "mic" : "mic")
}
// Connection used to open the pill when input volume changes
Connections {
target: AudioService.source?.audio ? AudioService.source?.audio : null
function onVolumeChanged() {
// Logger.log("Bar:Microphone", "onInputVolumeChanged")
if (!firstInputVolumeReceived) {
// Ignore the first volume change
firstInputVolumeReceived = true
} else {
pill.show()
externalHideTimer.restart()
}
}
}
// Connection used to open the pill when input mute state changes
Connections {
target: AudioService.source?.audio ? AudioService.source?.audio : null
function onMutedChanged() {
// Logger.log("Bar:Microphone", "onInputMutedChanged")
if (!firstInputVolumeReceived) {
// Ignore the first mute change
firstInputVolumeReceived = true
} else {
pill.show()
externalHideTimer.restart()
}
}
}
Timer {
id: externalHideTimer
running: false
interval: 1500
onTriggered: {
pill.hide()
}
}
NPill {
id: pill
rightOpen: BarWidgetRegistry.getNPillDirection(root)
icon: getIcon()
iconCircleColor: Color.mPrimary
collapsedIconColor: Color.mOnSurface
autoHide: false // Important to be false so we can hover as long as we want
text: Math.floor(AudioService.inputVolume * 100) + "%"
tooltipText: "Microphone: " + Math.round(AudioService.inputVolume * 100)
+ "%\nLeft click for advanced settings.\nScroll up/down to change volume.\nRight click to toggle mute."
onWheel: function (delta) {
wheelAccumulator += delta
if (wheelAccumulator >= 120) {
wheelAccumulator = 0
AudioService.setInputVolume(AudioService.inputVolume + AudioService.stepVolume)
} else if (wheelAccumulator <= -120) {
wheelAccumulator = 0
AudioService.setInputVolume(AudioService.inputVolume - AudioService.stepVolume)
}
}
onClicked: {
var settingsPanel = PanelService.getPanel("settingsPanel")
settingsPanel.requestedTab = SettingsPanel.Tab.AudioService
settingsPanel.open(screen)
}
onRightClicked: {
AudioService.setInputMuted(!AudioService.inputMuted)
}
}
Process {
id: pwvucontrolProcess
command: ["pwvucontrol"]
running: false
}
}

View File

@@ -0,0 +1,32 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Widgets
NIconButton {
id: root
property ShellScreen screen
property real scaling: 1.0
sizeRatio: 0.8
colorBg: Color.mSurfaceVariant
colorFg: Color.mOnSurface
colorBorder: Color.transparent
colorBorderHover: Color.transparent
icon: Settings.data.nightLight.enabled ? "bedtime" : "bedtime_off"
tooltipText: `Night light: ${Settings.data.nightLight.enabled ? "enabled" : "disabled"}\nLeft click to toggle.\nRight click to access settings.`
onClicked: Settings.data.nightLight.enabled = !Settings.data.nightLight.enabled
onRightClicked: {
var settingsPanel = PanelService.getPanel("settingsPanel")
settingsPanel.requestedTab = SettingsPanel.Tab.Display
settingsPanel.open(screen)
}
}

View File

@@ -11,14 +11,14 @@ NIconButton {
id: root
property ShellScreen screen
property real scaling: ScalingService.scale(screen)
property real scaling: 1.0
sizeMultiplier: 0.8
sizeRatio: 0.8
icon: "notifications"
tooltipText: "Notification History"
tooltipText: "Notification history"
colorBg: Color.mSurfaceVariant
colorFg: Color.mOnSurface
colorBorder: Color.transparent
colorBorderHover: Color.transparent
onClicked: PanelService.getPanel("notificationHistoryPanel")?.toggle(screen)
onClicked: PanelService.getPanel("notificationHistoryPanel")?.toggle(screen, this)
}

View File

@@ -10,11 +10,11 @@ NIconButton {
id: root
property ShellScreen screen
property real scaling: ScalingService.scale(screen)
property real scaling: 1.0
property var powerProfiles: PowerProfiles
readonly property bool hasPP: powerProfiles.hasPerformanceProfile
sizeMultiplier: 0.8
sizeRatio: 0.8
visible: hasPP
function profileIcon() {

View File

@@ -8,12 +8,12 @@ NIconButton {
id: root
property ShellScreen screen
property real scaling: ScalingService.scale(screen)
property real scaling: 1.0
visible: ScreenRecorderService.isRecording
icon: "videocam"
tooltipText: "Screen Recording Active\nClick To Stop Recording"
sizeMultiplier: 0.8
tooltipText: "Screen recording is active\nClick to stop recording"
sizeRatio: 0.8
colorBg: Color.mPrimary
colorFg: Color.mOnPrimary
anchors.verticalCenter: parent.verticalCenter

View File

@@ -7,11 +7,11 @@ NIconButton {
id: root
property ShellScreen screen
property real scaling: ScalingService.scale(screen)
property real scaling: 1.0
icon: "widgets"
tooltipText: "Open Side Panel"
sizeMultiplier: 0.8
tooltipText: "Open side panel"
sizeRatio: 0.8
colorBg: Color.mSurfaceVariant
colorFg: Color.mOnSurface

View File

@@ -8,7 +8,7 @@ Row {
id: root
property ShellScreen screen
property real scaling: ScalingService.scale(screen)
property real scaling: 1.0
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
@@ -46,6 +46,7 @@ Row {
NText {
id: cpuUsageText
text: `${SystemStatService.cpuUsage}%`
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
anchors.verticalCenter: parent.verticalCenter
@@ -67,6 +68,7 @@ Row {
NText {
text: `${SystemStatService.cpuTemp}°C`
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
anchors.verticalCenter: parent.verticalCenter
@@ -87,6 +89,51 @@ Row {
NText {
text: `${SystemStatService.memoryUsageGb}G`
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
}
// Network Download Speed Component
Row {
id: networkDownloadLayout
spacing: Style.marginXS * scaling
visible: Settings.data.bar.showNetworkStats
NIcon {
text: "download"
anchors.verticalCenter: parent.verticalCenter
}
NText {
text: SystemStatService.formatSpeed(SystemStatService.rxSpeed)
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
}
// Network Upload Speed Component
Row {
id: networkUploadLayout
spacing: Style.marginXS * scaling
visible: Settings.data.bar.showNetworkStats
NIcon {
text: "upload"
anchors.verticalCenter: parent.verticalCenter
}
NText {
text: SystemStatService.formatSpeed(SystemStatService.txSpeed)
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
anchors.verticalCenter: parent.verticalCenter

View File

@@ -0,0 +1,99 @@
pragma ComponentBehavior
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Widgets
Rectangle {
id: root
property ShellScreen screen
property real scaling: 1.0
readonly property real itemSize: Style.baseWidgetSize * 0.8 * scaling
// Always visible when there are toplevels
implicitWidth: taskbarRow.width + Style.marginM * scaling * 2
implicitHeight: Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant
Row {
id: taskbarRow
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
spacing: Style.marginXXS * root.scaling
Repeater {
model: ToplevelManager && ToplevelManager.toplevels ? ToplevelManager.toplevels : []
delegate: Item {
id: taskbarItem
required property Toplevel modelData
property Toplevel toplevel: modelData
property bool isActive: ToplevelManager.activeToplevel === modelData
width: root.itemSize
height: root.itemSize
Rectangle {
id: iconBackground
anchors.centerIn: parent
width: root.itemSize * 0.75
height: root.itemSize * 0.75
color: taskbarItem.isActive ? Color.mPrimary : root.color
border.width: 0
radius: Math.round(Style.radiusXS * root.scaling)
border.color: "transparent"
z: -1
IconImage {
id: appIcon
anchors.centerIn: parent
width: Style.marginL * root.scaling
height: Style.marginL * root.scaling
source: Icons.iconForAppId(taskbarItem.modelData.appId)
smooth: true
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onPressed: function (mouse) {
if (!taskbarItem.modelData)
return
if (mouse.button === Qt.LeftButton) {
try {
taskbarItem.modelData.activate()
} catch (error) {
Logger.error("Taskbar", "Failed to activate toplevel: " + error)
}
} else if (mouse.button === Qt.RightButton) {
try {
taskbarItem.modelData.close()
} 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"
}
}
}
}
}

View File

@@ -14,9 +14,17 @@ Rectangle {
id: root
property ShellScreen screen
property real scaling: ScalingService.scale(screen)
property real scaling: 1.0
readonly property real itemSize: 24 * scaling
function onLoaded() {
// When the widget is fully initialized with its props
// set the screen for the trayMenu
if (trayMenu.item) {
trayMenu.item.screen = screen
}
}
visible: SystemTray.items.values.length > 0
implicitWidth: tray.width + Style.marginM * scaling * 2
implicitHeight: Math.round(Style.capsuleHeight * scaling)
@@ -104,7 +112,7 @@ Rectangle {
// Anchor the menu to the tray icon item (parent) and position it below the icon
const menuX = (width / 2) - (trayMenu.item.width / 2)
const menuY = (Style.barHeight * scaling)
const menuY = Math.round(Style.barHeight * scaling)
trayMenu.item.menu = modelData.menu
trayMenu.item.showAt(parent, menuX, menuY)
} else {

View File

@@ -1,5 +1,6 @@
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Services.Pipewire
import qs.Commons
import qs.Modules.SettingsPanel
@@ -10,10 +11,14 @@ Item {
id: root
property ShellScreen screen
property real scaling: ScalingService.scale(screen)
property real scaling: 1.0
property string barSection: ""
property int sectionWidgetIndex: 0
property int sectionWidgetsCount: 0
// Used to avoid opening the pill on Quickshell startup
property bool firstVolumeReceived: false
property int wheelAccumulator: 0
implicitWidth: pill.width
implicitHeight: pill.height
@@ -51,6 +56,8 @@ Item {
NPill {
id: pill
rightOpen: BarWidgetRegistry.getNPillDirection(root)
icon: getIcon()
iconCircleColor: Color.mPrimary
collapsedIconColor: Color.mOnSurface
@@ -59,10 +66,13 @@ Item {
tooltipText: "Volume: " + Math.round(
AudioService.volume * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume."
onWheel: function (angle) {
if (angle > 0) {
onWheel: function (delta) {
wheelAccumulator += delta
if (wheelAccumulator >= 120) {
wheelAccumulator = 0
AudioService.increaseVolume()
} else if (angle < 0) {
} else if (wheelAccumulator <= -120) {
wheelAccumulator = 0
AudioService.decreaseVolume()
}
}
@@ -71,5 +81,14 @@ Item {
settingsPanel.requestedTab = SettingsPanel.Tab.AudioService
settingsPanel.open(screen)
}
onRightClicked: {
pwvucontrolProcess.running = true
}
}
Process {
id: pwvucontrolProcess
command: ["pwvucontrol"]
running: false
}
}

View File

@@ -11,11 +11,11 @@ NIconButton {
id: root
property ShellScreen screen
property real scaling: ScalingService.scale(screen)
property real scaling: 1.0
visible: Settings.data.network.wifiEnabled
sizeMultiplier: 0.8
sizeRatio: 0.8
Component.onCompleted: {
Logger.log("WiFi", "Widget component completed")
@@ -32,8 +32,9 @@ NIconButton {
icon: {
try {
if (NetworkService.ethernet)
if (NetworkService.ethernet) {
return "lan"
}
let connected = false
let signalStrength = 0
for (const net in NetworkService.networks) {
@@ -49,13 +50,6 @@ NIconButton {
return "signal_wifi_bad"
}
}
tooltipText: "Network / WiFi"
onClicked: {
try {
Logger.log("WiFi", "Button clicked, toggling panel")
PanelService.getPanel("wifiPanel")?.toggle(screen)
} catch (error) {
Logger.error("WiFi", "Error toggling panel:", error)
}
}
tooltipText: "Network / Wi-Fi"
onClicked: PanelService.getPanel("wifiPanel")?.toggle(screen, this)
}

View File

@@ -12,7 +12,7 @@ Item {
id: root
property ShellScreen screen: null
property real scaling: ScalingService.scale(screen)
property real scaling: 1.0
property bool isDestroying: false
property bool hovered: false
@@ -27,23 +27,27 @@ Item {
signal workspaceChanged(int workspaceId, color accentColor)
implicitHeight: Math.round(36 * scaling)
implicitHeight: Math.round(Style.barHeight * scaling)
implicitWidth: {
let total = 0
for (var i = 0; i < localWorkspaces.count; i++) {
const ws = localWorkspaces.get(i)
if (ws.isFocused)
total += Math.round(44 * scaling)
else if (ws.isActive)
total += Math.round(28 * scaling)
else
total += Math.round(16 * scaling)
total += calculatedWsWidth(ws)
}
total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills
total += horizontalPadding * 2
return total
}
function calculatedWsWidth(ws) {
if (ws.isFocused)
return Math.round(44 * scaling)
else if (ws.isActive)
return Math.round(28 * scaling)
else
return Math.round(20 * scaling)
}
Component.onCompleted: {
refreshWorkspaces()
}
@@ -103,7 +107,7 @@ Item {
property: "masterProgress"
from: 0.0
to: 1.0
duration: 1000
duration: Style.animationSlow * 2
easing.type: Easing.OutQuint
}
PropertyAction {
@@ -148,26 +152,50 @@ Item {
model: localWorkspaces
Item {
id: workspacePillContainer
height: Math.round(12 * scaling)
width: {
if (model.isFocused)
return Math.round(44 * scaling)
else if (model.isActive)
return Math.round(28 * scaling)
else
return Math.round(16 * scaling)
}
height: (Settings.data.bar.showWorkspaceLabel !== "none") ? Math.round(18 * scaling) : Math.round(14 * scaling)
width: root.calculatedWsWidth(model)
Rectangle {
id: workspacePill
id: pill
anchors.fill: parent
radius: {
if (model.isFocused)
return Math.round(12 * scaling)
else
// half of focused height (if you want to animate this too)
return Math.round(6 * scaling)
Loader {
active: (Settings.data.bar.showWorkspaceLabel !== "none")
sourceComponent: Component {
Text {
// Center horizontally
x: (pill.width - width) / 2
// Center vertically accounting for font metrics
y: (pill.height - height) / 2 + (height - contentHeight) / 2
text: {
if (Settings.data.bar.showWorkspaceLabel === "name" && model.name && model.name.length > 0) {
return model.name.substring(0, 2)
} else {
return model.idx.toString()
}
}
font.pointSize: model.isFocused ? Style.fontSizeXS * scaling : Style.fontSizeXXS * scaling
font.capitalization: Font.AllUppercase
font.family: Settings.data.ui.fontFixed
font.weight: Style.fontWeightBold
wrapMode: Text.Wrap
color: {
if (model.isFocused)
return Color.mOnPrimary
if (model.isUrgent)
return Color.mOnError
if (model.isActive || model.isOccupied)
return Color.mOnSecondary
if (model.isUrgent)
return Color.mOnError
return Color.mOnSurface
}
}
}
}
radius: width * 0.5
color: {
if (model.isFocused)
return Color.mPrimary

View File

@@ -0,0 +1,306 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Bluetooth
import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Widgets
ColumnLayout {
id: root
property string label: ""
property string tooltipText: ""
property var model: {
}
Layout.fillWidth: true
spacing: Style.marginM * scaling
NText {
text: root.label
font.pointSize: Style.fontSizeL * scaling
color: Color.mSecondary
font.weight: Style.fontWeightMedium
Layout.fillWidth: true
visible: root.model.length > 0
}
Repeater {
id: deviceList
Layout.fillWidth: true
model: root.model
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
Rectangle {
id: bluetoothDeviceRectangle
property bool canConnect: BluetoothService.canConnect(modelData)
property bool canDisconnect: BluetoothService.canDisconnect(modelData)
property bool isBusy: BluetoothService.isDeviceBusy(modelData)
Layout.fillWidth: true
Layout.preferredHeight: 64 * scaling + (10 * scaling * modelData.batteryAvailable)
radius: Style.radiusM * scaling
color: {
if (availableDeviceArea.containsMouse) {
if (canDisconnect && !isBusy)
return Color.mError
if (!isBusy)
return Color.mTertiary
return Color.mPrimary
}
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mPrimary
if (modelData.blocked)
return Color.mError
return Color.mSurfaceVariant
}
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
NTooltip {
id: tooltip
target: bluetoothDeviceRectangle
positionAbove: Settings.data.bar.position === "bottom"
text: root.tooltipText
}
RowLayout {
anchors.fill: parent
anchors.margins: Style.marginM * scaling
spacing: Style.marginS * scaling
Layout.alignment: Qt.AlignVCenter
// One device BT icon
NIcon {
text: BluetoothService.getDeviceIcon(modelData)
font.pointSize: Style.fontSizeXXL * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
Layout.alignment: Qt.AlignVCenter
}
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginXXS * scaling
// Device name
NText {
text: modelData.name || modelData.deviceName
font.pointSize: Style.fontSizeM * scaling
elide: Text.ElideRight
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
font.weight: Style.fontWeightMedium
Layout.fillWidth: true
}
// Signal Strength
RowLayout {
Layout.fillWidth: true
spacing: Style.marginXS * scaling
// Device signal strength - "Unknown" when not connected
NText {
text: BluetoothService.getSignalStrength(modelData)
font.pointSize: Style.fontSizeXS * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurfaceVariant
}
}
NIcon {
text: BluetoothService.getSignalIcon(modelData)
font.pointSize: Style.fontSizeXS * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0 && !modelData.pairing
&& !modelData.blocked
}
NText {
text: (modelData.signalStrength !== undefined
&& modelData.signalStrength > 0) ? modelData.signalStrength + "%" : ""
font.pointSize: Style.fontSizeXS * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0 && !modelData.pairing
&& !modelData.blocked
}
}
NText {
visible: modelData.batteryAvailable
text: BluetoothService.getBattery(modelData)
font.pointSize: Style.fontSizeXS * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurfaceVariant
}
}
}
// Spacer to push connect button to the right
Item {
Layout.fillWidth: true
}
// Call to action
Rectangle {
Layout.preferredWidth: 80 * scaling
Layout.preferredHeight: 28 * scaling
radius: Style.radiusM * scaling
visible: (modelData.state !== BluetoothDeviceState.Connecting)
color: Color.transparent
border.color: {
if (availableDeviceArea.containsMouse) {
return Color.mOnTertiary
}
if (bluetoothDeviceRectangle.canDisconnect && !isBusy) {
return Color.mError
}
return Color.mPrimary
}
border.width: Math.max(1, Style.borderS * scaling)
opacity: canConnect || isBusy || canDisconnect ? 1 : 0.5
NText {
anchors.centerIn: parent
text: {
if (modelData.pairing) {
return "Pairing..."
}
if (modelData.blocked) {
return "Blocked"
}
if (modelData.connected) {
return "Disconnect"
}
return "Connect"
}
font.pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightMedium
color: {
if (availableDeviceArea.containsMouse) {
return Color.mOnTertiary
}
if (bluetoothDeviceRectangle.canDisconnect && !isBusy) {
return Color.mError
} else {
return Color.mPrimary
}
}
}
}
}
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

@@ -45,7 +45,7 @@ NPanel {
NIconButton {
icon: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop_circle" : "refresh"
tooltipText: "Refresh Devices"
sizeMultiplier: 0.8
sizeRatio: 0.8
onClicked: {
if (BluetoothService.adapter) {
BluetoothService.adapter.discovering = !BluetoothService.adapter.discovering
@@ -56,7 +56,7 @@ NPanel {
NIconButton {
icon: "close"
tooltipText: "Close"
sizeMultiplier: 0.8
sizeRatio: 0.8
onClicked: {
root.close()
}
@@ -68,259 +68,70 @@ NPanel {
}
ScrollView {
id: scrollView
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
clip: true
contentWidth: availableWidth
// Available devices
Column {
id: column
ColumnLayout {
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
width: parent.width
spacing: Style.marginM * scaling
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
RowLayout {
width: parent.width
spacing: Style.marginM * scaling
NText {
text: "Available Devices"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
font.weight: Style.fontWeightMedium
}
}
Repeater {
model: {
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices)
// Connected devices
BluetoothDevicesList {
label: "Connected devices"
property var items: {
if (!BluetoothService.adapter || !Bluetooth.devices)
return []
var filtered = Bluetooth.devices.values.filter(dev => {
return dev && !dev.paired && !dev.pairing && !dev.blocked
&& (dev.signalStrength === undefined
|| dev.signalStrength > 0)
})
var filtered = Bluetooth.devices.values.filter(dev => dev && !dev.blocked && dev.connected)
return BluetoothService.sortDevices(filtered)
}
Rectangle {
property bool canConnect: BluetoothService.canConnect(modelData)
property bool isBusy: BluetoothService.isDeviceBusy(modelData)
width: parent.width
height: 70
radius: Style.radiusM * scaling
color: {
if (availableDeviceArea.containsMouse && !isBusy)
return Color.mTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mPrimary
if (modelData.blocked)
return Color.mError
return Color.mSurfaceVariant
}
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
Row {
anchors.left: parent.left
anchors.leftMargin: Style.marginM * scaling
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
// One device BT icon
NIcon {
text: BluetoothService.getDeviceIcon(modelData)
font.pointSize: Style.fontSizeXXL * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: Style.marginXXS * scaling
anchors.verticalCenter: parent.verticalCenter
// One device name
NText {
text: modelData.name || modelData.deviceName
font.pointSize: Style.fonttSizeMedium * scaling
elide: Text.ElideRight
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
font.weight: Style.fontWeightMedium
}
Row {
spacing: Style.marginXS * scaling
Row {
spacing: Style.marginS * spacing
// One device signal strength - "Unknown" when not connected
NText {
text: {
if (modelData.pairing)
return "Pairing..."
if (modelData.blocked)
return "Blocked"
return BluetoothService.getSignalStrength(modelData)
}
font.pointSize: Style.fontSizeXS * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
}
NIcon {
text: BluetoothService.getSignalIcon(modelData)
font.pointSize: Style.fontSizeXS * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0
&& !modelData.pairing && !modelData.blocked
}
NText {
text: (modelData.signalStrength !== undefined
&& modelData.signalStrength > 0) ? modelData.signalStrength + "%" : ""
font.pointSize: Style.fontSizeXS * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0
&& !modelData.pairing && !modelData.blocked
}
}
}
}
}
Rectangle {
width: 80 * scaling
height: 28 * scaling
radius: Style.radiusM * scaling
anchors.right: parent.right
anchors.rightMargin: Style.marginM * scaling
anchors.verticalCenter: parent.verticalCenter
visible: modelData.state !== BluetoothDeviceState.Connecting
color: Color.transparent
border.color: {
if (availableDeviceArea.containsMouse) {
return Color.mOnTertiary
} else {
return Color.mPrimary
}
}
border.width: Math.max(1, Style.borderS * scaling)
opacity: canConnect || isBusy ? 1 : 0.5
// On device connect button
NText {
anchors.centerIn: parent
text: {
if (modelData.pairing)
return "Pairing..."
if (modelData.blocked)
return "Blocked"
return "Connect"
}
font.pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightMedium
color: {
if (availableDeviceArea.containsMouse) {
return Color.mOnTertiary
} else {
return Color.mPrimary
}
}
}
}
MouseArea {
id: availableDeviceArea
anchors.fill: parent
hoverEnabled: true
cursorShape: canConnect && !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor)
enabled: canConnect && !isBusy
onClicked: {
if (modelData)
BluetoothService.connectDeviceWithTrust(modelData)
}
}
}
model: items
visible: items.length > 0
Layout.fillWidth: true
}
// Fallback if nothing available
Column {
width: parent.width
// Known devices
BluetoothDevicesList {
label: "Known devices"
tooltipText: "Left click to connect, right click to forget"
property var items: {
if (!BluetoothService.adapter || !Bluetooth.devices)
return []
var filtered = Bluetooth.devices.values.filter(dev => dev && !dev.blocked && !dev.connected
&& (dev.paired || dev.trusted))
return BluetoothService.sortDevices(filtered)
}
model: items
visible: items.length > 0
Layout.fillWidth: true
}
// Available devices
BluetoothDevicesList {
label: "Available devices"
property var items: {
if (!BluetoothService.adapter || !Bluetooth.devices)
return []
var filtered = Bluetooth.devices.values.filter(dev => dev && !dev.blocked && !dev.paired && !dev.trusted)
return BluetoothService.sortDevices(filtered)
}
model: items
visible: items.length > 0
Layout.fillWidth: true
}
// Fallback
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
visible: {
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices)
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices) {
return false
}
var availableCount = Bluetooth.devices.values.filter(dev => {
return dev && !dev.paired && !dev.pairing
@@ -328,25 +139,24 @@ NPanel {
&& (dev.signalStrength === undefined
|| dev.signalStrength > 0)
}).length
return availableCount === 0
return (availableCount === 0)
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: Style.marginM * scaling
NIcon {
text: "sync"
font.pointSize: Style.fontSizeXLL * 1.5 * scaling
color: Color.mPrimary
anchors.verticalCenter: parent.verticalCenter
RotationAnimation on rotation {
running: true
loops: Animation.Infinite
from: 0
to: 360
duration: 2000
duration: Style.animationSlow * 4
}
}
@@ -355,7 +165,6 @@ NPanel {
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
font.weight: Style.fontWeightMedium
anchors.verticalCenter: parent.verticalCenter
}
}
@@ -363,36 +172,15 @@ NPanel {
text: "Make sure your device is in pairing mode"
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
anchors.horizontalCenter: parent.horizontalCenter
Layout.alignment: Qt.AlignHCenter
}
}
NText {
text: "No devices found. Put your device in pairing mode and click Start Scanning."
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
visible: {
if (!BluetoothService.adapter || !Bluetooth.devices)
return true
var availableCount = Bluetooth.devices.values.filter(dev => {
return dev && !dev.paired && !dev.pairing
&& !dev.blocked
&& (dev.signalStrength === undefined
|| dev.signalStrength > 0)
}).length
return availableCount === 0 && !BluetoothService.adapter.discovering
}
wrapMode: Text.WordWrap
width: parent.width
horizontalAlignment: Text.AlignHCenter
Item {
Layout.fillHeight: true
}
}
}
// This item takes up all the remaining vertical space.
Item {
Layout.fillHeight: true
}
}
}
}

View File

@@ -29,7 +29,7 @@ NPanel {
NIconButton {
icon: "chevron_left"
tooltipText: "Previous Month"
tooltipText: "Previous month"
onClicked: {
let newDate = new Date(grid.year, grid.month - 1, 1)
grid.year = newDate.getFullYear()
@@ -48,7 +48,7 @@ NPanel {
NIconButton {
icon: "chevron_right"
tooltipText: "Next Month"
tooltipText: "Next month"
onClicked: {
let newDate = new Date(grid.year, grid.month + 1, 1)
grid.year = newDate.getFullYear()

View File

@@ -9,287 +9,294 @@ import qs.Commons
import qs.Services
import qs.Widgets
Loader {
active: (Settings.data.dock.monitors.length > 0)
sourceComponent: Component {
Variants {
model: Quickshell.screens
Variants {
model: Quickshell.screens
PanelWindow {
id: dockWindow
delegate: Loader {
required property ShellScreen modelData
readonly property real scaling: ScalingService.scale(screen)
screen: modelData
required property ShellScreen modelData
property real scaling: ScalingService.getScreenScale(modelData)
Connections {
target: ScalingService
function onScaleChanged(screenName, scale) {
if (screenName === modelData.name) {
scaling = scale
}
}
}
// Auto-hide properties - make reactive to settings changes
property bool autoHide: Settings.data.dock.autoHide || (Settings.data.bar.position === "bottom")
property bool hidden: autoHide
property int hideDelay: 500
property int showDelay: 100
property int hideAnimationDuration: Style.animationFast
property int showAnimationDuration: Style.animationFast
property int peekHeight: 2
property int fullHeight: dockContainer.height
property int iconSize: 36
active: Settings.isLoaded && modelData ? Settings.data.dock.monitors.includes(modelData.name) : false
// Track hover state
property bool dockHovered: false
property bool anyAppHovered: false
sourceComponent: PanelWindow {
id: dockWindow
// Dock is only shown if explicitely toggled
visible: modelData ? Settings.data.dock.monitors.includes(modelData.name) : false
screen: modelData
exclusionMode: ExclusionMode.Ignore
// Auto-hide properties - make reactive to settings changes
property bool autoHide: Settings.data.dock.autoHide || (Settings.data.bar.position === "bottom")
property bool hidden: autoHide
property int hideDelay: 500
property int showDelay: 100
property int hideAnimationDuration: Style.animationFast
property int showAnimationDuration: Style.animationFast
property int peekHeight: 2
property int fullHeight: dockContainer.height
property int iconSize: 36
anchors.bottom: true
anchors.left: true
anchors.right: true
focusable: false
color: Color.transparent
implicitHeight: iconSize * 1.4 * scaling
// Track hover state
property bool dockHovered: false
property bool anyAppHovered: false
// Watch for autoHide setting changes
onAutoHideChanged: {
if (!autoHide) {
// If auto-hide is disabled, show the dock
hidden = false
hideTimer.stop()
showTimer.stop()
} else {
// If auto-hide is enabled, start hidden
// Dock is only shown if explicitely toggled
exclusionMode: ExclusionMode.Ignore
anchors.bottom: true
anchors.left: true
anchors.right: true
focusable: false
color: Color.transparent
implicitHeight: iconSize * 1.4 * scaling
// Watch for autoHide setting changes
onAutoHideChanged: {
if (!autoHide) {
// If auto-hide is disabled, show the dock
hidden = false
hideTimer.stop()
showTimer.stop()
} else {
// If auto-hide is enabled, start hidden
hidden = true
}
}
// Timer for auto-hide delay
Timer {
id: hideTimer
interval: hideDelay
onTriggered: {
if (autoHide && !dockHovered && !anyAppHovered) {
hidden = true
}
}
}
// Timer for auto-hide delay
Timer {
id: hideTimer
interval: hideDelay
onTriggered: {
if (autoHide && !dockHovered && !anyAppHovered) {
hidden = true
}
// Timer for show delay
Timer {
id: showTimer
interval: showDelay
onTriggered: hidden = false
}
// Behavior for smooth hide/show animations
Behavior on margins.bottom {
NumberAnimation {
duration: hidden ? hideAnimationDuration : showAnimationDuration
easing.type: Easing.InOutQuad
}
}
MouseArea {
id: screenEdgeMouseArea
x: 0
y: modelData && modelData.geometry ? modelData.geometry.height - (fullHeight + 10 * scaling) : 0
width: screen.width
height: fullHeight + 10 * scaling
hoverEnabled: true
propagateComposedEvents: true
onEntered: {
if (autoHide && hidden) {
showTimer.start()
}
}
// Timer for show delay
Timer {
id: showTimer
interval: showDelay
onTriggered: hidden = false
}
// Behavior for smooth hide/show animations
Behavior on margins.bottom {
NumberAnimation {
duration: hidden ? hideAnimationDuration : showAnimationDuration
easing.type: Easing.InOutQuad
onExited: {
if (autoHide && !hidden && !dockHovered && !anyAppHovered) {
hideTimer.start()
}
}
}
margins.bottom: hidden ? -(fullHeight - peekHeight) : 0
Rectangle {
id: dockContainer
width: dock.width + 48 * scaling
height: iconSize * 1.4 * scaling
color: Color.mSurface
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
topLeftRadius: Style.radiusL * scaling
topRightRadius: Style.radiusL * scaling
MouseArea {
id: screenEdgeMouseArea
x: 0
y: modelData && modelData.geometry ? modelData.geometry.height - (fullHeight + 10 * scaling) : 0
width: screen.width
height: fullHeight + 10 * scaling
id: dockMouseArea
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
onEntered: {
if (autoHide && hidden) {
showTimer.start()
dockHovered = true
if (autoHide) {
showTimer.stop()
hideTimer.stop()
hidden = false
}
}
onExited: {
if (autoHide && !hidden && !dockHovered && !anyAppHovered) {
dockHovered = false
// Only start hide timer if we're not hovering over any app
if (autoHide && !anyAppHovered) {
hideTimer.start()
}
}
}
margins.bottom: hidden ? -(fullHeight - peekHeight) : 0
Item {
id: dock
width: runningAppsRow.width
height: parent.height - (20 * scaling)
anchors.centerIn: parent
Rectangle {
id: dockContainer
width: dock.width + 48 * scaling
height: iconSize * 1.4 * scaling
color: Color.mSurface
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
topLeftRadius: Style.radiusL * scaling
topRightRadius: Style.radiusL * scaling
MouseArea {
id: dockMouseArea
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
onEntered: {
dockHovered = true
if (autoHide) {
showTimer.stop()
hideTimer.stop()
hidden = false
}
}
onExited: {
dockHovered = false
// Only start hide timer if we're not hovering over any app
if (autoHide && !anyAppHovered) {
hideTimer.start()
}
}
NTooltip {
id: appTooltip
visible: false
positionAbove: true
}
Item {
id: dock
width: runningAppsRow.width
height: parent.height - (20 * scaling)
function getAppIcon(toplevel: Toplevel): string {
if (!toplevel)
return ""
return Icons.iconForAppId(toplevel.appId?.toLowerCase())
}
Row {
id: runningAppsRow
spacing: Style.marginL * scaling
height: parent.height
anchors.centerIn: parent
NTooltip {
id: appTooltip
visible: false
positionAbove: true
}
Repeater {
model: ToplevelManager ? ToplevelManager.toplevels : null
function getAppIcon(toplevel: Toplevel): string {
if (!toplevel)
return ""
return Icons.iconForAppId(toplevel.appId?.toLowerCase())
}
delegate: Rectangle {
id: appButton
width: iconSize * scaling
height: iconSize * scaling
color: Color.transparent
radius: Style.radiusM * scaling
Row {
id: runningAppsRow
spacing: Style.marginL * scaling
height: parent.height
anchors.centerIn: parent
property bool isActive: ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData
property bool hovered: appMouseArea.containsMouse
property string appId: modelData ? modelData.appId : ""
property string appTitle: modelData ? modelData.title : ""
Repeater {
model: ToplevelManager ? ToplevelManager.toplevels : null
// Hover background
Rectangle {
id: hoverBackground
anchors.fill: parent
color: appButton.hovered ? Color.mSurfaceVariant : Color.transparent
radius: parent.radius
opacity: appButton.hovered ? 0.8 : 0
delegate: Rectangle {
id: appButton
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutQuad
}
}
}
// The icon
Image {
id: appIcon
width: iconSize * scaling
height: iconSize * scaling
color: Color.transparent
radius: Style.radiusM * scaling
anchors.centerIn: parent
source: dock.getAppIcon(modelData)
visible: source.toString() !== ""
smooth: true
mipmap: false
antialiasing: false
fillMode: Image.PreserveAspectFit
property bool isActive: ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData
property bool hovered: appMouseArea.containsMouse
property string appId: modelData ? modelData.appId : ""
property string appTitle: modelData ? modelData.title : ""
scale: appButton.hovered ? 1.1 : 1.0
// Hover background
Rectangle {
id: hoverBackground
anchors.fill: parent
color: appButton.hovered ? Color.mSurfaceVariant : Color.transparent
radius: parent.radius
opacity: appButton.hovered ? 0.8 : 0
Behavior on scale {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutBack
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutQuad
}
// Fall back if no icon
NText {
anchors.centerIn: parent
visible: !appIcon.visible
text: "question_mark"
font.family: "Material Symbols Rounded"
font.pointSize: iconSize * 0.7 * scaling
color: appButton.isActive ? Color.mPrimary : Color.mOnSurfaceVariant
scale: appButton.hovered ? 1.1 : 1.0
Behavior on scale {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutBack
}
}
}
MouseArea {
id: appMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
onEntered: {
anyAppHovered = true
const appName = appButton.appTitle || appButton.appId || "Unknown"
appTooltip.text = appName.length > 40 ? appName.substring(0, 37) + "..." : appName
appTooltip.target = appButton
appTooltip.isVisible = true
if (autoHide) {
showTimer.stop()
hideTimer.stop()
hidden = false
}
}
// The icon
Image {
id: appIcon
width: iconSize * scaling
height: iconSize * scaling
anchors.centerIn: parent
source: dock.getAppIcon(modelData)
visible: source.toString() !== ""
smooth: true
mipmap: false
antialiasing: false
fillMode: Image.PreserveAspectFit
scale: appButton.hovered ? 1.1 : 1.0
Behavior on scale {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutBack
}
onExited: {
anyAppHovered = false
appTooltip.hide()
// Only start hide timer if we're not hovering over the dock
if (autoHide && !dockHovered) {
hideTimer.start()
}
}
// Fall back if no icon
NText {
anchors.centerIn: parent
visible: !appIcon.visible
text: "question_mark"
font.family: "Material Symbols Rounded"
font.pointSize: iconSize * 0.7 * scaling
color: appButton.isActive ? Color.mPrimary : Color.mOnSurfaceVariant
scale: appButton.hovered ? 1.1 : 1.0
Behavior on scale {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutBack
}
onClicked: function (mouse) {
if (mouse.button === Qt.MiddleButton && modelData?.close) {
modelData.close()
}
if (mouse.button === Qt.LeftButton && modelData?.activate) {
modelData.activate()
}
}
}
MouseArea {
id: appMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
onEntered: {
anyAppHovered = true
const appName = appButton.appTitle || appButton.appId || "Unknown"
appTooltip.text = appName.length > 40 ? appName.substring(0, 37) + "..." : appName
appTooltip.target = appButton
appTooltip.isVisible = true
if (autoHide) {
showTimer.stop()
hideTimer.stop()
hidden = false
}
}
onExited: {
anyAppHovered = false
appTooltip.hide()
// Only start hide timer if we're not hovering over the dock
if (autoHide && !dockHovered) {
hideTimer.start()
}
}
onClicked: function (mouse) {
if (mouse.button === Qt.MiddleButton && modelData?.close) {
modelData.close()
}
if (mouse.button === Qt.LeftButton && modelData?.activate) {
modelData.activate()
}
}
}
Rectangle {
visible: isActive
width: iconSize * 0.75
height: 4 * scaling
color: Color.mPrimary
radius: Style.radiusXS
anchors.top: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: Style.marginXXS * scaling
}
Rectangle {
visible: isActive
width: iconSize * 0.75
height: 4 * scaling
color: Color.mPrimary
radius: Style.radiusXS
anchors.top: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: Style.marginXXS * scaling
}
}
}

View File

@@ -6,6 +6,13 @@ import qs.Services
Item {
id: root
IpcHandler {
target: "screenRecorder"
function toggle() {
ScreenRecorderService.toggleRecording()
}
}
IpcHandler {
target: "settings"
function toggle() {
@@ -92,6 +99,24 @@ Item {
}
}
IpcHandler {
target: "volume"
function increase() {
AudioService.increaseVolume()
}
function decrease() {
AudioService.decreaseVolume()
}
function muteOutput() {
AudioService.setMuted(!AudioService.muted)
}
function muteInput() {
if (AudioService.source?.ready && AudioService.source?.audio) {
AudioService.source.audio.muted = !AudioService.source.audio.muted
}
}
}
IpcHandler {
target: "powerPanel"
function toggle() {

View File

@@ -18,11 +18,19 @@ NPanel {
panelHeight: Math.min(550 * scaling, screen?.height * 0.8)
// Positioning derives from Settings.data.bar.position for vertical (top/bottom)
// and from Settings.data.appLauncher.position for horizontal vs center.
// Options: center, top_left, top_right, bottom_left, bottom_right
// Options: center, top_left, top_right, bottom_left, bottom_right, bottom_center, top_center
readonly property string launcherPosition: Settings.data.appLauncher.position
panelAnchorCentered: launcherPosition === "center"
panelAnchorHorizontalCenter: launcherPosition === "center" || (launcherPosition.endsWith("_center"))
panelAnchorVerticalCenter: launcherPosition === "center"
panelAnchorLeft: launcherPosition !== "center" && (launcherPosition.endsWith("_left"))
panelAnchorRight: launcherPosition !== "center" && (launcherPosition.endsWith("_right"))
panelAnchorBottom: launcherPosition.startsWith("bottom_")
panelAnchorTop: launcherPosition.startsWith("top_")
// Background opacity following bar's approach
panelBackgroundColor: Qt.rgba(Color.mSurface.r, Color.mSurface.g, Color.mSurface.b,
Settings.data.appLauncher.backgroundOpacity)
// Properties
property string searchText: ""
@@ -236,7 +244,7 @@ NPanel {
// Search bar
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: Style.barHeight * scaling
Layout.preferredHeight: Math.round(Style.barHeight * scaling)
Layout.bottomMargin: Style.marginM * scaling
radius: Style.radiusM * scaling
color: Color.mSurface

View File

@@ -93,7 +93,7 @@ Loader {
id: lockBgImage
anchors.fill: parent
fillMode: Image.PreserveAspectCrop
source: WallpaperService.currentWallpaper !== "" ? WallpaperService.currentWallpaper : ""
source: screen ? WallpaperService.getWallpaper(screen.name) : ""
cache: true
smooth: true
mipmap: false

View File

@@ -12,12 +12,11 @@ import qs.Widgets
Variants {
model: Quickshell.screens
PanelWindow {
delegate: Loader {
id: root
required property ShellScreen modelData
readonly property real scaling: ScalingService.scale(screen)
screen: modelData
readonly property real scaling: ScalingService.getScreenScale(modelData)
// Access the notification model from the service
property ListModel notificationModel: NotificationService.notificationModel
@@ -25,228 +24,203 @@ Variants {
// Track notifications being removed for animation
property var removingNotifications: ({})
color: Color.transparent
// If no notification display activated in settings, then show them all
visible: modelData ? (Settings.data.notifications.monitors.includes(modelData.name)
|| (Settings.data.notifications.monitors.length === 0))
&& (NotificationService.notificationModel.count > 0) : false
active: Settings.isLoaded && modelData
&& (NotificationService.notificationModel.count > 0) ? (Settings.data.notifications.monitors.includes(
modelData.name)
|| (Settings.data.notifications.monitors.length === 0)) : false
// Position based on bar location
anchors.top: Settings.data.bar.position === "top"
anchors.bottom: Settings.data.bar.position === "bottom"
anchors.right: true
margins.top: Settings.data.bar.position === "top" ? (Style.barHeight + Style.marginM) * scaling : 0
margins.bottom: Settings.data.bar.position === "bottom" ? (Style.barHeight + Style.marginM) * scaling : 0
margins.right: Style.marginM * scaling
implicitWidth: 360 * scaling
implicitHeight: Math.min(notificationStack.implicitHeight, (NotificationService.maxVisible * 120) * scaling)
//WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.exclusionMode: ExclusionMode.Ignore
visible: (NotificationService.notificationModel.count > 0)
// Connect to animation signal from service
Component.onCompleted: {
NotificationService.animateAndRemove.connect(function (notification, index) {
// Prefer lookup by identity to avoid index mismatches
var delegate = null
if (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) {
delegate = child
break
}
}
}
sourceComponent: PanelWindow {
screen: modelData
color: Color.transparent
// Fallback to index if identity lookup failed
if (!delegate && 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)
}
})
}
// Main notification container
Column {
id: notificationStack
// Position based on bar location
anchors.top: Settings.data.bar.position === "top" ? parent.top : undefined
anchors.bottom: Settings.data.bar.position === "bottom" ? parent.bottom : undefined
anchors.right: parent.right
spacing: Style.marginS * scaling
width: 360 * scaling
visible: true
anchors.top: Settings.data.bar.position === "top"
anchors.bottom: Settings.data.bar.position === "bottom"
anchors.right: true
margins.top: Settings.data.bar.position === "top" ? (Style.barHeight + Style.marginM) * scaling : 0
margins.bottom: Settings.data.bar.position === "bottom" ? (Style.barHeight + Style.marginM) * scaling : 0
margins.right: Style.marginM * scaling
implicitWidth: 360 * scaling
implicitHeight: Math.min(notificationStack.implicitHeight, (NotificationService.maxVisible * 120) * scaling)
//WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.exclusionMode: ExclusionMode.Ignore
// Multiple notifications display
Repeater {
model: notificationModel
delegate: Rectangle {
width: 360 * scaling
height: Math.max(80 * scaling, contentColumn.implicitHeight + (Style.marginM * 2 * scaling))
clip: true
radius: Style.radiusM * scaling
border.color: Color.mPrimary
border.width: Math.max(1, Style.borderS * scaling)
color: Color.mSurface
// Animation properties
property real scaleValue: 0.8
property real opacityValue: 0.0
property bool isRemoving: false
// Scale and fade-in animation
scale: scaleValue
opacity: opacityValue
// Animate in when the item is created
Component.onCompleted: {
scaleValue = 1.0
opacityValue = 1.0
}
// Animate out when being removed
function animateOut() {
isRemoving = true
scaleValue = 0.8
opacityValue = 0.0
}
// Timer for delayed removal after animation
Timer {
id: removalTimer
interval: Style.animationSlow
repeat: false
onTriggered: {
NotificationService.forceRemoveNotification(model.rawNotification)
// Connect to animation signal from service
Component.onCompleted: {
NotificationService.animateAndRemove.connect(function (notification, index) {
// Prefer lookup by identity to avoid index mismatches
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) {
delegate = child
break
}
}
}
// Check if this notification is being removed
onIsRemovingChanged: {
if (isRemoving) {
// Remove from model after animation completes
removalTimer.start()
}
// Fallback to index if identity lookup failed
if (!delegate && notificationStack && notificationStack.children && notificationStack.children[index]) {
delegate = notificationStack.children[index]
}
// Animation behaviors
Behavior on scale {
NumberAnimation {
duration: Style.animationSlow
easing.type: Easing.OutExpo
//easing.type: Easing.OutBack looks better but notification get clipped on all sides
}
if (delegate && delegate.animateOut) {
delegate.animateOut()
} else {
// As a last resort, force-remove without animation to avoid stuck popups
NotificationService.forceRemoveNotification(notification)
}
})
}
Behavior on opacity {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutQuad
// Main notification container
Column {
id: notificationStack
// Position based on bar location
anchors.top: Settings.data.bar.position === "top" ? parent.top : undefined
anchors.bottom: Settings.data.bar.position === "bottom" ? parent.bottom : undefined
anchors.right: parent.right
spacing: Style.marginS * scaling
width: 360 * scaling
visible: true
// Multiple notifications display
Repeater {
model: notificationModel
delegate: Rectangle {
width: 360 * scaling
height: Math.max(80 * scaling, contentColumn.implicitHeight + (Style.marginM * 2 * scaling))
clip: true
radius: Style.radiusM * scaling
border.color: Color.mPrimary
border.width: Math.max(1, Style.borderS * scaling)
color: Color.mSurface
// Animation properties
property real scaleValue: 0.8
property real opacityValue: 0.0
property bool isRemoving: false
// Scale and fade-in animation
scale: scaleValue
opacity: opacityValue
// Animate in when the item is created
Component.onCompleted: {
scaleValue = 1.0
opacityValue = 1.0
}
}
Column {
id: contentColumn
anchors.fill: parent
anchors.margins: Style.marginM * scaling
spacing: Style.marginS * scaling
// Animate out when being removed
function animateOut() {
isRemoving = true
scaleValue = 0.8
opacityValue = 0.0
}
RowLayout {
// Timer for delayed removal after animation
Timer {
id: removalTimer
interval: Style.animationSlow
repeat: false
onTriggered: {
NotificationService.forceRemoveNotification(model.rawNotification)
}
}
// Check if this notification is being removed
onIsRemovingChanged: {
if (isRemoving) {
// Remove from model after animation completes
removalTimer.start()
}
}
// Animation behaviors
Behavior on scale {
NumberAnimation {
duration: Style.animationSlow
easing.type: Easing.OutExpo
//easing.type: Easing.OutBack looks better but notification get clipped on all sides
}
}
Behavior on opacity {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutQuad
}
}
Column {
id: contentColumn
anchors.fill: parent
anchors.margins: Style.marginM * scaling
spacing: Style.marginS * scaling
NText {
text: (model.appName || model.desktopEntry) || "Unknown App"
color: Color.mSecondary
font.pointSize: Style.fontSizeXS * scaling
}
Rectangle {
width: 6 * scaling
height: 6 * scaling
radius: Style.radiusXS * scaling
color: (model.urgency === NotificationUrgency.Critical) ? Color.mError : (model.urgency === NotificationUrgency.Low) ? Color.mOnSurface : Color.mPrimary
Layout.alignment: Qt.AlignVCenter
}
Item {
Layout.fillWidth: true
}
NText {
text: NotificationService.formatTimestamp(model.timestamp)
color: Color.mOnSurface
font.pointSize: Style.fontSizeXS * scaling
}
}
NText {
text: model.summary || "No summary"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
wrapMode: Text.Wrap
width: 300 * scaling
maximumLineCount: 3
elide: Text.ElideRight
}
NText {
text: model.body || ""
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurface
wrapMode: Text.Wrap
width: 300 * scaling
maximumLineCount: 5
elide: Text.ElideRight
}
// Notification actions
RowLayout {
visible: model.actions && model.actions.length > 0
spacing: Style.marginXS * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginS * scaling
Repeater {
model: model.actions || []
delegate: NPill {
text: modelData.text || modelData.identifier || "Action"
icon: modelData.identifier || ""
tooltipText: modelData.text || modelData.identifier || "Action"
sizeMultiplier: 0.7
RowLayout {
spacing: Style.marginS * scaling
NText {
text: (model.appName || model.desktopEntry) || "Unknown App"
color: Color.mSecondary
font.pointSize: Style.fontSizeXS * scaling
}
Rectangle {
width: 6 * scaling
height: 6 * scaling
radius: Style.radiusXS * scaling
color: (model.urgency === NotificationUrgency.Critical) ? Color.mError : (model.urgency === NotificationUrgency.Low) ? Color.mOnSurface : Color.mPrimary
Layout.alignment: Qt.AlignVCenter
}
Item {
Layout.fillWidth: true
forceOpen: true
// Style action buttons differently
pillColor: Color.mPrimary
textColor: Color.mOnPrimary
iconCircleColor: Color.mPrimary
iconTextColor: Color.mOnPrimary
onClicked: {
// Invoke the notification action
modelData.invoke()
// Animate out the notification after action
animateOut()
}
}
NText {
text: NotificationService.formatTimestamp(model.timestamp)
color: Color.mOnSurface
font.pointSize: Style.fontSizeXS * scaling
}
}
NText {
text: model.summary || "No summary"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
wrapMode: Text.Wrap
width: 300 * scaling
maximumLineCount: 3
elide: Text.ElideRight
}
NText {
text: model.body || ""
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurface
wrapMode: Text.Wrap
width: 300 * scaling
maximumLineCount: 5
elide: Text.ElideRight
}
// Actions removed
}
}
NIconButton {
icon: "close"
tooltipText: "Close"
sizeMultiplier: 0.8
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: Style.marginS * scaling
NIconButton {
icon: "close"
tooltipText: "Close"
sizeRatio: 0.8
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: Style.marginS * scaling
onClicked: {
animateOut()
onClicked: {
animateOut()
}
}
}
}

View File

@@ -45,15 +45,15 @@ NPanel {
NIconButton {
icon: "delete"
tooltipText: "Clear History"
sizeMultiplier: 0.8
tooltipText: "Clear history"
sizeRatio: 0.8
onClicked: NotificationService.clearHistory()
}
NIconButton {
icon: "close"
tooltipText: "Close"
sizeMultiplier: 0.8
sizeRatio: 0.8
onClicked: {
root.close()
}
@@ -77,19 +77,19 @@ NPanel {
NIcon {
text: "notifications_off"
font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mOnSurfaceVariant
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "No notifications"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurfaceVariant
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Notifications will appear here when you receive them"
text: "Your notifications will show up here as they arrive."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
@@ -158,8 +158,8 @@ NPanel {
// Trash icon button
NIconButton {
icon: "delete"
tooltipText: "Delete Notification"
sizeMultiplier: 0.7
tooltipText: "Delete notification"
sizeRatio: 0.7
onClicked: {
Logger.log("NotificationHistory", "Removing notification:", summary)

View File

@@ -14,7 +14,8 @@ NPanel {
panelWidth: 440 * scaling
panelHeight: 380 * scaling
panelAnchorCentered: true
panelAnchorHorizontalCenter: true
panelAnchorVerticalCenter: true
// Timer properties
property int timerDuration: 9000 // 9 seconds
@@ -308,7 +309,8 @@ NPanel {
return Color.mOnSurfaceVariant
}
opacity: Style.opacityHeavy
wrapMode: Text.WordWrap
wrapMode: Text.NoWrap
elide: Text.ElideRight
}
}

View File

@@ -11,9 +11,18 @@ import qs.Widgets
NPanel {
id: root
panelWidth: Math.max(screen?.width * 0.5, 1280) * scaling
panelHeight: Math.max(screen?.height * 0.5, 720) * scaling
panelAnchorCentered: true
panelWidth: {
var w = Math.round(Math.max(screen?.width * 0.4, 1000) * scaling)
w = Math.min(w, screen?.width - Style.marginL * 2)
return w
}
panelHeight: {
var h = Math.round(Math.max(screen?.height * 0.75, 800) * scaling)
h = Math.min(h, screen?.height - Style.barHeight * scaling - Style.marginL * 2)
return h
}
panelAnchorHorizontalCenter: true
panelAnchorVerticalCenter: true
// Tabs enumeration, order is NOT relevant
enum Tab {
@@ -183,7 +192,7 @@ NPanel {
Rectangle {
id: sidebar
Layout.preferredWidth: Style.sliderWidth * 1.3 * scaling
Layout.preferredWidth: 220 * scaling
Layout.fillHeight: true
color: Color.mSurfaceVariant
border.color: Color.mOutline
@@ -207,6 +216,19 @@ NPanel {
readonly property bool selected: index === currentTabIndex
property bool hovering: false
property color tabTextColor: selected ? Color.mOnPrimary : (tabItem.hovering ? Color.mOnTertiary : Color.mOnSurface)
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
Behavior on tabTextColor {
ColorAnimation {
duration: Style.animationFast
}
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: Style.marginS * scaling
@@ -266,7 +288,7 @@ NPanel {
// Tab label on the main right side
NText {
text: root.tabsModel[currentTabIndex].label
font.pointSize: Style.fontSizeL * scaling
font.pointSize: Style.fontSizeXL * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
Layout.fillWidth: true
@@ -286,21 +308,29 @@ NPanel {
Item {
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
Repeater {
model: root.tabsModel
onItemAdded: function (index, item) {
item.sourceComponent = root.tabsModel[index].source
}
delegate: Loader {
// All loaders will occupy the same space, stacked on top of each other.
anchors.fill: parent
visible: index === root.currentTabIndex
// The loader is only active (and uses memory) when its page is visible.
active: visible
active: index === root.currentTabIndex
sourceComponent: ColumnLayout {
ScrollView {
id: scrollView
Layout.fillWidth: true
Layout.fillHeight: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
padding: Style.marginL * scaling
clip: true
Loader {
active: true
sourceComponent: root.tabsModel[index].source
width: scrollView.availableWidth
}
}
}
}
}
}

View File

@@ -15,10 +15,6 @@ ColumnLayout {
property string currentVersion: "Unknown" // Fallback version
property var contributors: GitHubService.contributors
spacing: 0
Layout.fillWidth: true
Layout.fillHeight: true
Process {
id: currentVersionProcess
@@ -41,222 +37,210 @@ ColumnLayout {
}
}
ScrollView {
id: scrollView
NText {
text: "Noctalia Shell"
font.pointSize: Style.fontSizeXXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.alignment: Qt.AlignCenter
Layout.bottomMargin: Style.marginS * scaling
}
Layout.fillWidth: true
Layout.fillHeight: true
padding: Style.marginL * scaling
rightPadding: Style.marginM * scaling
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
// Versions
GridLayout {
Layout.alignment: Qt.AlignCenter
columns: 2
rowSpacing: Style.marginXS * scaling
columnSpacing: Style.marginS * scaling
ColumnLayout {
width: scrollView.availableWidth
spacing: 0
NText {
text: "Latest Version:"
color: Color.mOnSurface
Layout.alignment: Qt.AlignRight
}
NText {
text: "Noctalia: quiet by design"
font.pointSize: Style.fontSizeXXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.alignment: Qt.AlignCenter
Layout.bottomMargin: Style.marginS * scaling
}
NText {
text: root.latestVersion
color: Color.mOnSurface
font.weight: Style.fontWeightBold
}
NText {
text: "It may just be another quickshell setup but it won't get in your way."
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignCenter
Layout.bottomMargin: Style.marginL * scaling
}
NText {
text: "Installed Version:"
color: Color.mOnSurface
Layout.alignment: Qt.AlignRight
}
GridLayout {
Layout.alignment: Qt.AlignCenter
columns: 2
rowSpacing: Style.marginXS * scaling
columnSpacing: Style.marginS * scaling
NText {
text: root.currentVersion
color: Color.mOnSurface
font.weight: Style.fontWeightBold
}
}
NText {
text: "Latest Version:"
color: Color.mOnSurface
Layout.alignment: Qt.AlignRight
}
// Updater
Rectangle {
Layout.alignment: Qt.AlignCenter
Layout.topMargin: Style.marginS * scaling
Layout.preferredWidth: updateText.implicitWidth + 46 * scaling
Layout.preferredHeight: Math.round(Style.barHeight * scaling)
radius: Style.radiusL * scaling
color: updateArea.containsMouse ? Color.mPrimary : Color.transparent
border.color: Color.mPrimary
border.width: Math.max(1, Style.borderS * scaling)
visible: {
if (root.currentVersion === "Unknown" || root.latestVersion === "Unknown")
return false
NText {
text: root.latestVersion
color: Color.mOnSurface
font.weight: Style.fontWeightBold
}
const latest = root.latestVersion.replace("v", "").split(".")
const current = root.currentVersion.replace("v", "").split(".")
for (var i = 0; i < Math.max(latest.length, current.length); i++) {
const l = parseInt(latest[i] || "0")
const c = parseInt(current[i] || "0")
if (l > c)
return true
NText {
text: "Installed Version:"
color: Color.mOnSurface
Layout.alignment: Qt.AlignRight
}
NText {
text: root.currentVersion
color: Color.mOnSurface
font.weight: Style.fontWeightBold
}
}
Rectangle {
Layout.alignment: Qt.AlignCenter
Layout.topMargin: Style.marginS * scaling
Layout.preferredWidth: updateText.implicitWidth + 46 * scaling
Layout.preferredHeight: Style.barHeight * scaling
radius: Style.radiusL * scaling
color: updateArea.containsMouse ? Color.mPrimary : Color.transparent
border.color: Color.mPrimary
border.width: Math.max(1, Style.borderS * scaling)
visible: {
if (root.currentVersion === "Unknown" || root.latestVersion === "Unknown")
return false
const latest = root.latestVersion.replace("v", "").split(".")
const current = root.currentVersion.replace("v", "").split(".")
for (var i = 0; i < Math.max(latest.length, current.length); i++) {
const l = parseInt(latest[i] || "0")
const c = parseInt(current[i] || "0")
if (l > c)
return true
if (l < c)
return false
}
if (l < c)
return false
}
return false
}
RowLayout {
anchors.centerIn: parent
spacing: Style.marginS * scaling
NIcon {
text: "system_update"
font.pointSize: Style.fontSizeXXL * scaling
color: updateArea.containsMouse ? Color.mSurface : Color.mPrimary
}
NText {
id: updateText
text: "Download latest release"
font.pointSize: Style.fontSizeL * scaling
color: updateArea.containsMouse ? Color.mSurface : Color.mPrimary
}
}
MouseArea {
id: updateArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
Quickshell.execDetached(["xdg-open", "https://github.com/Ly-sec/Noctalia/releases/latest"])
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
NText {
text: `Shout-out to our ${root.contributors.length} <b>awesome</b> contributors!`
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignCenter
}
GridView {
id: contributorsGrid
Layout.topMargin: Style.marginL * scaling
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: cellWidth * 3 // Fixed 3 columns
Layout.preferredHeight: {
if (root.contributors.length === 0)
return 0
const columns = 3
const rows = Math.ceil(root.contributors.length / columns)
return rows * cellHeight
}
cellWidth: Style.baseWidgetSize * 7 * scaling
cellHeight: Style.baseWidgetSize * 3 * scaling
model: root.contributors
clip: true
delegate: Rectangle {
width: contributorsGrid.cellWidth - Style.marginM * scaling
height: contributorsGrid.cellHeight - Style.marginM * scaling
radius: Style.radiusL * scaling
color: contributorArea.containsMouse ? Color.mTertiary : Color.transparent
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
RowLayout {
anchors.fill: parent
anchors.margins: Style.marginS * scaling
spacing: Style.marginM * scaling
Item {
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: Style.baseWidgetSize * 2 * scaling
Layout.preferredHeight: Style.baseWidgetSize * 2 * scaling
NImageCircled {
imagePath: modelData.avatar_url || ""
anchors.fill: parent
anchors.margins: Style.marginXS * scaling
fallbackIcon: "person"
borderColor: contributorArea.containsMouse ? Color.mOnTertiary : Color.mPrimary
borderWidth: Math.max(1, Style.borderM * scaling)
Behavior on borderColor {
ColorAnimation {
duration: Style.animationFast
}
}
}
}
RowLayout {
anchors.centerIn: parent
spacing: Style.marginS * scaling
ColumnLayout {
spacing: Style.marginXS * scaling
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
NIcon {
text: "system_update"
font.pointSize: Style.fontSizeXXL * scaling
color: updateArea.containsMouse ? Color.mSurface : Color.mPrimary
NText {
text: modelData.login || "Unknown"
font.weight: Style.fontWeightBold
color: contributorArea.containsMouse ? Color.mSurface : Color.mOnSurface
elide: Text.ElideRight
Layout.fillWidth: true
}
NText {
id: updateText
text: "Download latest release"
font.pointSize: Style.fontSizeL * scaling
color: updateArea.containsMouse ? Color.mSurface : Color.mPrimary
}
}
MouseArea {
id: updateArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
Quickshell.execDetached(["xdg-open", "https://github.com/Ly-sec/Noctalia/releases/latest"])
text: (modelData.contributions || 0) + " " + ((modelData.contributions || 0) === 1 ? "commit" : "commits")
font.pointSize: Style.fontSizeXS * scaling
color: contributorArea.containsMouse ? Color.mSurface : Color.mOnSurface
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL * 2 * scaling
Layout.bottomMargin: Style.marginL * scaling
}
MouseArea {
id: contributorArea
NText {
text: `Shout-out to our ${root.contributors.length} awesome contributors!`
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.alignment: Qt.AlignCenter
Layout.topMargin: Style.marginL * 2
}
ScrollView {
Layout.alignment: Qt.AlignCenter
Layout.preferredWidth: 200 * Style.marginXS * scaling
Layout.fillHeight: true
Layout.topMargin: Style.marginL * scaling
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
GridView {
id: contributorsGrid
anchors.fill: parent
width: 200 * 4 * scaling
height: Math.ceil(root.contributors.length / 4) * 100
cellWidth: Style.baseWidgetSize * 6.25 * scaling
cellHeight: Style.baseWidgetSize * 3.125 * scaling
model: root.contributors
delegate: Rectangle {
width: contributorsGrid.cellWidth - Style.marginL * scaling
height: contributorsGrid.cellHeight - Style.marginXS * scaling
radius: Style.radiusL * scaling
color: contributorArea.containsMouse ? Color.mSecondary : Color.transparent
RowLayout {
anchors.fill: parent
anchors.margins: Style.marginS * scaling
spacing: Style.marginM * scaling
Item {
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: Style.baseWidgetSize * 2 * scaling
Layout.preferredHeight: Style.baseWidgetSize * 2 * scaling
NImageCircled {
imagePath: modelData.avatar_url || ""
anchors.fill: parent
anchors.margins: Style.marginXS * scaling
fallbackIcon: "person"
borderColor: Color.mPrimary
borderWidth: Math.max(1, Style.borderM * scaling)
}
}
ColumnLayout {
spacing: Style.marginXS * scaling
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
NText {
text: modelData.login || "Unknown"
font.weight: Style.fontWeightBold
color: contributorArea.containsMouse ? Color.mSurface : Color.mOnSurface
elide: Text.ElideRight
Layout.fillWidth: true
}
NText {
text: (modelData.contributions || 0) + " " + ((modelData.contributions
|| 0) === 1 ? "commit" : "commits")
font.pointSize: Style.fontSizeXS * scaling
color: contributorArea.containsMouse ? Color.mSurface : Color.mOnSurface
}
}
}
MouseArea {
id: contributorArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData.html_url)
Quickshell.execDetached(["xdg-open", modelData.html_url])
}
}
}
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData.html_url)
Quickshell.execDetached(["xdg-open", modelData.html_url])
}
}
}
}
Item {
Layout.fillHeight: true
}
}

View File

@@ -11,7 +11,6 @@ ColumnLayout {
property real localVolume: AudioService.volume
// Connection used to open the pill when volume changes
Connections {
target: AudioService.sink?.audio ? AudioService.sink?.audio : null
function onVolumeChanged() {
@@ -19,335 +18,448 @@ ColumnLayout {
}
}
spacing: 0
ScrollView {
id: scrollView
// Master Volume
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
Layout.fillHeight: true
padding: Style.marginM * scaling
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
NLabel {
label: "Output Volume"
description: "System-wide volume level."
}
RowLayout {
// Pipewire seems a bit finicky, if we spam too many volume changes it breaks easily
// Probably because they have some quick fades in and out to avoid clipping
// We use a timer to space out the updates, to avoid lock up
Timer {
interval: Style.animationFast
running: true
repeat: true
onTriggered: {
if (Math.abs(localVolume - AudioService.volume) >= 0.01) {
AudioService.setVolume(localVolume)
}
}
}
NSlider {
Layout.fillWidth: true
from: 0
to: Settings.data.audio.volumeOverdrive ? 2.0 : 1.0
value: localVolume
stepSize: 0.01
onMoved: {
localVolume = value
}
}
NText {
text: Math.floor(AudioService.volume * 100) + "%"
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: Style.marginS * scaling
color: Color.mOnSurface
}
}
}
// Mute Toggle
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginM * scaling
NToggle {
label: "Mute Audio Output"
description: "Mute or unmute the default audio output."
checked: AudioService.muted
onToggled: checked => {
if (AudioService.sink && AudioService.sink.audio) {
AudioService.sink.audio.muted = checked
}
}
}
}
// Input Volume
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginM * scaling
NLabel {
label: "Input Volume"
description: "Microphone input volume level."
}
RowLayout {
NSlider {
Layout.fillWidth: true
from: 0
to: 1.0
value: AudioService.inputVolume
stepSize: 0.01
onMoved: {
AudioService.setInputVolume(value)
}
}
NText {
text: Math.floor(AudioService.inputVolume * 100) + "%"
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: Style.marginS * scaling
color: Color.mOnSurface
}
}
}
// Input Mute Toggle
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginM * scaling
NToggle {
label: "Mute Audio Input"
description: "Mute or unmute the default audio input (microphone)."
checked: AudioService.inputMuted
onToggled: checked => {
AudioService.setInputMuted(checked)
}
}
}
// Volume Step Size
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginM * scaling
NSpinBox {
Layout.fillWidth: true
label: "Volume Step Size"
description: "Adjust the step size for volume changes (scroll wheel, keyboard shortcuts)."
minimum: 1
maximum: 25
value: Settings.data.audio.volumeStep
stepSize: 1
suffix: "%"
onValueChanged: {
Settings.data.audio.volumeStep = value
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// AudioService Devices
ColumnLayout {
spacing: Style.marginS * scaling
NText {
text: "Audio Devices"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
Layout.bottomMargin: Style.marginS * scaling
}
// -------------------------------
// Output Devices
ButtonGroup {
id: sinks
}
ColumnLayout {
width: scrollView.availableWidth
spacing: 0
spacing: Style.marginXS * scaling
Layout.fillWidth: true
Layout.bottomMargin: Style.marginL * scaling
Item {
Layout.fillWidth: true
Layout.preferredHeight: 0
NLabel {
label: "Output Device"
description: "Select the desired audio output device."
}
ColumnLayout {
spacing: Style.marginXS * scaling
Layout.fillWidth: true
NText {
text: "Audio Output Volume"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.bottomMargin: Style.marginS * scaling
}
// Volume Controls
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginS * scaling
// Master Volume
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
NLabel {
label: "Master Volume"
description: "System-wide volume level."
}
RowLayout {
// Pipewire seems a bit finicky, if we spam too many volume changes it breaks easily
// Probably because they have some quick fades in and out to avoid clipping
// We use a timer to space out the updates, to avoid lock up
Timer {
interval: Style.animationFast
running: true
repeat: true
onTriggered: {
if (Math.abs(localVolume - AudioService.volume) >= 0.01) {
AudioService.setVolume(localVolume)
}
}
}
NSlider {
Layout.fillWidth: true
from: 0
to: Settings.data.audio.volumeOverdrive ? 2.0 : 1.0
value: localVolume
stepSize: 0.01
onMoved: {
localVolume = value
}
}
NText {
text: Math.floor(AudioService.volume * 100) + "%"
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: Style.marginS * scaling
color: Color.mOnSurface
}
}
}
// Mute Toggle
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginM * scaling
NToggle {
label: "Mute Audio Output"
description: "Mute or unmute the default audio output."
checked: AudioService.muted
onToggled: checked => {
if (AudioService.sink && AudioService.sink.audio) {
AudioService.sink.audio.muted = checked
}
}
}
}
// Volume Step Size
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginM * scaling
NSpinBox {
Layout.fillWidth: true
label: "Volume Step Size"
description: "Adjust the step size for volume changes (scroll wheel, keyboard shortcuts)."
minimum: 1
maximum: 25
value: Settings.data.audio.volumeStep
stepSize: 1
suffix: "%"
onValueChanged: {
Settings.data.audio.volumeStep = value
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL * 2 * scaling
Layout.bottomMargin: Style.marginL * scaling
}
// AudioService Devices
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NText {
text: "Audio Devices"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.bottomMargin: Style.marginS * scaling
}
// -------------------------------
// Output Devices
ButtonGroup {
id: sinks
}
ColumnLayout {
spacing: Style.marginXS * scaling
Layout.fillWidth: true
Layout.bottomMargin: Style.marginL * scaling
NLabel {
label: "Output Device"
description: "Select the desired audio output device."
}
Repeater {
model: AudioService.sinks
NRadioButton {
required property PwNode modelData
ButtonGroup.group: sinks
checked: AudioService.sink?.id === modelData.id
onClicked: AudioService.setAudioSink(modelData)
text: modelData.description
}
}
}
}
// -------------------------------
// Input Devices
ButtonGroup {
id: sources
}
ColumnLayout {
spacing: Style.marginXS * scaling
Layout.fillWidth: true
Layout.bottomMargin: Style.marginL * scaling
NLabel {
label: "Input Device"
description: "Select the desired audio input device."
}
Repeater {
model: AudioService.sources
NRadioButton {
required property PwNode modelData
ButtonGroup.group: sources
checked: AudioService.source?.id === modelData.id
onClicked: AudioService.setAudioSource(modelData)
text: modelData.description
}
}
Repeater {
model: AudioService.sinks
NRadioButton {
required property PwNode modelData
ButtonGroup.group: sinks
checked: AudioService.sink?.id === modelData.id
onClicked: AudioService.setAudioSink(modelData)
text: modelData.description
}
}
}
// Divider
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL * scaling
Layout.bottomMargin: Style.marginXL * scaling
// -------------------------------
// Input Devices
ButtonGroup {
id: sources
}
ColumnLayout {
spacing: Style.marginXS * scaling
Layout.fillWidth: true
Layout.bottomMargin: Style.marginL * scaling
NLabel {
label: "Input Device"
description: "Select the desired audio input device."
}
// Bar Mini Media player
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NText {
text: "Bar Media Player"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.bottomMargin: Style.marginS * scaling
}
// Miniplayer section
NToggle {
label: "Show Album Art In Bar Media Player"
description: "Show the album art of the currently playing song next to the title."
checked: Settings.data.audio.showMiniplayerAlbumArt
onToggled: checked => {
Settings.data.audio.showMiniplayerAlbumArt = checked
}
}
NToggle {
label: "Show Audio Visualizer In Bar Media Player"
description: "Shows an audio visualizer in the background of the miniplayer."
checked: Settings.data.audio.showMiniplayerCava
onToggled: checked => {
Settings.data.audio.showMiniplayerCava = checked
}
}
}
// Divider
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// AudioService Visualizer Category
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
NText {
text: "Audio Visualizer"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.bottomMargin: Style.marginS * scaling
}
// AudioService Visualizer section
NComboBox {
id: audioVisualizerCombo
label: "Visualization Type"
description: "Choose a visualization type for media playback"
model: ListModel {
ListElement {
key: "none"
name: "None"
}
ListElement {
key: "linear"
name: "Linear"
}
ListElement {
key: "mirrored"
name: "Mirrored"
}
ListElement {
key: "wave"
name: "Wave"
}
}
currentKey: Settings.data.audio.visualizerType
onSelected: key => {
Settings.data.audio.visualizerType = key
}
}
NComboBox {
label: "Frame Rate"
description: "Target frame rate for audio visualizer. (default: 60)"
model: ListModel {
ListElement {
key: "30"
name: "30 FPS"
}
ListElement {
key: "60"
name: "60 FPS"
}
ListElement {
key: "100"
name: "100 FPS"
}
ListElement {
key: "120"
name: "120 FPS"
}
ListElement {
key: "144"
name: "144 FPS"
}
ListElement {
key: "165"
name: "165 FPS"
}
ListElement {
key: "240"
name: "240 FPS"
}
}
currentKey: Settings.data.audio.cavaFrameRate
onSelected: key => {
Settings.data.audio.cavaFrameRate = key
}
Repeater {
model: AudioService.sources
NRadioButton {
required property PwNode modelData
ButtonGroup.group: sources
checked: AudioService.source?.id === modelData.id
onClicked: AudioService.setAudioSource(modelData)
text: modelData.description
}
}
}
}
// Divider
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// Media Player Preferences
ColumnLayout {
spacing: Style.marginL * scaling
NText {
text: "Media Player"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
Layout.bottomMargin: Style.marginS * scaling
}
// Miniplayer section
NToggle {
label: "Show Album Art In Bar Media Player"
description: "Show the album art of the currently playing song next to the title."
checked: Settings.data.audio.showMiniplayerAlbumArt
onToggled: checked => {
Settings.data.audio.showMiniplayerAlbumArt = checked
}
}
NToggle {
label: "Show Audio Visualizer In Bar Media Player"
description: "Shows an audio visualizer in the background of the miniplayer."
checked: Settings.data.audio.showMiniplayerCava
onToggled: checked => {
Settings.data.audio.showMiniplayerCava = checked
}
}
// Preferred player (persistent)
NTextInput {
label: "Preferred Player"
description: "Substring to match MPRIS player (identity/bus/desktop)."
placeholderText: "e.g. spotify, vlc, mpv"
text: Settings.data.audio.preferredPlayer
onTextChanged: {
Settings.data.audio.preferredPlayer = text
MediaService.updateCurrentPlayer()
}
}
// Blacklist editor
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
RowLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
NTextInput {
id: blacklistInput
label: "Blacklist player"
description: "Substring, e.g. plex, shim, mpv."
placeholderText: "type substring and press +"
}
// Button aligned to the center of the actual input field
NIconButton {
icon: "add"
Layout.alignment: Qt.AlignBottom
Layout.bottomMargin: blacklistInput.description ? Style.marginS * scaling : 0
onClicked: {
const val = (blacklistInput.text || "").trim()
if (val !== "") {
const arr = (Settings.data.audio.mprisBlacklist || [])
if (!arr.find(x => String(x).toLowerCase() === val.toLowerCase())) {
Settings.data.audio.mprisBlacklist = [...arr, val]
blacklistInput.text = ""
MediaService.updateCurrentPlayer()
}
}
}
}
}
// Current blacklist entries
Flow {
Layout.fillWidth: true
Layout.leftMargin: Style.marginS * scaling
spacing: Style.marginS * scaling
Repeater {
model: Settings.data.audio.mprisBlacklist
delegate: Rectangle {
required property string modelData
// Padding around the inner row
property real pad: Style.marginS * scaling
// Visuals
color: Color.applyOpacity(Color.mOnSurface, "20")
border.color: Color.applyOpacity(Color.mOnSurface, "50")
border.width: Math.max(1, Style.borderS * scaling)
// Content
RowLayout {
id: chipRow
spacing: Style.marginXS * scaling
anchors.fill: parent
anchors.margins: pad
NText {
text: modelData
color: Color.mOnSurface
font.pointSize: Style.fontSizeS * scaling
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: Style.marginS * scaling
}
NIconButton {
icon: "close"
sizeRatio: 0.8
Layout.alignment: Qt.AlignVCenter
Layout.rightMargin: Style.marginXS * scaling
onClicked: {
const arr = (Settings.data.audio.mprisBlacklist || [])
const idx = arr.findIndex(x => String(x) === modelData)
if (idx >= 0) {
arr.splice(idx, 1)
Settings.data.audio.mprisBlacklist = arr
MediaService.updateCurrentPlayer()
}
}
}
}
// Intrinsic size derived from inner row + padding
implicitWidth: chipRow.implicitWidth + pad * 2
implicitHeight: Math.max(chipRow.implicitHeight + pad * 2, Style.baseWidgetSize * 0.8 * scaling)
radius: Style.radiusM * scaling
}
}
}
}
}
// Divider
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// AudioService Visualizer Category
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
NText {
text: "Audio Visualizer"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
Layout.bottomMargin: Style.marginS * scaling
}
// AudioService Visualizer section
NComboBox {
id: audioVisualizerCombo
label: "Visualization Type"
description: "Choose a visualization type for media playback"
model: ListModel {
ListElement {
key: "none"
name: "None"
}
ListElement {
key: "linear"
name: "Linear"
}
ListElement {
key: "mirrored"
name: "Mirrored"
}
ListElement {
key: "wave"
name: "Wave"
}
}
currentKey: Settings.data.audio.visualizerType
onSelected: key => {
Settings.data.audio.visualizerType = key
}
}
NComboBox {
label: "Frame Rate"
description: "Target frame rate for audio visualizer."
model: ListModel {
ListElement {
key: "30"
name: "30 FPS"
}
ListElement {
key: "60"
name: "60 FPS"
}
ListElement {
key: "100"
name: "100 FPS"
}
ListElement {
key: "120"
name: "120 FPS"
}
ListElement {
key: "144"
name: "144 FPS"
}
ListElement {
key: "165"
name: "165 FPS"
}
ListElement {
key: "240"
name: "240 FPS"
}
}
currentKey: Settings.data.audio.cavaFrameRate
onSelected: key => {
Settings.data.audio.cavaFrameRate = key
}
}
}
// Divider
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
}

View File

@@ -8,202 +8,199 @@ import qs.Widgets
ColumnLayout {
id: root
spacing: 0
ColumnLayout {
spacing: Style.marginL * scaling
ScrollView {
id: scrollView
Layout.fillWidth: true
Layout.fillHeight: true
padding: Style.marginM * scaling
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
RowLayout {
NComboBox {
Layout.fillWidth: true
label: "Bar Position"
description: "Choose where to place the bar on the screen."
model: ListModel {
ListElement {
key: "top"
name: "Top"
}
ListElement {
key: "bottom"
name: "Bottom"
}
}
currentKey: Settings.data.bar.position
onSelected: key => Settings.data.bar.position = key
}
}
ColumnLayout {
width: scrollView.availableWidth
spacing: 0
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
Item {
Layout.fillWidth: true
Layout.preferredHeight: 0
NText {
text: "Background Opacity"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
ColumnLayout {
spacing: Style.marginL * scaling
NText {
text: "Adjust the background opacity of the bar"
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
ColumnLayout {
spacing: Style.marginXXS * scaling
RowLayout {
NSlider {
Layout.fillWidth: true
NText {
text: "Bar Position"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
NText {
text: "Choose where to place the bar on the screen"
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
NComboBox {
Layout.fillWidth: true
model: ListModel {
ListElement {
key: "top"
name: "Top"
}
ListElement {
key: "bottom"
name: "Bottom"
}
}
currentKey: Settings.data.bar.position
onSelected: key => {
Settings.data.bar.position = key
}
}
from: 0
to: 1
stepSize: 0.01
value: Settings.data.bar.backgroundOpacity
onMoved: Settings.data.bar.backgroundOpacity = value
cutoutColor: Color.mSurface
}
ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
NText {
text: "Background Opacity"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
NText {
text: "Adjust the background opacity of the bar"
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
RowLayout {
NSlider {
Layout.fillWidth: true
from: 0
to: 1
stepSize: 0.01
value: Settings.data.bar.backgroundOpacity
onMoved: Settings.data.bar.backgroundOpacity = value
cutoutColor: Color.mSurface
}
NText {
text: Math.floor(Settings.data.bar.backgroundOpacity * 100) + "%"
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: Style.marginS * scaling
color: Color.mOnSurface
}
}
}
NToggle {
label: "Show Active Window's Icon"
description: "Display the app icon next to the title of the currently focused window."
checked: Settings.data.bar.showActiveWindowIcon
onToggled: checked => {
Settings.data.bar.showActiveWindowIcon = checked
}
}
NToggle {
label: "Show Battery Percentage"
description: "Show battery percentage at all times."
checked: Settings.data.bar.alwaysShowBatteryPercentage
onToggled: checked => {
Settings.data.bar.alwaysShowBatteryPercentage = checked
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL * scaling
Layout.bottomMargin: Style.marginL * scaling
}
// Widgets Management Section
ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
NText {
text: "Widgets Positioning"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.bottomMargin: Style.marginS * scaling
}
NText {
text: "Drag and drop widgets to reorder them within each section, or use the add/remove buttons to manage widgets."
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
// Bar Sections
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.topMargin: Style.marginM * scaling
spacing: Style.marginM * scaling
// Left Section
NSectionEditor {
sectionName: "Left"
widgetModel: Settings.data.bar.widgets.left
availableWidgets: availableWidgets
scrollView: scrollView
onAddWidget: (widgetName, section) => addWidgetToSection(widgetName, section)
onRemoveWidget: (section, index) => removeWidgetFromSection(section, index)
onReorderWidget: (section, fromIndex, toIndex) => reorderWidgetInSection(section, fromIndex, toIndex)
}
// Center Section
NSectionEditor {
sectionName: "Center"
widgetModel: Settings.data.bar.widgets.center
availableWidgets: availableWidgets
scrollView: scrollView
onAddWidget: (widgetName, section) => addWidgetToSection(widgetName, section)
onRemoveWidget: (section, index) => removeWidgetFromSection(section, index)
onReorderWidget: (section, fromIndex, toIndex) => reorderWidgetInSection(section, fromIndex, toIndex)
}
// Right Section
NSectionEditor {
sectionName: "Right"
widgetModel: Settings.data.bar.widgets.right
availableWidgets: availableWidgets
scrollView: scrollView
onAddWidget: (widgetName, section) => addWidgetToSection(widgetName, section)
onRemoveWidget: (section, index) => removeWidgetFromSection(section, index)
onReorderWidget: (section, fromIndex, toIndex) => reorderWidgetInSection(section, fromIndex, toIndex)
}
}
NText {
text: Math.floor(Settings.data.bar.backgroundOpacity * 100) + "%"
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: Style.marginS * scaling
color: Color.mOnSurface
}
}
}
NToggle {
label: "Show Active Window's Icon"
description: "Display the app icon next to the title of the currently focused window."
checked: Settings.data.bar.showActiveWindowIcon
onToggled: checked => {
Settings.data.bar.showActiveWindowIcon = checked
}
}
NToggle {
label: "Show Battery Percentage"
description: "Display battery percentage at all times."
checked: Settings.data.bar.alwaysShowBatteryPercentage
onToggled: checked => {
Settings.data.bar.alwaysShowBatteryPercentage = checked
}
}
NToggle {
label: "Show Network Statistics"
description: "Display network upload and download speeds in the system monitor."
checked: Settings.data.bar.showNetworkStats
onToggled: checked => {
Settings.data.bar.showNetworkStats = checked
}
}
NComboBox {
label: "Show Workspaces Labels"
description: "Display the workspace name or index in the workspace indicator"
model: ListModel {
ListElement {
key: "none"
name: "None"
}
ListElement {
key: "index"
name: "Index"
}
ListElement {
key: "name"
name: "Name"
}
}
currentKey: Settings.data.bar.showWorkspaceLabel
onSelected: key => {
Settings.data.bar.showWorkspaceLabel = key
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// Widgets Management Section
ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
NText {
text: "Widgets Positioning"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.bottomMargin: Style.marginS * scaling
}
NText {
text: "Drag and drop widgets to reorder them within each section, or use the add/remove buttons to manage widgets."
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
// Bar Sections
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.topMargin: Style.marginM * scaling
spacing: Style.marginM * scaling
// Left Section
NSectionEditor {
sectionName: "Left"
sectionId: "left"
widgetModel: Settings.data.bar.widgets.left
availableWidgets: availableWidgets
onAddWidget: (widgetName, section) => addWidgetToSection(widgetName, section)
onRemoveWidget: (section, index) => removeWidgetFromSection(section, index)
onReorderWidget: (section, fromIndex, toIndex) => reorderWidgetInSection(section, fromIndex, toIndex)
}
// Center Section
NSectionEditor {
sectionName: "Center"
sectionId: "center"
widgetModel: Settings.data.bar.widgets.center
availableWidgets: availableWidgets
onAddWidget: (widgetName, section) => addWidgetToSection(widgetName, section)
onRemoveWidget: (section, index) => removeWidgetFromSection(section, index)
onReorderWidget: (section, fromIndex, toIndex) => reorderWidgetInSection(section, fromIndex, toIndex)
}
// Right Section
NSectionEditor {
sectionName: "Right"
sectionId: "right"
widgetModel: Settings.data.bar.widgets.right
availableWidgets: availableWidgets
onAddWidget: (widgetName, section) => addWidgetToSection(widgetName, section)
onRemoveWidget: (section, index) => removeWidgetFromSection(section, index)
onReorderWidget: (section, fromIndex, toIndex) => reorderWidgetInSection(section, fromIndex, toIndex)
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// Helper functions
function addWidgetToSection(widgetName, section) {
//Logger.log("BarTab", "Adding widget", widgetName, "to section", section)
var sectionArray = Settings.data.bar.widgets[section]
if (sectionArray) {
// Create a new array to avoid modifying the original
var newArray = sectionArray.slice()

View File

@@ -2,165 +2,325 @@ import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services
import qs.Widgets
Item {
readonly property real scaling: ScalingService.scale(screen)
readonly property string tabIcon: "brightness_6"
readonly property string tabLabel: "Brightness"
Layout.fillWidth: true
Layout.fillHeight: true
ColumnLayout {
id: root
ScrollView {
anchors.fill: parent
clip: true
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ScrollBar.horizontal.policy: ScrollBar.AsNeeded
contentWidth: parent.width
// Time dropdown options (00:00 .. 23:30)
ListModel {
id: timeOptions
}
Component.onCompleted: {
for (var h = 0; h < 24; h++) {
for (var m = 0; m < 60; m += 30) {
var hh = ("0" + h).slice(-2)
var mm = ("0" + m).slice(-2)
var key = hh + ":" + mm
timeOptions.append({
"key": key,
"name": key
})
}
}
}
ColumnLayout {
width: parent.width
ColumnLayout {
spacing: Style.marginL * scaling
Layout.margins: Style.marginL * scaling
// Check for wlsunset availability when enabling Night Light
Process {
id: wlsunsetCheck
command: ["which", "wlsunset"]
running: false
onExited: function (exitCode) {
if (exitCode === 0) {
Settings.data.nightLight.enabled = true
NightLightService.apply()
ToastService.showNotice("Night Light", "Enabled")
} else {
Settings.data.nightLight.enabled = false
ToastService.showWarning("Night Light", "wlsunset not installed")
}
}
stdout: StdioCollector {}
stderr: StdioCollector {}
}
spacing: Style.marginL * scaling
// Brightness Step Section
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
NSpinBox {
Layout.fillWidth: true
label: "Brightness Step Size"
description: "Adjust the step size for brightness changes (scroll wheel, keyboard shortcuts)."
minimum: 1
maximum: 50
value: Settings.data.brightness.brightnessStep
stepSize: 1
suffix: "%"
onValueChanged: {
Settings.data.brightness.brightnessStep = value
}
}
}
// Monitor Overview Section
ColumnLayout {
spacing: Style.marginL * scaling
NLabel {
label: "Monitors Brightness Control"
description: "Current brightness levels for all detected monitors."
}
// Single monitor display using the same data source as the bar icon
Repeater {
model: BrightnessService.monitors
Rectangle {
Layout.fillWidth: true
radius: Style.radiusM * scaling
color: Color.mSurface
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
implicitHeight: contentCol.implicitHeight + Style.marginXL * 2 * scaling
NText {
text: "Brightness Settings"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
// Bar Visibility Section
NToggle {
label: "Show Bar Icon"
description: "Display the brightness control icon in the bar."
checked: Settings.data.bar.showBrightness
onToggled: checked => {
Settings.data.bar.showBrightness = checked
}
}
// Brightness Step Section
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
id: contentCol
anchors.fill: parent
anchors.margins: Style.marginL * scaling
spacing: Style.marginM * scaling
NSpinBox {
RowLayout {
Layout.fillWidth: true
label: "Brightness Step Size"
description: "Adjust the step size for brightness changes (scroll wheel, keyboard shortcuts)."
minimum: 1
maximum: 50
value: Settings.data.brightness.brightnessStep
stepSize: 1
suffix: "%"
onValueChanged: {
Settings.data.brightness.brightnessStep = value
spacing: Style.marginM * scaling
NText {
text: `${model.modelData.name} [${model.modelData.model}]`
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
}
Item {
Layout.fillWidth: true
}
NText {
text: model.method
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignRight
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL * scaling
Layout.bottomMargin: Style.marginL * scaling
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
// Monitor Overview Section
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
NText {
text: "Brightness:"
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurface
}
NLabel {
label: "Monitors Brightness Control"
description: "Current brightness levels for all detected monitors."
}
// Single monitor display using the same data source as the bar icon
Repeater {
model: BrightnessService.monitors
Rectangle {
NSlider {
Layout.fillWidth: true
radius: Style.radiusM * scaling
color: Color.mSurface
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
implicitHeight: contentCol.implicitHeight + Style.marginXL * 2 * scaling
ColumnLayout {
id: contentCol
anchors.fill: parent
anchors.margins: Style.marginL * scaling
spacing: Style.marginM * scaling
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NText {
text: `${model.modelData.name} [${model.modelData.model}]`
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
}
Item {
Layout.fillWidth: true
}
NText {
text: model.method
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignRight
}
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NText {
text: "Brightness:"
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurface
}
NSlider {
Layout.fillWidth: true
from: 0
to: 1
value: model.brightness
stepSize: 0.05
onPressedChanged: {
if (!pressed) {
var monitor = BrightnessService.getMonitorForScreen(model.modelData)
monitor.setBrightness(value)
}
}
}
NText {
text: Math.round(model.brightness * 100) + "%"
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
Layout.alignment: Qt.AlignRight
}
from: 0
to: 1
value: model.brightness
stepSize: 0.05
onPressedChanged: {
if (!pressed) {
var monitor = BrightnessService.getMonitorForScreen(model.modelData)
monitor.setBrightness(value)
}
}
}
}
}
Item {
Layout.fillHeight: true
NText {
text: Math.round(model.brightness * 100) + "%"
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
Layout.alignment: Qt.AlignRight
}
}
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// Night Light Section
ColumnLayout {
spacing: Style.marginXS * scaling
Layout.fillWidth: true
NText {
text: "Night Light"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
}
NText {
text: "Reduce blue light emission to help you sleep better and reduce eye strain."
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
NToggle {
label: "Enable Night Light"
description: "Apply a warm color filter to reduce blue light emission."
checked: Settings.data.nightLight.enabled
onToggled: checked => {
if (checked) {
// Verify wlsunset exists before enabling
wlsunsetCheck.running = true
} else {
Settings.data.nightLight.enabled = false
NightLightService.apply()
ToastService.showNotice("Night Light", "Disabled")
}
}
}
// Temperature
ColumnLayout {
spacing: Style.marginXS * scaling
Layout.alignment: Qt.AlignVCenter
NLabel {
label: "Color temperature"
description: "Choose two temperatures in Kelvin."
}
RowLayout {
visible: Settings.data.nightLight.enabled
spacing: Style.marginM * scaling
Layout.fillWidth: false
Layout.fillHeight: true
Layout.alignment: Qt.AlignVCenter
NText {
text: "Night"
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignVCenter
}
NTextInput {
text: Settings.data.nightLight.nightTemp
inputMethodHints: Qt.ImhDigitsOnly
Layout.alignment: Qt.AlignVCenter
onEditingFinished: {
var nightTemp = parseInt(text)
var dayTemp = parseInt(Settings.data.nightLight.dayTemp)
if (!isNaN(nightTemp) && !isNaN(dayTemp)) {
// Clamp value between [1000 .. (dayTemp-500)]
var clampedValue = Math.min(dayTemp - 500, Math.max(1000, nightTemp))
text = Settings.data.nightLight.nightTemp = clampedValue.toString()
}
}
}
Item {}
NText {
text: "Day"
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignVCenter
}
NTextInput {
text: Settings.data.nightLight.dayTemp
inputMethodHints: Qt.ImhDigitsOnly
Layout.alignment: Qt.AlignVCenter
onEditingFinished: {
var dayTemp = parseInt(text)
var nightTemp = parseInt(Settings.data.nightLight.nightTemp)
if (!isNaN(nightTemp) && !isNaN(dayTemp)) {
// Clamp value between [(nightTemp+500) .. 6500]
var clampedValue = Math.max(nightTemp + 500, Math.min(6500, dayTemp))
text = Settings.data.nightLight.dayTemp = clampedValue.toString()
}
}
}
}
}
NToggle {
label: "Automatic Scheduling"
description: `Based on the sunset and sunrise time in <i>${LocationService.stableName}</i> - recommended.`
checked: Settings.data.nightLight.autoSchedule
onToggled: checked => Settings.data.nightLight.autoSchedule = checked
visible: Settings.data.nightLight.enabled
}
// Schedule settings
ColumnLayout {
spacing: Style.marginXS * scaling
visible: Settings.data.nightLight.enabled && !Settings.data.nightLight.autoSchedule
RowLayout {
Layout.fillWidth: false
spacing: Style.marginM * scaling
NLabel {
label: "Manual Scheduling"
}
Item {// add a little more spacing
}
NText {
text: "Sunrise Time"
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
}
NComboBox {
model: timeOptions
currentKey: Settings.data.nightLight.manualSunrise
placeholder: "Select start time"
onSelected: key => {
Settings.data.nightLight.manualSunrise = key
}
preferredWidth: 120 * scaling
}
Item {// add a little more spacing
}
NText {
text: "Sunset Time"
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
}
NComboBox {
model: timeOptions
currentKey: Settings.data.nightLight.manualSunset
placeholder: "Select stop time"
onSelected: key => {
Settings.data.nightLight.manualSunset = key
}
preferredWidth: 120 * scaling
}
}
}
}

View File

@@ -9,7 +9,12 @@ import qs.Widgets
ColumnLayout {
id: root
spacing: 0
// Cache for scheme JSON (can be flat or {dark, light})
property var schemeColorsCache: ({})
// Scale properties for card animations
property real cardScaleLow: 0.95
property real cardScaleHigh: 1.0
// Helper function to get color from scheme file (supports dark/light variants)
function getSchemeColor(schemePath, colorKey) {
@@ -31,13 +36,6 @@ ColumnLayout {
return "#000000"
}
// Cache for scheme JSON (can be flat or {dark, light})
property var schemeColorsCache: ({})
// Scale properties for card animations
property real cardScaleLow: 0.95
property real cardScaleHigh: 1.0
// This function is called by the FileView Repeater when a scheme file is loaded
function schemeLoaded(schemeName, jsonData) {
var value = jsonData || {}
@@ -55,6 +53,29 @@ ColumnLayout {
}
}
// Simple process to check if matugen exists
Process {
id: matugenCheck
command: ["which", "matugen"]
running: false
onExited: function (exitCode) {
if (exitCode === 0) {
// Matugen exists, enable it
Settings.data.colorSchemes.useWallpaperColors = true
Settings.data.colorSchemes.predefinedScheme = ""
MatugenService.generateFromWallpaper()
ToastService.showNotice("Matugen", "Enabled")
} else {
// Matugen not found
ToastService.showWarning("Matugen", "Not installed")
}
}
stdout: StdioCollector {}
stderr: StdioCollector {}
}
// A non-visual Item to host the Repeater that loads the color scheme files.
Item {
visible: false
@@ -83,285 +104,384 @@ ColumnLayout {
}
}
// UI Code
ScrollView {
id: scrollView
ColumnLayout {
spacing: 0
Layout.fillWidth: true
Layout.fillHeight: true
padding: Style.marginM * scaling
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
Item {
Layout.fillWidth: true
Layout.preferredHeight: 0
}
ColumnLayout {
width: scrollView.availableWidth
spacing: 0
spacing: Style.marginL * scaling
Layout.fillWidth: true
Item {
// Dark Mode Toggle (affects both Matugen and predefined schemes that provide variants)
NToggle {
label: "Dark Mode"
description: Settings.data.colorSchemes.useWallpaperColors ? "Generate dark theme colors when using Matugen." : "Use a dark variant if available."
checked: Settings.data.colorSchemes.darkMode
enabled: true
onToggled: checked => {
Settings.data.colorSchemes.darkMode = checked
if (Settings.data.colorSchemes.useWallpaperColors) {
MatugenService.generateFromWallpaper()
} else if (Settings.data.colorSchemes.predefinedScheme) {
// Re-apply current scheme to pick the right variant
ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme)
// Force refresh of previews
var tmp = schemeColorsCache
schemeColorsCache = {}
schemeColorsCache = tmp
}
}
}
// Use Matugen
NToggle {
label: "Enable Matugen"
description: "Automatically generate colors based on your active wallpaper."
checked: Settings.data.colorSchemes.useWallpaperColors
onToggled: checked => {
if (checked) {
// Check if matugen is installed
matugenCheck.running = true
} else {
Settings.data.colorSchemes.useWallpaperColors = false
ToastService.showNotice("Matugen", "Disabled")
}
}
}
NDivider {
Layout.fillWidth: true
Layout.preferredHeight: 0
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
ColumnLayout {
spacing: Style.marginL * scaling
spacing: Style.marginS * scaling
Layout.fillWidth: true
// Dark Mode Toggle (affects both Matugen and predefined schemes that provide variants)
NToggle {
label: "Dark Mode"
description: Settings.data.colorSchemes.useWallpaperColors ? "Generate dark theme colors when using Matugen." : "Use a dark variant if available."
checked: Settings.data.colorSchemes.darkMode
enabled: true
onToggled: checked => {
Settings.data.colorSchemes.darkMode = checked
if (Settings.data.colorSchemes.useWallpaperColors) {
ColorSchemeService.changedWallpaper()
} else if (Settings.data.colorSchemes.predefinedScheme) {
// Re-apply current scheme to pick the right variant
ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme)
// Force refresh of previews
var tmp = schemeColorsCache
schemeColorsCache = {}
schemeColorsCache = tmp
}
}
NText {
text: "Predefined Color Schemes"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
}
// App theming
NToggle {
label: "Theme external apps (GTK, Qt & kitty)"
description: "Writes GTK (gtk.css), Qt5/6 (noctalia.conf) and Kitty (noctalia.conf) themes based on your colors."
checked: Settings.data.colorSchemes.themeApps
onToggled: checked => {
Settings.data.colorSchemes.themeApps = checked
if (Settings.data.colorSchemes.useWallpaperColors) {
ColorSchemeService.changedWallpaper()
}
}
}
// Use Matugen
NToggle {
label: "Enable Matugen"
description: "Automatically generate colors based on your active wallpaper."
checked: Settings.data.colorSchemes.useWallpaperColors
onToggled: checked => {
if (checked) {
// Check if matugen is installed
matugenCheck.running = true
} else {
Settings.data.colorSchemes.useWallpaperColors = false
ToastService.showNotice("Matugen", "Disabled")
}
}
}
NDivider {
NText {
text: "These color schemes are only active when 'Use Matugen' is turned off. With Matugen enabled, colors will be automatically generated from your wallpaper. You can still switch between light and dark themes while using Matugen."
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
Layout.fillWidth: true
Layout.topMargin: Style.marginL * scaling
Layout.bottomMargin: Style.marginL * scaling
wrapMode: Text.WordWrap
}
}
ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
// Color Schemes Grid
GridLayout {
columns: 3
rowSpacing: Style.marginM * scaling
columnSpacing: Style.marginM * scaling
Layout.fillWidth: true
Repeater {
model: ColorSchemeService.schemes
Rectangle {
id: schemeCard
property string schemePath: modelData
NText {
text: "Predefined Color Schemes"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
}
Layout.preferredHeight: 120 * scaling
radius: Style.radiusM * scaling
color: getSchemeColor(modelData, "mSurface")
border.width: Math.max(1, Style.borderL * scaling)
border.color: Settings.data.colorSchemes.predefinedScheme === modelData ? Color.mPrimary : Color.mOutline
scale: root.cardScaleLow
NText {
text: "These color schemes only apply when 'Use Matugen' is disabled. When enabled, Matugen will generate colors based on your wallpaper instead. You can toggle between light and dark themes when using Matugen."
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurface
Layout.fillWidth: true
wrapMode: Text.WordWrap
}
}
// Mouse area for selection
MouseArea {
anchors.fill: parent
onClicked: {
// Disable useWallpaperColors when picking a predefined color scheme
Settings.data.colorSchemes.useWallpaperColors = false
Logger.log("ColorSchemeTab", "Disabled matugen setting")
ColumnLayout {
spacing: Style.marginXS * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginL * scaling
Settings.data.colorSchemes.predefinedScheme = schemePath
ColorSchemeService.applyScheme(schemePath)
}
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
// Color Schemes Grid
GridLayout {
columns: 4
rowSpacing: Style.marginL * scaling
columnSpacing: Style.marginL * scaling
Layout.fillWidth: true
onEntered: {
schemeCard.scale = root.cardScaleHigh
}
Repeater {
model: ColorSchemeService.schemes
onExited: {
schemeCard.scale = root.cardScaleLow
}
}
Rectangle {
id: schemeCard
property string schemePath: modelData
// Card content
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginXL * scaling
spacing: Style.marginS * scaling
// Scheme name
NText {
text: {
// Remove json and the full path
var chunks = schemePath.replace(".json", "").split("/")
return chunks[chunks.length - 1]
}
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
color: getSchemeColor(modelData, "mOnSurface")
Layout.fillWidth: true
Layout.preferredHeight: 120 * scaling
radius: Style.radiusM * scaling
color: getSchemeColor(modelData, "mSurface")
border.width: Math.max(1, Style.borderL * scaling)
border.color: Settings.data.colorSchemes.predefinedScheme === modelData ? Color.mPrimary : Color.mOutline
scale: root.cardScaleLow
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
}
// Mouse area for selection
MouseArea {
anchors.fill: parent
onClicked: {
// Disable useWallpaperColors when picking a predefined color scheme
Settings.data.colorSchemes.useWallpaperColors = false
Logger.log("ColorSchemeTab", "Disabled matugen setting")
// Color swatches
RowLayout {
id: swatches
Settings.data.colorSchemes.predefinedScheme = schemePath
ColorSchemeService.applyScheme(schemePath)
}
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
spacing: Style.marginS * scaling
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
onEntered: {
schemeCard.scale = root.cardScaleHigh
}
readonly property int swatchSize: 20 * scaling
onExited: {
schemeCard.scale = root.cardScaleLow
}
}
// Card content
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginXL * scaling
spacing: Style.marginS * scaling
// Scheme name
NText {
text: {
// Remove json and the full path
var chunks = schemePath.replace(".json", "").split("/")
return chunks[chunks.length - 1]
}
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
color: getSchemeColor(modelData, "mOnSurface")
Layout.fillWidth: true
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
}
// Color swatches
RowLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
// Primary color swatch
Rectangle {
width: 28 * scaling
height: 28 * scaling
radius: width * 0.5
color: getSchemeColor(modelData, "mPrimary")
}
// Secondary color swatch
Rectangle {
width: 28 * scaling
height: 28 * scaling
radius: width * 0.5
color: getSchemeColor(modelData, "mSecondary")
}
// Tertiary color swatch
Rectangle {
width: 28 * scaling
height: 28 * scaling
radius: width * 0.5
color: getSchemeColor(modelData, "mTertiary")
}
// Error color swatch
Rectangle {
width: 28 * scaling
height: 28 * scaling
radius: width * 0.5
color: getSchemeColor(modelData, "mError")
}
}
}
// Selection indicator
// Primary color swatch
Rectangle {
visible: Settings.data.colorSchemes.predefinedScheme === schemePath
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Style.marginS * scaling
width: 24 * scaling
height: 24 * scaling
width: swatches.swatchSize
height: swatches.swatchSize
radius: width * 0.5
color: Color.mPrimary
NText {
anchors.centerIn: parent
text: "✓"
font.pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightBold
color: Color.mOnPrimary
}
color: getSchemeColor(modelData, "mPrimary")
}
// Smooth animations
Behavior on scale {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutCubic
}
// Secondary color swatch
Rectangle {
width: swatches.swatchSize
height: swatches.swatchSize
radius: width * 0.5
color: getSchemeColor(modelData, "mSecondary")
}
Behavior on border.color {
ColorAnimation {
duration: Style.animationNormal
}
// Tertiary color swatch
Rectangle {
width: swatches.swatchSize
height: swatches.swatchSize
radius: width * 0.5
color: getSchemeColor(modelData, "mTertiary")
}
Behavior on border.width {
NumberAnimation {
duration: Style.animationFast
}
// Error color swatch
Rectangle {
width: swatches.swatchSize
height: swatches.swatchSize
radius: width * 0.5
color: getSchemeColor(modelData, "mError")
}
}
}
// Selection indicator
Rectangle {
visible: Settings.data.colorSchemes.predefinedScheme === schemePath
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Style.marginS * scaling
width: 24 * scaling
height: 24 * scaling
radius: width * 0.5
color: Color.mPrimary
NText {
anchors.centerIn: parent
text: "✓"
font.pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightBold
color: Color.mOnPrimary
}
}
// Smooth animations
Behavior on scale {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutCubic
}
}
Behavior on border.color {
ColorAnimation {
duration: Style.animationNormal
}
}
Behavior on border.width {
NumberAnimation {
duration: Style.animationFast
}
}
}
}
}
}
}
// Simple process to check if matugen exists
Process {
id: matugenCheck
command: ["which", "matugen"]
running: false
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
visible: Settings.data.colorSchemes.useWallpaperColors
}
onExited: function (exitCode) {
if (exitCode === 0) {
// Matugen exists, enable it
Settings.data.colorSchemes.useWallpaperColors = true
ColorSchemeService.changedWallpaper()
ToastService.showNotice("Matugen", "Enabled!")
} else {
// Matugen not found
ToastService.showWarning("Matugen", "Not installed!")
// Matugen template toggles (moved from MatugenTab)
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
visible: Settings.data.colorSchemes.useWallpaperColors
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
NText {
text: "Matugen Templates"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
}
NText {
text: "Select which external components Matugen should apply theming to."
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
Layout.fillWidth: true
wrapMode: Text.WordWrap
}
}
stdout: StdioCollector {}
stderr: StdioCollector {}
NCheckbox {
label: "GTK 4 (libadwaita)"
description: "Write ~/.config/gtk-4.0/gtk.css"
checked: Settings.data.matugen.gtk4
onToggled: checked => {
Settings.data.matugen.gtk4 = checked
if (Settings.data.colorSchemes.useWallpaperColors)
MatugenService.generateFromWallpaper()
}
}
NCheckbox {
label: "GTK 3"
description: "Write ~/.config/gtk-3.0/gtk.css"
checked: Settings.data.matugen.gtk3
onToggled: checked => {
Settings.data.matugen.gtk3 = checked
if (Settings.data.colorSchemes.useWallpaperColors)
MatugenService.generateFromWallpaper()
}
}
NCheckbox {
label: "Qt6ct"
description: "Write ~/.config/qt6ct/colors/noctalia.conf"
checked: Settings.data.matugen.qt6
onToggled: checked => {
Settings.data.matugen.qt6 = checked
if (Settings.data.colorSchemes.useWallpaperColors)
MatugenService.generateFromWallpaper()
}
}
NCheckbox {
label: "Qt5ct"
description: "Write ~/.config/qt5ct/colors/noctalia.conf"
checked: Settings.data.matugen.qt5
onToggled: checked => {
Settings.data.matugen.qt5 = checked
if (Settings.data.colorSchemes.useWallpaperColors)
MatugenService.generateFromWallpaper()
}
}
NCheckbox {
label: "Kitty"
description: "Write ~/.config/kitty/themes/noctalia.conf and reload"
checked: Settings.data.matugen.kitty
onToggled: checked => {
Settings.data.matugen.kitty = checked
if (Settings.data.colorSchemes.useWallpaperColors)
MatugenService.generateFromWallpaper()
}
}
NCheckbox {
label: "Ghostty"
description: "Write ~/.config/ghostty/themes/noctalia and reload"
checked: Settings.data.matugen.ghostty
onToggled: checked => {
Settings.data.matugen.ghostty = checked
if (Settings.data.colorSchemes.useWallpaperColors)
MatugenService.generateFromWallpaper()
}
}
NCheckbox {
label: "Foot"
description: "Write ~/.config/foot/themes/noctalia and reload"
checked: Settings.data.matugen.foot
onToggled: checked => {
Settings.data.matugen.foot = checked
if (Settings.data.colorSchemes.useWallpaperColors)
MatugenService.generateFromWallpaper()
}
}
NCheckbox {
label: "Fuzzel"
description: "Write ~/.config/fuzzel/themes/noctalia and reload"
checked: Settings.data.matugen.fuzzel
onToggled: checked => {
Settings.data.matugen.fuzzel = checked
if (Settings.data.colorSchemes.useWallpaperColors)
MatugenService.generateFromWallpaper()
}
}
NCheckbox {
label: "Vesktop"
description: "Write ~/.config/vesktop/themes/noctalia.theme.css"
checked: Settings.data.matugen.vesktop
onToggled: checked => {
Settings.data.matugen.vesktop = checked
if (Settings.data.colorSchemes.useWallpaperColors)
MatugenService.generateFromWallpaper()
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginM * scaling
Layout.bottomMargin: Style.marginM * scaling
}
NCheckbox {
label: "User Templates"
description: "Enable user-defined Matugen config from ~/.config/matugen/config.toml"
checked: Settings.data.matugen.enableUserTemplates
onToggled: checked => {
Settings.data.matugen.enableUserTemplates = checked
if (Settings.data.colorSchemes.useWallpaperColors)
MatugenService.generateFromWallpaper()
}
}
}
}

View File

@@ -6,13 +6,8 @@ import qs.Commons
import qs.Services
import qs.Widgets
Item {
readonly property real scaling: ScalingService.scale(screen)
readonly property string tabIcon: "monitor"
readonly property string tabLabel: "Display"
readonly property int tabIndex: 5
Layout.fillWidth: true
Layout.fillHeight: true
ColumnLayout {
id: root
// Helper functions to update arrays immutably
function addMonitor(list, name) {
@@ -27,195 +22,173 @@ Item {
})
}
ScrollView {
anchors.fill: parent
clip: true
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ScrollBar.horizontal.policy: ScrollBar.AsNeeded
contentWidth: parent.width
NText {
text: "Monitor-specific configuration"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
}
ColumnLayout {
id: contentColumn
width: Math.max(parent.width, 300) // Minimum reasonable width without scaling
NText {
text: "Bars and notifications appear on all displays by default. Choose specific displays below to limit where they're shown."
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
ColumnLayout {
spacing: Style.marginL * scaling
Layout.margins: Style.marginL * scaling
ColumnLayout {
spacing: Style.marginL * scaling
Layout.topMargin: Style.marginL * scaling
Repeater {
model: Quickshell.screens || []
delegate: Rectangle {
Layout.fillWidth: true
Layout.minimumWidth: 550 * scaling
radius: Style.radiusM * scaling
color: Color.mSurface
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
implicitHeight: contentCol.implicitHeight + Style.marginXL * 2 * scaling
NText {
text: "Permonitor configuration"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
property real localScaling: ScalingService.getScreenScale(modelData)
Connections {
target: ScalingService
function onScaleChanged(screenName, scale) {
if (screenName === modelData.name) {
localScaling = scale
}
}
}
NText {
text: "By default, bars and notifications are shown on all displays. Select one or more below to narrow your view."
font.pointSize: Style.fontSize * scaling
color: Color.mOnSurfaceVariant
}
ColumnLayout {
id: contentCol
anchors.fill: parent
anchors.margins: Style.marginL * scaling
spacing: Style.marginXXS * scaling
Repeater {
model: Quickshell.screens || []
delegate: Rectangle {
NText {
text: (modelData.name || "Unknown")
font.pointSize: Style.fontSizeXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
}
NText {
text: `Resolution: ${modelData.width}x${modelData.height} - Position: (${modelData.x}, ${modelData.y})`
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
// Remove the scaling-based minimum width that causes issues at low scaling
// Layout.minimumWidth: 400 * scaling
radius: Style.radiusM * scaling
color: Color.mSurface
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
implicitHeight: contentCol.implicitHeight + Style.marginXL * 2 * scaling
}
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NToggle {
Layout.fillWidth: true
label: "Bar"
description: "Enable the bar on this monitor."
checked: (Settings.data.bar.monitors || []).indexOf(modelData.name) !== -1
onToggled: checked => {
if (checked) {
Settings.data.bar.monitors = addMonitor(Settings.data.bar.monitors, modelData.name)
} else {
Settings.data.bar.monitors = removeMonitor(Settings.data.bar.monitors, modelData.name)
}
}
}
NToggle {
Layout.fillWidth: true
label: "Notifications"
description: "Enable notifications on this monitor."
checked: (Settings.data.notifications.monitors || []).indexOf(modelData.name) !== -1
onToggled: checked => {
if (checked) {
Settings.data.notifications.monitors = addMonitor(Settings.data.notifications.monitors,
modelData.name)
} else {
Settings.data.notifications.monitors = removeMonitor(Settings.data.notifications.monitors,
modelData.name)
}
}
}
NToggle {
Layout.fillWidth: true
label: "Dock"
description: "Enable the dock on this monitor."
checked: (Settings.data.dock.monitors || []).indexOf(modelData.name) !== -1
onToggled: checked => {
if (checked) {
Settings.data.dock.monitors = addMonitor(Settings.data.dock.monitors, modelData.name)
} else {
Settings.data.dock.monitors = removeMonitor(Settings.data.dock.monitors, modelData.name)
}
}
}
ColumnLayout {
id: contentCol
anchors.fill: parent
anchors.margins: Style.marginL * scaling
spacing: Style.marginXXS * scaling
Layout.minimumWidth: 0
spacing: Style.marginS * scaling
Layout.fillWidth: true
NText {
text: (modelData.name || "Unknown")
font.pointSize: Style.fontSizeXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
}
NText {
text: `Resolution: ${modelData.width}x${modelData.height} - Position: (${modelData.x}, ${modelData.y})`
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
}
ColumnLayout {
spacing: Style.marginL * scaling
Layout.minimumWidth: 0
RowLayout {
Layout.fillWidth: true
NToggle {
Layout.fillWidth: true
Layout.minimumWidth: 0
label: "Bar"
description: "Enable the bar on this monitor."
checked: (Settings.data.bar.monitors || []).indexOf(modelData.name) !== -1
onToggled: checked => {
if (checked) {
Settings.data.bar.monitors = addMonitor(Settings.data.bar.monitors, modelData.name)
} else {
Settings.data.bar.monitors = removeMonitor(Settings.data.bar.monitors, modelData.name)
}
}
}
NToggle {
Layout.fillWidth: true
Layout.minimumWidth: 0
label: "Notifications"
description: "Enable notifications on this monitor."
checked: (Settings.data.notifications.monitors || []).indexOf(modelData.name) !== -1
onToggled: checked => {
if (checked) {
Settings.data.notifications.monitors = addMonitor(
Settings.data.notifications.monitors, modelData.name)
} else {
Settings.data.notifications.monitors = removeMonitor(
Settings.data.notifications.monitors, modelData.name)
}
}
}
NToggle {
Layout.fillWidth: true
Layout.minimumWidth: 0
label: "Dock"
description: "Enable the dock on this monitor."
checked: (Settings.data.dock.monitors || []).indexOf(modelData.name) !== -1
onToggled: checked => {
if (checked) {
Settings.data.dock.monitors = addMonitor(Settings.data.dock.monitors, modelData.name)
} else {
Settings.data.dock.monitors = removeMonitor(Settings.data.dock.monitors,
modelData.name)
}
}
}
spacing: Style.marginL * scaling
ColumnLayout {
spacing: Style.marginL * scaling
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
RowLayout {
Layout.fillWidth: true
ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
Layout.minimumWidth: 0
NText {
text: "Scale"
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
NText {
text: "Scale the user interface on this monitor."
font.pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
NText {
text: `${Math.round(ScalingService.scaleByName(modelData.name) * 100)}%`
Layout.alignment: Qt.AlignVCenter
Layout.minimumWidth: implicitWidth
}
NText {
text: "Scale"
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
RowLayout {
spacing: Style.marginS * scaling
NText {
text: "Scale the user interface on this monitor."
font.pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
Layout.minimumWidth: 0
NSlider {
id: scaleSlider
from: 0.6
to: 1.8
stepSize: 0.01
value: ScalingService.scaleByName(modelData.name)
onPressedChanged: {
var data = Settings.data.monitorsScaling || {}
data[modelData.name] = value
Settings.data.monitorsScaling = data
}
Layout.fillWidth: true
Layout.minimumWidth: 50 // Ensure minimum slider width
}
NIconButton {
icon: "refresh"
tooltipText: "Reset Scaling"
fontPointSize: Style.fontSizeL * scaling
Layout.preferredWidth: implicitWidth
Layout.minimumWidth: implicitWidth
onClicked: {
var data = Settings.data.monitorsScaling || {}
data[modelData.name] = 1.0
Settings.data.monitorsScaling = data
}
}
}
}
NText {
text: `${Math.round(localScaling * 100)}%`
Layout.alignment: Qt.AlignVCenter
Layout.minimumWidth: 50 * scaling
horizontalAlignment: Text.AlignRight
}
}
RowLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
NSlider {
id: scaleSlider
from: 0.7
to: 1.8
stepSize: 0.01
value: localScaling
onPressedChanged: ScalingService.setScreenScale(modelData, value)
Layout.fillWidth: true
Layout.minimumWidth: 150 * scaling
}
NIconButton {
icon: "refresh"
tooltipText: "Reset scaling"
onClicked: ScalingService.setScreenScale(modelData, 1.0)
}
}
}
}
}
Item {
Layout.fillHeight: true
}
}
}
}

View File

@@ -8,209 +8,199 @@ import qs.Widgets
ColumnLayout {
id: root
spacing: 0
ScrollView {
id: scrollView
// Profile section
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: true
padding: Style.marginM * scaling
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
spacing: Style.marginL * scaling
// Avatar preview
NImageCircled {
width: 128 * scaling
height: 128 * scaling
imagePath: Settings.data.general.avatarImage
fallbackIcon: "person"
borderColor: Color.mPrimary
borderWidth: Math.max(1, Style.borderM * scaling)
}
NTextInput {
label: "Profile Picture"
description: "Your profile picture that appears throughout the interface."
text: Settings.data.general.avatarImage
placeholderText: "/home/user/.face"
onEditingFinished: {
Settings.data.general.avatarImage = text
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// User Interface
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NText {
text: "User Interface"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
Layout.bottomMargin: Style.marginS * scaling
}
NToggle {
label: "Show Corners"
description: "Display rounded corners on the edge of the screen."
checked: Settings.data.general.showScreenCorners
onToggled: checked => {
Settings.data.general.showScreenCorners = checked
}
}
NToggle {
label: "Dim Desktop"
description: "Dim the desktop when panels or menus are open."
checked: Settings.data.general.dimDesktop
onToggled: checked => {
Settings.data.general.dimDesktop = checked
}
}
NToggle {
label: "Auto-hide Dock"
description: "Automatically hide the dock when not in use."
checked: Settings.data.dock.autoHide
onToggled: checked => {
Settings.data.dock.autoHide = checked
}
}
ColumnLayout {
width: scrollView.availableWidth
spacing: 0
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
Item {
Layout.fillWidth: true
Layout.preferredHeight: 0
NLabel {
label: "Border radius"
description: "Adjust the rounded border of all UI elements."
}
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NText {
text: "General Settings"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
// Profile section
ColumnLayout {
spacing: Style.marginS * scaling
RowLayout {
NSlider {
Layout.fillWidth: true
Layout.topMargin: Style.marginS * scaling
RowLayout {
Layout.fillWidth: true
spacing: Style.marginL * scaling
// Avatar preview
NImageCircled {
width: 64 * scaling
height: 64 * scaling
imagePath: Settings.data.general.avatarImage
fallbackIcon: "person"
borderColor: Color.mPrimary
borderWidth: Math.max(1, Style.borderM * scaling)
}
NTextInput {
label: "Profile Picture"
description: "Your profile picture displayed in various places throughout the shell."
text: Settings.data.general.avatarImage
placeholderText: "/home/user/.face"
Layout.fillWidth: true
onEditingFinished: {
Settings.data.general.avatarImage = text
}
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NText {
text: "User Interface"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.bottomMargin: Style.marginS * scaling
}
NToggle {
label: "Show Corners"
description: "Display rounded corners on the edge of the screen."
checked: Settings.data.general.showScreenCorners
onToggled: checked => {
Settings.data.general.showScreenCorners = checked
}
}
NToggle {
label: "Dim Desktop"
description: "Dim the desktop when panels or menus are open."
checked: Settings.data.general.dimDesktop
onToggled: checked => {
Settings.data.general.dimDesktop = checked
}
}
NToggle {
label: "Auto-hide Dock"
description: "Automatically hide the dock when not in use."
checked: Settings.data.dock.autoHide
onToggled: checked => {
Settings.data.dock.autoHide = checked
}
}
ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
NText {
text: "Border radius"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
NText {
text: "Adjust the rounded border of all UI elements"
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
RowLayout {
NSlider {
Layout.fillWidth: true
from: 0
to: 1
stepSize: 0.01
value: Settings.data.general.radiusRatio
onMoved: Settings.data.general.radiusRatio = value
cutoutColor: Color.mSurface
}
NText {
text: Math.floor(Settings.data.general.radiusRatio * 100) + "%"
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: Style.marginS * scaling
color: Color.mOnSurface
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginL * scaling
from: 0
to: 1
stepSize: 0.01
value: Settings.data.general.radiusRatio
onMoved: Settings.data.general.radiusRatio = value
cutoutColor: Color.mSurface
}
NText {
text: "Fonts"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
text: Math.floor(Settings.data.general.radiusRatio * 100) + "%"
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: Style.marginS * scaling
color: Color.mOnSurface
Layout.bottomMargin: Style.marginS * scaling
}
}
}
// Animation Speed
ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
NLabel {
label: "Animation Speed"
description: "Adjust global animation speed."
}
RowLayout {
NSlider {
Layout.fillWidth: true
from: 0.1
to: 2.0
stepSize: 0.01
value: Settings.data.general.animationSpeed
onMoved: Settings.data.general.animationSpeed = value
cutoutColor: Color.mSurface
}
// Font configuration section
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
NTextInput {
label: "Default Font"
description: "Main font used throughout the interface."
text: Settings.data.ui.fontDefault
placeholderText: "Roboto"
Layout.fillWidth: true
onEditingFinished: {
Settings.data.ui.fontDefault = text
}
}
NTextInput {
label: "Fixed Width Font"
description: "Monospace font used for terminal and code display."
text: Settings.data.ui.fontFixed
placeholderText: "DejaVu Sans Mono"
Layout.fillWidth: true
onEditingFinished: {
Settings.data.ui.fontFixed = text
}
}
NTextInput {
label: "Billboard Font"
description: "Large font used for clocks and prominent displays."
text: Settings.data.ui.fontBillboard
placeholderText: "Inter"
Layout.fillWidth: true
onEditingFinished: {
Settings.data.ui.fontBillboard = text
}
}
NText {
text: Math.round(Settings.data.general.animationSpeed * 100) + "%"
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: Style.marginS * scaling
color: Color.mOnSurface
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// Fonts
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NText {
text: "Fonts"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
Layout.bottomMargin: Style.marginS * scaling
}
// Font configuration section
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NTextInput {
label: "Default Font"
description: "Main font used throughout the interface."
text: Settings.data.ui.fontDefault
placeholderText: "Roboto"
Layout.fillWidth: true
onEditingFinished: {
Settings.data.ui.fontDefault = text
}
}
NTextInput {
label: "Fixed Width Font"
description: "Monospace font used for terminal and code display."
text: Settings.data.ui.fontFixed
placeholderText: "DejaVu Sans Mono"
Layout.fillWidth: true
onEditingFinished: {
Settings.data.ui.fontFixed = text
}
}
NTextInput {
label: "Billboard Font"
description: "Large font used for clocks and prominent displays."
text: Settings.data.ui.fontBillboard
placeholderText: "Inter"
Layout.fillWidth: true
onEditingFinished: {
Settings.data.ui.fontBillboard = text
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
}

View File

@@ -8,94 +8,103 @@ import qs.Widgets
ColumnLayout {
id: root
spacing: 0
ColumnLayout {
spacing: Style.marginL * scaling
ScrollView {
id: scrollView
NComboBox {
id: launcherPosition
label: "Position"
description: "Choose where the Launcher panel appears."
Layout.fillWidth: true
model: ListModel {
ListElement {
key: "center"
name: "Center (default)"
}
ListElement {
key: "top_left"
name: "Top Left"
}
ListElement {
key: "top_right"
name: "Top Right"
}
ListElement {
key: "bottom_left"
name: "Bottom Left"
}
ListElement {
key: "bottom_right"
name: "Bottom Right"
}
ListElement {
key: "bottom_center"
name: "Bottom Center"
}
ListElement {
key: "top_center"
name: "Top Center"
}
}
currentKey: Settings.data.appLauncher.position
onSelected: function (key) {
Settings.data.appLauncher.position = key
}
}
Layout.fillWidth: true
Layout.fillHeight: true
padding: Style.marginM * scaling
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
NToggle {
label: "Enable Clipboard History"
description: "Show clipboard history in the launcher."
checked: Settings.data.appLauncher.enableClipboardHistory
onToggled: checked => {
Settings.data.appLauncher.enableClipboardHistory = checked
}
}
ColumnLayout {
width: scrollView.availableWidth
spacing: 0
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
Item {
Layout.fillWidth: true
Layout.preferredHeight: 0
NText {
text: "Background Opacity"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
ColumnLayout {
spacing: Style.marginL * scaling
NText {
text: "Adjust the background opacity of the launcher."
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
NText {
text: "Launcher"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
NToggle {
label: "Enable Clipboard History"
description: "Show clipboard history in the Launcher (command >clip)."
checked: Settings.data.appLauncher.enableClipboardHistory
onToggled: checked => {
Settings.data.appLauncher.enableClipboardHistory = checked
}
}
NDivider {
RowLayout {
NSlider {
id: launcherBgOpacity
Layout.fillWidth: true
Layout.topMargin: Style.marginL * scaling
Layout.bottomMargin: Style.marginS * scaling
from: 0.0
to: 1.0
stepSize: 0.01
value: Settings.data.appLauncher.backgroundOpacity
onMoved: Settings.data.appLauncher.backgroundOpacity = value
cutoutColor: Color.mSurface
}
NText {
text: "Launcher Position"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
text: Math.floor(Settings.data.appLauncher.backgroundOpacity * 100) + "%"
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: Style.marginS * scaling
color: Color.mOnSurface
Layout.bottomMargin: Style.marginS * scaling
}
NComboBox {
id: launcherPosition
label: "Position"
description: "Choose where the Launcher panel appears."
Layout.fillWidth: true
model: ListModel {
ListElement {
key: "center"
name: "Center (default)"
}
ListElement {
key: "top_left"
name: "Top Left"
}
ListElement {
key: "top_right"
name: "Top Right"
}
ListElement {
key: "bottom_left"
name: "Bottom Left"
}
ListElement {
key: "bottom_right"
name: "Bottom Right"
}
}
currentKey: Settings.data.appLauncher.position
onSelected: function (key) {
Settings.data.appLauncher.position = key
}
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
}

View File

@@ -9,68 +9,41 @@ import qs.Widgets
ColumnLayout {
id: root
spacing: 0
spacing: Style.marginL * scaling
ScrollView {
id: scrollView
NToggle {
label: "WiFi Enabled"
description: "Enable WiFi connectivity."
checked: Settings.data.network.wifiEnabled
onToggled: checked => {
Settings.data.network.wifiEnabled = checked
NetworkService.setWifiEnabled(checked)
if (checked) {
ToastService.showNotice("WiFi", "Enabled")
} else {
ToastService.showNotice("WiFi", "Disabled")
}
}
}
NToggle {
label: "Bluetooth Enabled"
description: "Enable Bluetooth connectivity."
checked: Settings.data.network.bluetoothEnabled
onToggled: checked => {
Settings.data.network.bluetoothEnabled = checked
BluetoothService.setBluetoothEnabled(checked)
if (checked) {
ToastService.showNotice("Bluetooth", "Enabled")
} else {
ToastService.showNotice("Bluetooth", "Disabled")
}
}
}
NDivider {
Layout.fillWidth: true
Layout.fillHeight: true
padding: Style.marginM * scaling
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ColumnLayout {
width: scrollView.availableWidth
spacing: 0
Item {
Layout.fillWidth: true
Layout.preferredHeight: 0
}
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NText {
text: "Interfaces"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
NToggle {
label: "WiFi Enabled"
description: "Enable WiFi connectivity."
checked: Settings.data.network.wifiEnabled
onToggled: checked => {
Settings.data.network.wifiEnabled = checked
NetworkService.setWifiEnabled(checked)
if (checked) {
ToastService.showNotice("WiFi", "Enabled")
} else {
ToastService.showNotice("WiFi", "Disabled")
}
}
}
NToggle {
label: "Bluetooth Enabled"
description: "Enable Bluetooth connectivity."
checked: Settings.data.network.bluetoothEnabled
onToggled: checked => {
Settings.data.network.bluetoothEnabled = checked
BluetoothService.setBluetoothEnabled(checked)
if (checked) {
ToastService.showNotice("Bluetooth", "Enabled")
} else {
ToastService.showNotice("Bluetooth", "Disabled")
}
}
}
}
}
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
}

View File

@@ -8,295 +8,269 @@ import qs.Widgets
ColumnLayout {
id: root
spacing: 0
ScrollView {
id: scrollView
spacing: Style.marginL * scaling
// Output Directory
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
Layout.fillHeight: true
padding: Style.marginM * scaling
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
Layout.topMargin: Style.marginS * scaling
ColumnLayout {
width: scrollView.availableWidth
spacing: 0
Item {
Layout.fillWidth: true
Layout.preferredHeight: 0
NTextInput {
label: "Output Directory"
description: "Directory where screen recordings will be saved."
placeholderText: "/home/xxx/Videos"
text: Settings.data.screenRecorder.directory
onEditingFinished: {
Settings.data.screenRecorder.directory = text
}
ColumnLayout {
spacing: Style.marginXS * scaling
Layout.fillWidth: true
Layout.maximumWidth: 420 * scaling
}
NText {
text: "Recordings"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.bottomMargin: Style.marginS * scaling
}
// Output Directory
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginS * scaling
NTextInput {
label: "Output Directory"
description: "Directory where screen recordings will be saved."
placeholderText: "/home/xxx/Videos"
text: Settings.data.screenRecorder.directory
onEditingFinished: {
Settings.data.screenRecorder.directory = text
}
}
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginM * scaling
// Show Cursor
NToggle {
label: "Show Cursor"
description: "Record mouse cursor in the video."
checked: Settings.data.screenRecorder.showCursor
onToggled: checked => {
Settings.data.screenRecorder.showCursor = checked
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL * 2 * scaling
Layout.bottomMargin: Style.marginL * scaling
}
// Video Settings
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NText {
text: "Video Settings"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.bottomMargin: Style.marginS * scaling
}
// Source
NComboBox {
label: "Video Source"
description: "We recommend using portal, if you get artifacts try screen."
model: ListModel {
ListElement {
key: "portal"
name: "Portal"
}
ListElement {
key: "screen"
name: "Screen"
}
}
currentKey: Settings.data.screenRecorder.videoSource
onSelected: key => {
Settings.data.screenRecorder.videoSource = key
}
}
// Frame Rate
NComboBox {
label: "Frame Rate"
description: "Target frame rate for screen recordings. (default: 60)"
model: ListModel {
ListElement {
key: "30"
name: "30 FPS"
}
ListElement {
key: "60"
name: "60 FPS"
}
ListElement {
key: "100"
name: "100 FPS"
}
ListElement {
key: "120"
name: "120 FPS"
}
ListElement {
key: "144"
name: "144 FPS"
}
ListElement {
key: "165"
name: "165 FPS"
}
ListElement {
key: "240"
name: "240 FPS"
}
}
currentKey: Settings.data.screenRecorder.frameRate
onSelected: key => {
Settings.data.screenRecorder.frameRate = key
}
}
// Video Quality
NComboBox {
label: "Video Quality"
description: "Higher quality results in larger file sizes."
model: ListModel {
ListElement {
key: "medium"
name: "Medium"
}
ListElement {
key: "high"
name: "High"
}
ListElement {
key: "very_high"
name: "Very High"
}
ListElement {
key: "ultra"
name: "Ultra"
}
}
currentKey: Settings.data.screenRecorder.quality
onSelected: key => {
Settings.data.screenRecorder.quality = key
}
}
// Video Codec
NComboBox {
label: "Video Codec"
description: "Different codecs offer different compression and compatibility."
model: ListModel {
ListElement {
key: "h264"
name: "H264"
}
ListElement {
key: "hevc"
name: "HEVC"
}
ListElement {
key: "av1"
name: "AV1"
}
ListElement {
key: "vp8"
name: "VP8"
}
ListElement {
key: "vp9"
name: "VP9"
}
}
currentKey: Settings.data.screenRecorder.videoCodec
onSelected: key => {
Settings.data.screenRecorder.videoCodec = key
}
}
// Color Range
NComboBox {
label: "Color Range"
description: "Limited is recommended for better compatibility."
model: ListModel {
ListElement {
key: "limited"
name: "Limited"
}
ListElement {
key: "full"
name: "Full"
}
}
currentKey: Settings.data.screenRecorder.colorRange
onSelected: key => {
Settings.data.screenRecorder.colorRange = key
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL * 2 * scaling
Layout.bottomMargin: Style.marginL * scaling
}
// Audio Settings
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NText {
text: "Audio Settings"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.bottomMargin: Style.marginS * scaling
}
// Audio Source
NComboBox {
label: "Audio Source"
description: "Audio source to capture during recording."
model: ListModel {
ListElement {
key: "default_output"
name: "System Output"
}
ListElement {
key: "default_input"
name: "Microphone Input"
}
ListElement {
key: "both"
name: "System Output + Microphone Input"
}
}
currentKey: Settings.data.screenRecorder.audioSource
onSelected: key => {
Settings.data.screenRecorder.audioSource = key
}
}
// Audio Codec
NComboBox {
label: "Audio Codec"
description: "Opus is recommended for best performance and smallest audio size."
model: ListModel {
ListElement {
key: "opus"
name: "Opus"
}
ListElement {
key: "aac"
name: "AAC"
}
}
currentKey: Settings.data.screenRecorder.audioCodec
onSelected: key => {
Settings.data.screenRecorder.audioCodec = key
}
}
}
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginM * scaling
// Show Cursor
NToggle {
label: "Show Cursor"
description: "Record mouse cursor in the video."
checked: Settings.data.screenRecorder.showCursor
onToggled: checked => {
Settings.data.screenRecorder.showCursor = checked
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// Video Settings
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NText {
text: "Video Settings"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
Layout.bottomMargin: Style.marginS * scaling
}
// Source
NComboBox {
label: "Video Source"
description: "We recommend using portal, if you get artifacts try screen."
model: ListModel {
ListElement {
key: "portal"
name: "Portal"
}
ListElement {
key: "screen"
name: "Screen"
}
}
currentKey: Settings.data.screenRecorder.videoSource
onSelected: key => {
Settings.data.screenRecorder.videoSource = key
}
}
// Frame Rate
NComboBox {
label: "Frame Rate"
description: "Target frame rate for screen recordings."
model: ListModel {
ListElement {
key: "30"
name: "30 FPS"
}
ListElement {
key: "60"
name: "60 FPS"
}
ListElement {
key: "100"
name: "100 FPS"
}
ListElement {
key: "120"
name: "120 FPS"
}
ListElement {
key: "144"
name: "144 FPS"
}
ListElement {
key: "165"
name: "165 FPS"
}
ListElement {
key: "240"
name: "240 FPS"
}
}
currentKey: Settings.data.screenRecorder.frameRate
onSelected: key => {
Settings.data.screenRecorder.frameRate = key
}
}
// Video Quality
NComboBox {
label: "Video Quality"
description: "Higher quality results in larger file sizes."
model: ListModel {
ListElement {
key: "medium"
name: "Medium"
}
ListElement {
key: "high"
name: "High"
}
ListElement {
key: "very_high"
name: "Very High"
}
ListElement {
key: "ultra"
name: "Ultra"
}
}
currentKey: Settings.data.screenRecorder.quality
onSelected: key => {
Settings.data.screenRecorder.quality = key
}
}
// Video Codec
NComboBox {
label: "Video Codec"
description: "Different codecs offer different compression and compatibility."
model: ListModel {
ListElement {
key: "h264"
name: "H264"
}
ListElement {
key: "hevc"
name: "HEVC"
}
ListElement {
key: "av1"
name: "AV1"
}
ListElement {
key: "vp8"
name: "VP8"
}
ListElement {
key: "vp9"
name: "VP9"
}
}
currentKey: Settings.data.screenRecorder.videoCodec
onSelected: key => {
Settings.data.screenRecorder.videoCodec = key
}
}
// Color Range
NComboBox {
label: "Color Range"
description: "Limited is recommended for better compatibility."
model: ListModel {
ListElement {
key: "limited"
name: "Limited"
}
ListElement {
key: "full"
name: "Full"
}
}
currentKey: Settings.data.screenRecorder.colorRange
onSelected: key => {
Settings.data.screenRecorder.colorRange = key
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL * 2 * scaling
Layout.bottomMargin: Style.marginL * scaling
}
// Audio Settings
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NText {
text: "Audio Settings"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
Layout.bottomMargin: Style.marginS * scaling
}
// Audio Source
NComboBox {
label: "Audio Source"
description: "Audio source to capture during recording."
model: ListModel {
ListElement {
key: "default_output"
name: "System Output"
}
ListElement {
key: "default_input"
name: "Microphone Input"
}
ListElement {
key: "both"
name: "System Output + Microphone Input"
}
}
currentKey: Settings.data.screenRecorder.audioSource
onSelected: key => {
Settings.data.screenRecorder.audioSource = key
}
}
// Audio Codec
NComboBox {
label: "Audio Codec"
description: "Opus is recommended for best performance and smallest audio size."
model: ListModel {
ListElement {
key: "opus"
name: "Opus"
}
ListElement {
key: "aac"
name: "AAC"
}
}
currentKey: Settings.data.screenRecorder.audioCodec
onSelected: key => {
Settings.data.screenRecorder.audioCodec = key
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
}

View File

@@ -8,134 +8,116 @@ import qs.Widgets
ColumnLayout {
id: root
spacing: 0
ScrollView {
id: scrollView
// Location section
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: true
padding: Style.marginM * scaling
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
spacing: Style.marginL * scaling
ColumnLayout {
width: scrollView.availableWidth
spacing: 0
Item {
Layout.fillWidth: true
Layout.preferredHeight: 0
}
ColumnLayout {
spacing: Style.marginXS * scaling
Layout.fillWidth: true
NText {
text: "Location"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.bottomMargin: Style.marginS * scaling
}
// Location section
ColumnLayout {
spacing: Style.marginM * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginS * scaling
NTextInput {
label: "Location name"
description: "Choose a known location near you."
text: Settings.data.location.name
placeholderText: "Enter the location name"
Layout.fillWidth: true
onEditingFinished: {
Settings.data.location.name = text.trim()
LocationService.resetWeather()
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL * 2 * scaling
Layout.bottomMargin: Style.marginL * scaling
}
// Time section
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NText {
text: "Time Format"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.bottomMargin: Style.marginS * scaling
}
NToggle {
label: "Use 12-Hour Clock"
description: "Display time in 12-hour format (AM/PM) instead of 24-hour."
checked: Settings.data.location.use12HourClock
onToggled: checked => {
Settings.data.location.use12HourClock = checked
}
}
NToggle {
label: "Reverse Day/Month"
description: "Display date as DD/MM instead of MM/DD."
checked: Settings.data.location.reverseDayMonth
onToggled: checked => {
Settings.data.location.reverseDayMonth = checked
}
}
NToggle {
label: "Show Date with Clock"
description: "Display date alongside time (e.g., 18:12 - Sat, 23 Aug)."
checked: Settings.data.location.showDateWithClock
onToggled: checked => {
Settings.data.location.showDateWithClock = checked
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL * 2 * scaling
Layout.bottomMargin: Style.marginL * scaling
}
// Weather section
ColumnLayout {
spacing: Style.marginM * scaling
Layout.fillWidth: true
NText {
text: "Weather"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.bottomMargin: Style.marginS * scaling
}
NToggle {
label: "Use Fahrenheit"
description: "Display temperature in Fahrenheit instead of Celsius."
checked: Settings.data.location.useFahrenheit
onToggled: checked => {
Settings.data.location.useFahrenheit = checked
}
}
NTextInput {
label: "Location name"
description: "Choose a known location near you."
text: Settings.data.location.name
placeholderText: "Enter the location name"
onEditingFinished: {
// Verify the location has really changed to avoid extra resets
var newLocation = text.trim()
if (newLocation != Settings.data.location.name) {
Settings.data.location.name = newLocation
LocationService.resetWeather()
}
}
Layout.maximumWidth: 420 * scaling
}
NText {
visible: LocationService.coordinatesReady
text: `${LocationService.stableName} (${LocationService.displayCoordinates})`
font.pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignRight
Layout.alignment: Qt.AlignBottom
Layout.bottomMargin: 12 * scaling
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// Time section
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NText {
text: "Time Format"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
}
NToggle {
label: "Use 12-Hour Clock"
description: "Display time in 12-hour format (AM/PM) instead of 24-hour."
checked: Settings.data.location.use12HourClock
onToggled: checked => {
Settings.data.location.use12HourClock = checked
}
}
NToggle {
label: "Reverse Day/Month"
description: "Display date as DD/MM instead of MM/DD."
checked: Settings.data.location.reverseDayMonth
onToggled: checked => {
Settings.data.location.reverseDayMonth = checked
}
}
NToggle {
label: "Show Date with Clock"
description: "Display date alongside time (e.g., 18:12 - Sat, 23 Aug)."
checked: Settings.data.location.showDateWithClock
onToggled: checked => {
Settings.data.location.showDateWithClock = checked
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// Weather section
ColumnLayout {
spacing: Style.marginM * scaling
Layout.fillWidth: true
NText {
text: "Weather"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
}
NToggle {
label: "Use Fahrenheit"
description: "Display temperature in Fahrenheit instead of Celsius."
checked: Settings.data.location.useFahrenheit
onToggled: checked => {
Settings.data.location.useFahrenheit = checked
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
}

View File

@@ -6,241 +6,275 @@ import qs.Commons
import qs.Services
import qs.Widgets
Item {
readonly property real scaling: ScalingService.scale(screen)
readonly property string tabIcon: "photo_library"
readonly property string tabLabel: "Wallpaper Selector"
readonly property int tabIndex: 7
Layout.fillWidth: true
Layout.fillHeight: true
ColumnLayout {
id: root
width: parent.width
ScrollView {
anchors.fill: parent
clip: true
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ScrollBar.horizontal.policy: ScrollBar.AsNeeded
contentWidth: parent.width
spacing: Style.marginL * scaling
property list<string> wallpapersList: []
property string currentWallpaper: ""
Component.onCompleted: {
wallpapersList = screen ? WallpaperService.getWallpapersList(screen.name) : []
currentWallpaper = screen ? WallpaperService.getWallpaper(screen.name) : ""
}
Connections {
target: WallpaperService
function onWallpaperChanged(screenName, path) {
if (screenName === screen.name) {
currentWallpaper = WallpaperService.getWallpaper(screen.name)
}
}
function onWallpaperDirectoryChanged(screenName, directory) {
if (screenName === screen.name) {
wallpapersList = WallpaperService.getWallpapersList(screen.name)
currentWallpaper = WallpaperService.getWallpaper(screen.name)
}
}
function onWallpaperListChanged(screenName, count) {
if (screenName === screen.name) {
wallpapersList = WallpaperService.getWallpapersList(screen.name)
currentWallpaper = WallpaperService.getWallpaper(screen.name)
}
}
}
// Current wallpaper display
NText {
text: "Current Wallpaper"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 140 * scaling
radius: Style.radiusM * scaling
color: Color.transparent
NImageRounded {
anchors.fill: parent
anchors.margins: Style.marginXS * scaling
imagePath: currentWallpaper
fallbackIcon: "image"
imageRadius: Style.radiusM * scaling
borderColor: Color.mSecondary
borderWidth: Style.borderL * 2 * scaling
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// Wallpaper selector
RowLayout {
Layout.fillWidth: true
ColumnLayout {
width: parent.width
ColumnLayout {
spacing: Style.marginL * scaling
Layout.margins: Style.marginL * scaling
Layout.fillWidth: true
// Wallpaper grid
NText {
text: "Wallpaper Selector"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
}
NText {
text: "Click on a wallpaper to set it as your current wallpaper."
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
// Current wallpaper display
NText {
text: "Current Wallpaper"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
NIconButton {
icon: "refresh"
tooltipText: "Refresh wallpaper list"
onClicked: {
WallpaperService.refreshWallpapersList()
}
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
}
}
NToggle {
label: "Apply to all monitors"
description: "Apply selected wallpaper to all monitors at once."
checked: Settings.data.wallpaper.setWallpaperOnAllMonitors
onToggled: checked => Settings.data.wallpaper.setWallpaperOnAllMonitors = checked
visible: (wallpapersList.length > 0)
}
// Wallpaper grid container
Item {
visible: !WallpaperService.scanning
Layout.fillWidth: true
Layout.preferredHeight: {
return Math.ceil(wallpapersList.length / wallpaperGridView.columns) * wallpaperGridView.cellHeight
}
GridView {
id: wallpaperGridView
anchors.fill: parent
model: wallpapersList
interactive: false
clip: true
property int columns: 4
property int itemSize: Math.floor((width - leftMargin - rightMargin - (4 * Style.marginS * scaling)) / columns)
cellWidth: Math.floor((width - leftMargin - rightMargin) / columns)
cellHeight: Math.floor(itemSize * 0.67) + Style.marginS * scaling
leftMargin: Style.marginS * scaling
rightMargin: Style.marginS * scaling
topMargin: Style.marginS * scaling
bottomMargin: Style.marginS * scaling
delegate: Rectangle {
id: wallpaperItem
property string wallpaperPath: modelData
property bool isSelected: screen ? (wallpaperPath === currentWallpaper) : false
width: wallpaperGridView.itemSize
height: Math.round(wallpaperGridView.itemSize * 0.67)
color: Color.transparent
// NImageCached relies on the image being visible to work properly.
// MultiEffect relies on the image being invisible to apply effects.
// That's why we don't have rounded corners here, as we don't want to bring back qt5compat.
NImageCached {
id: img
imagePath: wallpaperPath
anchors.fill: parent
}
// Borders on top
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 140 * scaling
radius: Style.radiusM * scaling
color: Color.mPrimary
anchors.fill: parent
color: Color.transparent
border.color: isSelected ? Color.mSecondary : Color.mSurface
border.width: Math.max(1, Style.borderL * 1.5 * scaling)
}
NImageRounded {
id: currentWallpaperImage
anchors.fill: parent
anchors.margins: Style.marginXS * scaling
imagePath: WallpaperService.currentWallpaper
fallbackIcon: "image"
imageRadius: Style.radiusM * scaling
// Selection tick-mark
Rectangle {
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: Style.marginS * scaling
width: 28 * scaling
height: 28 * scaling
radius: width / 2
color: Color.mSecondary
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
visible: isSelected
NIcon {
text: "check"
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSecondary
anchors.centerIn: parent
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL * scaling
Layout.bottomMargin: Style.marginL * scaling
}
// Hover effect
Rectangle {
anchors.fill: parent
color: Color.mSurface
opacity: (mouseArea.containsMouse || isSelected) ? 0 : 0.3
radius: parent.radius
RowLayout {
Layout.fillWidth: true
ColumnLayout {
Layout.fillWidth: true
// Wallpaper grid
NText {
text: "Wallpaper Selector"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
NText {
text: "Click on a wallpaper to set it as your current wallpaper."
color: Color.mOnSurface
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
NText {
text: Settings.data.wallpaper.swww.enabled ? "Wallpapers will change with " + Settings.data.wallpaper.swww.transitionType
+ " transition." : "Wallpapers will change instantly."
color: Color.mOnSurface
font.pointSize: Style.fontSizeXS * scaling
visible: Settings.data.wallpaper.swww.enabled
}
}
NIconButton {
icon: "refresh"
tooltipText: "Refresh wallpaper list"
onClicked: {
WallpaperService.listWallpapers()
}
Layout.alignment: Qt.AlignTop | Qt.AlignRight
}
}
// Wallpaper grid container
Item {
Layout.fillWidth: true
Layout.preferredHeight: {
return Math.ceil(
WallpaperService.wallpaperList.length / wallpaperGridView.columns) * wallpaperGridView.cellHeight
}
GridView {
id: wallpaperGridView
anchors.fill: parent
clip: true
model: WallpaperService.wallpaperList
boundsBehavior: Flickable.StopAtBounds
flickableDirection: Flickable.AutoFlickDirection
interactive: false
property int columns: 5
property int itemSize: Math.floor(
(width - leftMargin - rightMargin - (4 * Style.marginS * scaling)) / columns)
cellWidth: Math.floor((width - leftMargin - rightMargin) / columns)
cellHeight: Math.floor(itemSize * 0.67) + Style.marginS * scaling
leftMargin: Style.marginS * scaling
rightMargin: Style.marginS * scaling
topMargin: Style.marginS * scaling
bottomMargin: Style.marginS * scaling
delegate: Rectangle {
id: wallpaperItem
property string wallpaperPath: modelData
property bool isSelected: wallpaperPath === WallpaperService.currentWallpaper
width: wallpaperGridView.itemSize
height: Math.floor(wallpaperGridView.itemSize * 0.67)
color: Color.transparent
// NImageCached relies on the image being visible to work properly.
// MultiEffect relies on the image being invisible to apply effects.
// That's why we don't have rounded corners here, as we don't want to bring back qt5compat.
NImageCached {
id: img
imagePath: wallpaperPath
anchors.fill: parent
}
// Borders on top
Rectangle {
anchors.fill: parent
color: Color.transparent
border.color: isSelected ? Color.mPrimary : Color.mSurface
border.width: Math.max(1, Style.borderL * scaling)
}
// Selection tick-mark
Rectangle {
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: Style.marginXS * scaling
width: 28 * scaling
height: 28 * scaling
radius: width / 2
color: Color.mPrimary
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
visible: isSelected
NIcon {
text: "check"
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
color: Color.mOnPrimary
anchors.centerIn: parent
}
}
// Hover effect
Rectangle {
anchors.fill: parent
color: Color.mOnSurface
opacity: mouseArea.containsMouse ? 0.1 : 0
radius: parent.radius
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton
hoverEnabled: true
onClicked: {
WallpaperService.changeWallpaper(wallpaperPath)
}
}
}
}
// Empty state
Rectangle {
anchors.fill: parent
color: Color.mSurface
radius: Style.radiusM * scaling
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
visible: WallpaperService.wallpaperList.length === 0 && !WallpaperService.scanning
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginM * scaling
NIcon {
text: "folder_open"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "No wallpapers found"
color: Color.mOnSurface
font.weight: Style.fontWeightBold
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Make sure your wallpaper directory is configured and contains image files."
color: Color.mOnSurface
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
Layout.preferredWidth: Style.sliderWidth * 1.5 * scaling
}
MouseArea {
id: mouseArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton
hoverEnabled: true
onPressed: {
if (Settings.data.wallpaper.setWallpaperOnAllMonitors) {
WallpaperService.changeWallpaper(undefined, wallpaperPath)
} else if (screen) {
WallpaperService.changeWallpaper(screen.name, wallpaperPath)
}
}
}
}
}
}
// Empty state
Rectangle {
color: Color.mSurface
radius: Style.radiusM * scaling
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
visible: wallpapersList.length === 0 || WallpaperService.scanning
Layout.fillWidth: true
Layout.preferredHeight: 130 * scaling
ColumnLayout {
anchors.fill: parent
visible: WallpaperService.scanning
NBusyIndicator {
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
}
}
ColumnLayout {
anchors.fill: parent
visible: wallpapersList.length === 0 && !WallpaperService.scanning
Item {
Layout.fillHeight: true
}
NIcon {
text: "folder_open"
font.pointSize: Style.fontSizeXL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "No wallpaper found."
color: Color.mOnSurface
font.weight: Style.fontWeightBold
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Make sure your wallpaper directory is configured and contains image files."
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.alignment: Qt.AlignHCenter
}
Item {
Layout.fillHeight: true
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
}

View File

@@ -1,6 +1,7 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services
@@ -9,333 +10,57 @@ import qs.Widgets
ColumnLayout {
id: root
spacing: 0
ScrollView {
id: scrollView
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
Layout.fillHeight: true
padding: Style.marginM * scaling
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ColumnLayout {
width: scrollView.availableWidth
spacing: 0
Item {
Layout.fillWidth: true
Layout.preferredHeight: 0
NTextInput {
label: "Wallpaper Directory"
description: "Path to your common wallpaper directory."
text: Settings.data.wallpaper.directory
onEditingFinished: {
Settings.data.wallpaper.directory = text
}
Layout.maximumWidth: 420 * scaling
}
// Monitor-specific directories
NToggle {
label: "Monitor-specific directories"
description: "Enable multi-monitor wallpaper directory management."
checked: Settings.data.wallpaper.enableMultiMonitorDirectories
onToggled: checked => Settings.data.wallpaper.enableMultiMonitorDirectories = checked
}
NBox {
visible: Settings.data.wallpaper.enableMultiMonitorDirectories
Layout.fillWidth: true
Layout.minimumWidth: 550 * scaling
radius: Style.radiusM * scaling
color: Color.mSurfaceVariant
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
implicitHeight: contentCol.implicitHeight + Style.marginXL * 2 * scaling
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NText {
text: "Directory"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.bottomMargin: Style.marginS * scaling
}
// Wallpaper Settings Category
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginS * scaling
// Wallpaper Folder
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
NTextInput {
label: "Wallpaper Directory"
description: "Path to your wallpaper directory."
text: Settings.data.wallpaper.directory
Layout.fillWidth: true
onEditingFinished: {
Settings.data.wallpaper.directory = text
}
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL * 2 * scaling
Layout.bottomMargin: Style.marginL * scaling
}
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NText {
text: "Automation"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.bottomMargin: Style.marginS * scaling
}
// Random Wallpaper
NToggle {
label: "Random Wallpaper"
description: "Automatically select random wallpapers from the folder."
checked: Settings.data.wallpaper.isRandom
onToggled: checked => {
Settings.data.wallpaper.isRandom = checked
}
}
// Interval
ColumnLayout {
RowLayout {
NLabel {
label: "Wallpaper Interval"
description: "How often to change wallpapers automatically (in seconds)."
Layout.fillWidth: true
}
id: contentCol
anchors.fill: parent
anchors.margins: Style.marginXL * scaling
spacing: Style.marginM * scaling
Repeater {
model: Quickshell.screens || []
delegate: RowLayout {
NText {
text: sliderWpInterval.value + " seconds"
Layout.alignment: Qt.AlignBottom | Qt.AlignRight
text: (modelData.name || "Unknown")
color: Color.mSecondary
font.weight: Style.fontWeightBold
Layout.preferredWidth: 90 * scaling
}
}
NSlider {
id: sliderWpInterval
Layout.fillWidth: true
from: 10
to: 900
stepSize: 10
value: Settings.data.wallpaper.randomInterval
onPressedChanged: Settings.data.wallpaper.randomInterval = Math.round(value)
cutoutColor: Color.mSurface
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL * 2 * scaling
Layout.bottomMargin: Style.marginL * scaling
}
// -------------------------------
// SWWW
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NText {
text: "SWWW"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.bottomMargin: Style.marginS * scaling
}
// Use SWWW
NToggle {
label: "Use SWWW"
description: "Use SWWW daemon for advanced wallpaper management."
checked: Settings.data.wallpaper.swww.enabled
onToggled: checked => {
if (checked) {
// Check if swww is installed
swwwCheck.running = true
} else {
Settings.data.wallpaper.swww.enabled = false
ToastService.showNotice("SWWW", "Disabled")
}
}
}
// SWWW Settings (only visible when useSWWW is enabled)
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginS * scaling
visible: Settings.data.wallpaper.swww.enabled
// Resize Mode
NComboBox {
label: "Resize Mode"
description: "How SWWW should resize wallpapers to fit the screen."
model: ListModel {
ListElement {
key: "no"
name: "No"
}
ListElement {
key: "crop"
name: "Crop"
}
ListElement {
key: "fit"
name: "Fit"
}
ListElement {
key: "stretch"
name: "Stretch"
}
}
currentKey: Settings.data.wallpaper.swww.resizeMethod
onSelected: key => {
Settings.data.wallpaper.swww.resizeMethod = key
}
}
// Transition Type
NComboBox {
label: "Transition Type"
description: "Animation type when switching between wallpapers."
model: ListModel {
ListElement {
key: "none"
name: "None"
}
ListElement {
key: "simple"
name: "Simple"
}
ListElement {
key: "fade"
name: "Fade"
}
ListElement {
key: "left"
name: "Left"
}
ListElement {
key: "right"
name: "Right"
}
ListElement {
key: "top"
name: "Top"
}
ListElement {
key: "bottom"
name: "Bottom"
}
ListElement {
key: "wipe"
name: "Wipe"
}
ListElement {
key: "wave"
name: "Wave"
}
ListElement {
key: "grow"
name: "Grow"
}
ListElement {
key: "center"
name: "Center"
}
ListElement {
key: "any"
name: "Any"
}
ListElement {
key: "outer"
name: "Outer"
}
ListElement {
key: "random"
name: "Random"
}
}
currentKey: Settings.data.wallpaper.swww.transitionType
onSelected: key => {
Settings.data.wallpaper.swww.transitionType = key
}
}
// Transition FPS
ColumnLayout {
RowLayout {
NTextInput {
Layout.fillWidth: true
ColumnLayout {
NText {
text: "Transition FPS"
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
NText {
text: "Frames per second for transition animations."
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurface
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
NText {
text: sliderWpTransitionFps.value + " FPS"
Layout.alignment: Qt.AlignBottom | Qt.AlignRight
}
}
NSlider {
id: sliderWpTransitionFps
Layout.fillWidth: true
from: 30
to: 500
stepSize: 5
value: Settings.data.wallpaper.swww.transitionFps
onPressedChanged: Settings.data.wallpaper.swww.transitionFps = Math.round(value)
cutoutColor: Color.mSurface
}
}
// Transition Duration
ColumnLayout {
RowLayout {
Layout.fillWidth: true
ColumnLayout {
NText {
text: "Transition Duration"
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
NText {
text: "Duration of transition animations in seconds."
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurface
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
NText {
text: sliderWpTransitionDuration.value.toFixed(2) + "s"
Layout.alignment: Qt.AlignBottom | Qt.AlignRight
}
}
NSlider {
id: sliderWpTransitionDuration
Layout.fillWidth: true
from: 0.25
to: 10
stepSize: 0.05
value: Settings.data.wallpaper.swww.transitionDuration
onPressedChanged: Settings.data.wallpaper.swww.transitionDuration = value
cutoutColor: Color.mSurface
text: WallpaperService.getMonitorDirectory(modelData.name)
onEditingFinished: WallpaperService.setMonitorDirectory(modelData.name, text)
Layout.maximumWidth: 420 * scaling
}
}
}
@@ -343,25 +68,221 @@ ColumnLayout {
}
}
// Process to check if swww is installed
Process {
id: swwwCheck
command: ["which", "swww"]
running: false
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
onExited: function (exitCode) {
if (exitCode === 0) {
// SWWW exists, enable it
Settings.data.wallpaper.swww.enabled = true
WallpaperService.startSWWWDaemon()
ToastService.showNotice("SWWW", "Enabled!")
} else {
// SWWW not found
ToastService.showWarning("SWWW", "Not installed!")
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NText {
text: "Automation"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
}
// Random Wallpaper
NToggle {
label: "Random Wallpaper"
description: "Schedule random wallpaper changes at regular intervals."
checked: Settings.data.wallpaper.randomEnabled
onToggled: checked => Settings.data.wallpaper.randomEnabled = checked
}
// Interval
ColumnLayout {
visible: Settings.data.wallpaper.randomEnabled
RowLayout {
NLabel {
label: "Wallpaper Interval"
description: "How often to change wallpapers automatically."
Layout.fillWidth: true
}
NText {
// Show friendly H:MM format from current settings
text: Time.formatVagueHumanReadableDuration(Settings.data.wallpaper.randomIntervalSec)
Layout.alignment: Qt.AlignBottom | Qt.AlignRight
}
}
// Preset chips using Repeater
RowLayout {
id: presetRow
spacing: Style.marginS * scaling
// Factorized presets data
property var intervalPresets: [5 * 60, 10 * 60, 15 * 60, 30 * 60, 45 * 60, 60 * 60, 90 * 60, 120 * 60]
// Whether current interval equals one of the presets
property bool isCurrentPreset: {
return intervalPresets.some(seconds => seconds === Settings.data.wallpaper.randomIntervalSec)
}
// Allow user to force open the custom input; otherwise it's auto-open when not a preset
property bool customForcedVisible: false
function setIntervalSeconds(sec) {
Settings.data.wallpaper.randomIntervalSec = sec
WallpaperService.restartRandomWallpaperTimer()
// Hide custom when selecting a preset
customForcedVisible = false
}
// Helper to color selected chip
function isSelected(sec) {
return Settings.data.wallpaper.randomIntervalSec === sec
}
// Repeater for preset chips
Repeater {
model: presetRow.intervalPresets
delegate: IntervalPresetChip {
seconds: modelData
label: Time.formatVagueHumanReadableDuration(modelData)
selected: presetRow.isSelected(modelData)
onClicked: presetRow.setIntervalSeconds(modelData)
}
}
// Custom… opens inline input
IntervalPresetChip {
label: customRow.visible ? "Custom" : "Custom…"
selected: customRow.visible
onClicked: presetRow.customForcedVisible = !presetRow.customForcedVisible
}
}
// Custom HH:MM inline input
RowLayout {
id: customRow
visible: presetRow.customForcedVisible || !presetRow.isCurrentPreset
spacing: Style.marginS * scaling
Layout.topMargin: Style.marginS * scaling
NTextInput {
label: "Custom Interval"
description: "Enter time as HH:MM (e.g., 01:30)."
inputMaxWidth: 100 * scaling
text: {
const s = Settings.data.wallpaper.randomIntervalSec
const h = Math.floor(s / 3600)
const m = Math.floor((s % 3600) / 60)
return h + ":" + (m < 10 ? ("0" + m) : m)
}
onEditingFinished: {
const m = text.trim().match(/^(\d{1,2}):(\d{2})$/)
if (m) {
let h = parseInt(m[1])
let min = parseInt(m[2])
if (isNaN(h) || isNaN(min))
return
h = Math.max(0, Math.min(24, h))
min = Math.max(0, Math.min(59, min))
Settings.data.wallpaper.randomIntervalSec = (h * 3600) + (min * 60)
WallpaperService.restartRandomWallpaperTimer()
// Keep custom visible after manual entry
presetRow.customForcedVisible = true
}
}
}
}
}
stdout: StdioCollector {}
stderr: StdioCollector {}
// Transition Type
NComboBox {
label: "Transition Type"
description: "Animation type when switching between wallpapers."
model: WallpaperService.transitionsModel
currentKey: Settings.data.wallpaper.transitionType
onSelected: key => Settings.data.wallpaper.transitionType = key
}
// Transition Duration
ColumnLayout {
NLabel {
label: "Transition Duration"
description: "Duration of transition animations in seconds."
}
RowLayout {
spacing: Style.marginL * scaling
NSlider {
Layout.fillWidth: true
from: 100
to: 5000
stepSize: 100
value: Settings.data.wallpaper.transitionDuration
onMoved: Settings.data.wallpaper.transitionDuration = value
cutoutColor: Color.mSurface
}
NText {
text: (Settings.data.wallpaper.transitionDuration / 1000).toFixed(2) + "s"
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
}
}
}
// Edge Smoothness
ColumnLayout {
NLabel {
label: "Transition Edge Smoothness"
description: "Duration of transition animations in seconds."
}
RowLayout {
spacing: Style.marginL * scaling
NSlider {
Layout.fillWidth: true
from: 0.0
to: 1.0
value: Settings.data.wallpaper.transitionEdgeSmoothness
onMoved: Settings.data.wallpaper.transitionEdgeSmoothness = value
cutoutColor: Color.mSurface
}
NText {
text: Math.round(Settings.data.wallpaper.transitionEdgeSmoothness * 100) + "%"
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
}
}
}
}
// Reusable component for interval preset chips
component IntervalPresetChip: Rectangle {
property int seconds: 0
property string label: ""
property bool selected: false
signal clicked
radius: height * 0.5
color: selected ? Color.mPrimary : Color.mSurfaceVariant
implicitHeight: Math.max(Style.baseWidgetSize * 0.55 * scaling, 24 * scaling)
implicitWidth: chipLabel.implicitWidth + Style.marginM * 1.5 * scaling
border.width: 1
border.color: selected ? Color.transparent : Color.mOutline
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: parent.clicked()
}
NText {
id: chipLabel
anchors.centerIn: parent
text: parent.label
font.pointSize: Style.fontSizeS * scaling
color: parent.selected ? Color.mOnPrimary : Color.mOnSurface
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
}

View File

@@ -7,19 +7,12 @@ import qs.Commons
import qs.Services
import qs.Widgets
// Media player area (placeholder until MediaPlayer service is wired)
NBox {
id: root
Layout.fillWidth: true
Layout.fillHeight: true
// Let content dictate the height (no hardcoded height here)
// Height can be overridden by parent layout (SidePanel binds it to stats card)
//implicitHeight: content.implicitHeight + Style.marginL * 2 * scaling
// Component.onCompleted: {
// Logger.logMediaService.trackArtUrl)
// }
ColumnLayout {
anchors.fill: parent
Layout.fillHeight: true
@@ -51,6 +44,7 @@ NBox {
Item {
Layout.fillWidth: true
Layout.fillHeight: true
}
}
@@ -165,7 +159,6 @@ NBox {
NImageCircled {
id: trackArt
visible: MediaService.trackArtUrl.toString() !== ""
anchors.fill: parent
anchors.margins: Style.marginXS * scaling
imagePath: MediaService.trackArtUrl
@@ -222,78 +215,87 @@ NBox {
}
// -------------------------
// Progress bar
Rectangle {
id: progressBarBackground
// Progress slider (uses shared NSlider behavior like BarTab)
Item {
id: progressWrapper
visible: (MediaService.currentPlayer && MediaService.trackLength > 0)
width: parent.width
height: 4 * scaling
radius: Style.radiusS * scaling
color: Color.mSurface
Layout.fillWidth: true
height: Style.baseWidgetSize * 0.5 * scaling
// Local preview while dragging
property real localSeekRatio: -1
// Track the last ratio we actually sent to the backend to avoid redundant seeks
property real lastSentSeekRatio: -1
// Minimum change required to issue a new seek during drag
property real seekEpsilon: 0.01
property real progressRatio: {
if (!MediaService.currentPlayer || !MediaService.isPlaying || MediaService.trackLength <= 0) {
if (!MediaService.currentPlayer || MediaService.trackLength <= 0)
return 0
}
return Math.min(1, MediaService.currentPosition / MediaService.trackLength)
const r = MediaService.currentPosition / MediaService.trackLength
if (isNaN(r) || !isFinite(r))
return 0
return Math.max(0, Math.min(1, r))
}
property real effectiveRatio: (MediaService.isSeeking
&& localSeekRatio >= 0) ? Math.max(0, Math.min(1,
localSeekRatio)) : progressRatio
Rectangle {
id: progressFill
width: progressBarBackground.progressRatio * parent.width
height: parent.height
radius: parent.radius
color: Color.mPrimary
Behavior on width {
NumberAnimation {
duration: Style.animationFast
// Debounced backend seek during drag
Timer {
id: seekDebounce
interval: 75
repeat: false
onTriggered: {
if (MediaService.isSeeking && progressWrapper.localSeekRatio >= 0) {
const next = Math.max(0, Math.min(1, progressWrapper.localSeekRatio))
if (progressWrapper.lastSentSeekRatio < 0 || Math.abs(
next - progressWrapper.lastSentSeekRatio) >= progressWrapper.seekEpsilon) {
MediaService.seekByRatio(next)
progressWrapper.lastSentSeekRatio = next
}
}
}
}
// Interactive progress handle
Rectangle {
id: progressHandle
visible: (MediaService.currentPlayer && MediaService.trackLength > 0)
width: 16 * scaling
height: 16 * scaling
radius: width * 0.5
color: Color.mPrimary
border.color: Color.mOutline
border.width: Math.max(1 * Style.borderM * scaling)
x: Math.max(0, Math.min(parent.width - width, progressFill.width - width / 2))
anchors.verticalCenter: parent.verticalCenter
scale: progressMouseArea.containsMouse || progressMouseArea.pressed ? 1.2 : 1.0
Behavior on scale {
NumberAnimation {
duration: Style.animationFast
}
}
}
// Mouse area for seeking
MouseArea {
id: progressMouseArea
NSlider {
id: progressSlider
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
from: 0
to: 1
stepSize: 0
snapAlways: false
enabled: MediaService.trackLength > 0 && MediaService.canSeek
cutoutColor: Color.mSurface
heightRatio: 0.65
onClicked: function (mouse) {
let ratio = mouse.x / width
MediaService.seekByRatio(ratio)
onMoved: {
progressWrapper.localSeekRatio = value
seekDebounce.restart()
}
onPositionChanged: function (mouse) {
onPressedChanged: {
if (pressed) {
let ratio = Math.max(0, Math.min(1, mouse.x / width))
MediaService.seekByRatio(ratio)
MediaService.isSeeking = true
progressWrapper.localSeekRatio = value
MediaService.seekByRatio(value)
progressWrapper.lastSentSeekRatio = value
} else {
seekDebounce.stop()
MediaService.seekByRatio(value)
MediaService.isSeeking = false
progressWrapper.localSeekRatio = -1
progressWrapper.lastSentSeekRatio = -1
}
}
}
// While not dragging, bind slider to live progress
// during drag, let the slider manage its own value
Binding {
target: progressSlider
property: "value"
value: progressWrapper.progressRatio
when: !MediaService.isSeeking
}
}
// -------------------------
@@ -322,7 +324,7 @@ NBox {
// Next button
NIconButton {
icon: "skip_next"
tooltipText: "Next Media"
tooltipText: "Next media"
visible: MediaService.canGoNext
onClicked: MediaService.canGoNext ? MediaService.next() : {}
}
@@ -330,7 +332,7 @@ NBox {
}
Loader {
active: Settings.data.audio.visualizerType == "linear"
active: Settings.data.audio.visualizerType == "linear" && MediaService.isPlaying
Layout.alignment: Qt.AlignHCenter
sourceComponent: LinearSpectrum {
@@ -343,7 +345,7 @@ NBox {
}
Loader {
active: Settings.data.audio.visualizerType == "mirrored"
active: Settings.data.audio.visualizerType == "mirrored" && MediaService.isPlaying
Layout.alignment: Qt.AlignHCenter
sourceComponent: MirroredSpectrum {
@@ -356,7 +358,7 @@ NBox {
}
Loader {
active: Settings.data.audio.visualizerType == "wave"
active: Settings.data.audio.visualizerType == "wave" && MediaService.isPlaying
Layout.alignment: Qt.AlignHCenter
sourceComponent: WaveSpectrum {

View File

@@ -29,7 +29,7 @@ NBox {
// Performance
NIconButton {
icon: "speed"
tooltipText: "Set Performance Power Profile"
tooltipText: "Set performance power profile"
enabled: hasPP
opacity: enabled ? Style.opacityFull : Style.opacityMedium
colorBg: (enabled && powerProfiles.profile === PowerProfile.Performance) ? Color.mPrimary : Color.mSurfaceVariant
@@ -43,7 +43,7 @@ NBox {
// Balanced
NIconButton {
icon: "balance"
tooltipText: "Set Balanced Power Profile"
tooltipText: "Set balanced power profile"
enabled: hasPP
opacity: enabled ? Style.opacityFull : Style.opacityMedium
colorBg: (enabled && powerProfiles.profile === PowerProfile.Balanced) ? Color.mPrimary : Color.mSurfaceVariant
@@ -57,7 +57,7 @@ NBox {
// Eco
NIconButton {
icon: "eco"
tooltipText: "Set Eco Power Profile"
tooltipText: "Set eco power profile"
enabled: hasPP
opacity: enabled ? Style.opacityFull : Style.opacityMedium
colorBg: (enabled && powerProfiles.profile === PowerProfile.PowerSaver) ? Color.mPrimary : Color.mSurfaceVariant

View File

@@ -58,7 +58,7 @@ NBox {
}
NIconButton {
icon: "settings"
tooltipText: "Open Settings"
tooltipText: "Open settings"
onClicked: {
settingsPanel.requestedTab = SettingsPanel.Tab.General
settingsPanel.open(screen)
@@ -68,7 +68,7 @@ NBox {
NIconButton {
id: powerButton
icon: "power_settings_new"
tooltipText: "Power Menu"
tooltipText: "Power menu"
onClicked: {
powerPanel.open(screen)
sidePanel.close()
@@ -78,7 +78,7 @@ NBox {
NIconButton {
id: closeButton
icon: "close"
tooltipText: "Close Side Panel"
tooltipText: "Close side panel"
onClicked: {
sidePanel.close()
}

View File

@@ -26,7 +26,7 @@ NBox {
// Screen Recorder
NIconButton {
icon: "videocam"
tooltipText: ScreenRecorderService.isRecording ? "Stop Screen Recording" : "Start Screen Recording"
tooltipText: ScreenRecorderService.isRecording ? "Stop screen recording" : "Start screen recording"
colorBg: ScreenRecorderService.isRecording ? Color.mPrimary : Color.mSurfaceVariant
colorFg: ScreenRecorderService.isRecording ? Color.mOnPrimary : Color.mPrimary
onClicked: {
@@ -37,7 +37,7 @@ NBox {
// Idle Inhibitor
NIconButton {
icon: "coffee"
tooltipText: IdleInhibitorService.isInhibited ? "Disable Keep Awake" : "Enable Keep Awake"
tooltipText: IdleInhibitorService.isInhibited ? "Disable keep awake" : "Enable keep awake"
colorBg: IdleInhibitorService.isInhibited ? Color.mPrimary : Color.mSurfaceVariant
colorFg: IdleInhibitorService.isInhibited ? Color.mOnPrimary : Color.mPrimary
onClicked: {
@@ -48,12 +48,15 @@ NBox {
// Wallpaper
NIconButton {
icon: "image"
tooltipText: "Open 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(screen)
}
onRightClicked: {
WallpaperService.setRandomWallpaper()
}
}
Item {

View File

@@ -1,70 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Widgets
// ToastManager creates toast overlays on each screen
Variants {
model: Quickshell.screens
delegate: PanelWindow {
id: root
required property ShellScreen modelData
readonly property real scaling: ScalingService.scale(screen)
screen: modelData
// Only show on screens that have notifications enabled
visible: modelData ? (Settings.data.notifications.monitors.includes(modelData.name)
|| (Settings.data.notifications.monitors.length === 0)) : false
// Position based on bar location, like Notification popup does
anchors {
top: Settings.data.bar.position === "top"
bottom: Settings.data.bar.position === "bottom"
left: true
right: true
}
// Set margins based on bar position
margins.top: Settings.data.bar.position === "top" ? (Style.barHeight + Style.marginS) * scaling : 0
margins.bottom: Settings.data.bar.position === "bottom" ? (Style.barHeight + Style.marginS) * scaling : 0
// Small height when hidden, appropriate height when visible
implicitHeight: toast.visible ? toast.height + Style.marginS * scaling : 1
// Transparent background
color: Color.transparent
// High layer to appear above other panels
//WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
exclusionMode: PanelWindow.ExclusionMode.Ignore
NToast {
id: toast
scaling: root.scaling
// Simple positioning - margins already account for bar
targetY: Style.marginS * scaling
// Hidden position based on bar location
hiddenY: Settings.data.bar.position === "top" ? -toast.height - 20 : toast.height + 20
Component.onCompleted: {
// Only register toasts for screens that have notifications enabled
if (modelData ? (Settings.data.notifications.monitors.includes(modelData.name)
|| (Settings.data.notifications.monitors.length === 0)) : false) {
// Register this toast with the service
ToastService.allToasts.push(toast)
// Connect dismissal signal
toast.dismissed.connect(ToastService.onToastDismissed)
}
}
}
}
}

View File

@@ -0,0 +1,78 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Widgets
Variants {
model: Quickshell.screens
delegate: Loader {
required property ShellScreen modelData
property real scaling: ScalingService.getScreenScale(modelData)
Connections {
target: ScalingService
function onScaleChanged(screenName, scale) {
if (screenName === modelData.name) {
scaling = scale
}
}
}
// Only show on screens that have notifications enabled
active: Settings.isLoaded && modelData ? (Settings.data.notifications.monitors.includes(modelData.name)
|| (Settings.data.notifications.monitors.length === 0)) : false
sourceComponent: PanelWindow {
id: root
screen: modelData
// Position based on bar location, like Notification popup does
anchors {
top: Settings.data.bar.position === "top"
bottom: Settings.data.bar.position === "bottom"
}
// Set a width instead of anchoring left/right so we can click on the side of the toast
implicitWidth: 500 * scaling
// Small height when hidden, appropriate height when visible
implicitHeight: Math.round(toast.visible ? toast.height + Style.marginM * scaling : 1)
// Set margins based on bar position
margins.top: Settings.data.bar.position === "top" ? (Style.barHeight + Style.marginS) * scaling : 0
margins.bottom: Settings.data.bar.position === "bottom" ? (Style.barHeight + Style.marginS) * scaling : 0
// Transparent background
color: Color.transparent
// Overlay layer to appear above other panels
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
exclusionMode: PanelWindow.ExclusionMode.Ignore
NToast {
id: toast
screen: modelData
// Simple positioning - margins already account for bar
targetY: Style.marginS * scaling
// Hidden position based on bar location
hiddenY: Settings.data.bar.position === "top" ? -toast.height - 20 : toast.height + 20
Component.onCompleted: {
// Register this toast with the service
ToastService.allToasts.push(toast)
// Connect dismissal signal
toast.dismissed.connect(ToastService.onToastDismissed)
}
}
}
}
}

View File

@@ -46,12 +46,13 @@ NPanel {
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
Layout.leftMargin: Style.marginS * scaling
}
NIconButton {
icon: "refresh"
tooltipText: "Refresh Networks"
sizeMultiplier: 0.8
tooltipText: "Refresh networks"
sizeRatio: 0.8
enabled: Settings.data.network.wifiEnabled && !NetworkService.isLoading
onClicked: {
NetworkService.refreshNetworks()
@@ -61,7 +62,7 @@ NPanel {
NIconButton {
icon: "close"
tooltipText: "Close"
sizeMultiplier: 0.8
sizeRatio: 0.8
onClicked: {
root.close()
}
@@ -72,6 +73,16 @@ NPanel {
Layout.fillWidth: true
}
// Show errors at the very top
NText {
visible: NetworkService.connectStatus === "error" && NetworkService.connectError.length > 0
text: NetworkService.connectError
color: Color.mError
font.pointSize: Style.fontSizeXS * scaling
wrapMode: Text.Wrap
Layout.fillWidth: true
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
@@ -127,69 +138,65 @@ NPanel {
// Network list
ListView {
id: networkList
anchors.fill: parent
visible: Settings.data.network.wifiEnabled && !NetworkService.isLoading
model: Object.values(NetworkService.networks)
spacing: Style.marginM * scaling
spacing: Style.marginS * scaling
clip: true
delegate: Item {
width: parent ? parent.width : 0
height: modelData.ssid === passwordPromptSsid
&& showPasswordPrompt ? 108 * scaling : Style.baseWidgetSize * 1.5 * scaling
&& showPasswordPrompt ? 130 * scaling : Style.baseWidgetSize * 1.75 * scaling
ColumnLayout {
anchors.fill: parent
spacing: 0
Rectangle {
id: rect
Layout.fillWidth: true
Layout.preferredHeight: Style.baseWidgetSize * 1.5 * scaling
Layout.fillHeight: true
radius: Style.radiusS * scaling
color: modelData.connected ? Color.mPrimary : (networkMouseArea.containsMouse ? Color.mTertiary : Color.transparent)
color: networkMouseArea.containsMouse ? Color.mTertiary : Color.transparent
border.color: modelData.connected ? Color.mPrimary : Color.transparent
border.width: Math.max(1, Style.borderM * scaling)
RowLayout {
anchors.fill: parent
anchors.margins: Style.marginS * scaling
anchors {
fill: parent
leftMargin: Style.marginL * scaling
rightMargin: Style.marginL * scaling
}
spacing: Style.marginS * scaling
NIcon {
text: NetworkService.signalIcon(modelData.signal)
font.pointSize: Style.fontSizeXXL * scaling
color: modelData.connected ? Color.mSurface : (networkMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface)
color: networkMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface
}
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginXS * scaling
Layout.alignment: Qt.AlignVCenter
spacing: 0
// SSID
NText {
Layout.fillWidth: true
text: modelData.ssid || "Unknown Network"
font.pointSize: Style.fontSizeNormal * scaling
elide: Text.ElideRight
Layout.fillWidth: true
color: modelData.connected ? Color.mSurface : (networkMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface)
color: networkMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface
}
// Security Protocol
NText {
text: modelData.security && modelData.security !== "--" ? modelData.security : "Open"
font.pointSize: Style.fontSizeXS * scaling
elide: Text.ElideRight
Layout.fillWidth: true
color: modelData.connected ? Color.mSurface : (networkMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface)
}
NText {
visible: NetworkService.connectStatusSsid === modelData.ssid
&& NetworkService.connectStatus === "error" && NetworkService.connectError.length > 0
text: NetworkService.connectError
color: Color.mError
font.pointSize: Style.fontSizeXS * scaling
font.pointSize: Style.fontSizeXXS * scaling
elide: Text.ElideRight
Layout.fillWidth: true
color: networkMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface
}
}
@@ -203,17 +210,25 @@ NPanel {
NBusyIndicator {
visible: NetworkService.connectingSsid === modelData.ssid
running: NetworkService.connectingSsid === modelData.ssid
color: Color.mPrimary
color: networkMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface
anchors.centerIn: parent
size: Style.baseWidgetSize * 0.7 * scaling
}
}
NText {
RowLayout {
visible: modelData.connected
text: "connected"
font.pointSize: Style.fontSizeXS * scaling
color: modelData.connected ? Color.mSurface : (networkMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface)
NText {
text: "Connected"
font.pointSize: Style.fontSizeXS * scaling
color: networkMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface
Layout.alignment: Qt.AlignVCenter
}
NIcon {
text: "check"
font.pointSize: Style.fontSizeXXL * scaling
color: networkMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface
}
}
}
@@ -224,6 +239,7 @@ NPanel {
onClicked: {
if (modelData.connected) {
NetworkService.disconnectNetwork(modelData.ssid)
showPasswordPrompt = false
} else if (NetworkService.isSecured(modelData.security) && !modelData.existing) {
passwordPromptSsid = modelData.ssid
showPasswordPrompt = true
@@ -240,11 +256,11 @@ NPanel {
// Password prompt section
Rectangle {
id: passwordPromptSection
visible: modelData.ssid === passwordPromptSsid && showPasswordPrompt
Layout.fillWidth: true
Layout.preferredHeight: modelData.ssid === passwordPromptSsid && showPasswordPrompt ? 60 : 0
Layout.margins: Style.marginS * scaling
visible: modelData.ssid === passwordPromptSsid && showPasswordPrompt
color: Color.mSurfaceVariant
radius: Style.radiusS * scaling
@@ -255,7 +271,7 @@ NPanel {
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.barHeight * scaling
Layout.preferredHeight: Math.round(Style.barHeight * scaling)
Rectangle {
anchors.fill: parent
@@ -280,8 +296,10 @@ NPanel {
echoMode: TextInput.Password
onTextChanged: passwordInput = text
onAccepted: {
NetworkService.submitPassword(passwordPromptSsid, passwordInput)
showPasswordPrompt = false
if (passwordInput !== "") {
NetworkService.submitPassword(passwordPromptSsid, passwordInput)
showPasswordPrompt = false
}
}
MouseArea {
@@ -295,7 +313,7 @@ NPanel {
Rectangle {
Layout.preferredWidth: Style.baseWidgetSize * 2.5 * scaling
Layout.preferredHeight: Style.barHeight * scaling
Layout.preferredHeight: Math.round(Style.barHeight * scaling)
radius: Style.radiusM * scaling
color: Color.mPrimary
@@ -315,8 +333,10 @@ NPanel {
MouseArea {
anchors.fill: parent
onClicked: {
NetworkService.submitPassword(passwordPromptSsid, passwordInput)
showPasswordPrompt = false
if (passwordInput !== "") {
NetworkService.submitPassword(passwordPromptSsid, passwordInput)
showPasswordPrompt = false
}
}
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
@@ -324,6 +344,13 @@ NPanel {
onExited: parent.color = Color.mPrimary
}
}
NIconButton {
icon: "close"
onClicked: {
showPasswordPrompt = false
}
}
}
}
}

218
README.md
View File

@@ -27,11 +27,11 @@ Features a modern modular architecture with a status bar, notification system, c
## Preview
![Launcher](https://assets.noctalia.dev/screenshots/launcher.png)
![Launcher](/Assets/Screenshots/launcher.png)
![SettingsPanel](https://assets.noctalia.dev/screenshots/settings-panel.png)
![SettingsPanel](/Assets/Screenshots/settings-panel.png?v=2)
![SidePanel](https://assets.noctalia.dev/screenshots/light-mode.png)
![SidePanel](/Assets/Screenshots/light-mode.png?v=2)
---
@@ -67,19 +67,29 @@ Features a modern modular architecture with a status bar, notification system, c
- `ttf-roboto` - The default font used for most of the UI
- `inter-font` - The default font used for Headers (ex: clock on the LockScreen)
- `ttf-material-symbols-variable-git` - Icon font for UI elements
- `xdg-desktop-portal-gnome` - Desktop integration (or alternative portal)
- `gpu-screen-recorder` - Screen recording functionality
- `brightnessctl` - For internal/laptop monitor brightness
- `ddcutil` - For desktop monitor brightness (might introduce some system instability with certain monitors)
### Optional
- `cliphist` - For clipboard history support
- `swww` - Wallpaper animations and effects
- `matugen` - Material You color scheme generation
- `cava` - Audio visualizer component
- `gpu-screen-recorder` - Screen recording functionality
- `brightnessctl` - For internal/laptop monitor brightness
- `ddcutil` - For desktop monitor brightness (might introduce some system instability with certain monitors)
If you want to use the ArchUpdater Widget, make sure you have any polkit agent installed.
- `wlsunset` - To be able to use NightLight
> There is one more optional dependency.
> `xdg-desktop-portal` to be able to use the "Portal" option from the screenRecorder.
If you want to use the `ArchUpdater` widget, you will have to set your `TERMINAL` environment variable.
Example command (you can edit the /etc/environment file manually too):
`sudo sed -i '/^TERMINAL=/d' /etc/environment && echo 'TERMINAL=/usr/bin/kitty' | sudo tee -a /etc/environment
`
Please do not forget to edit `TERMINAL=/usr/bin/kitty` to match your terminal.
---
@@ -87,65 +97,157 @@ If you want to use the ArchUpdater Widget, make sure you have any polkit agent i
### Installation
```bash
# Install Quickshell
yay -S quickshell-git
#### Arch Linux
# Download and install Noctalia (latest release)
mkdir -p ~/.config/quickshell && curl -sL https://github.com/noctalia-dev/noctalia-shell/releases/latest/download/noctalia-latest.tar.gz | tar -xz --strip-components=1 -C ~/.config/quickshell
<details>
<summary><strong>AUR</strong></summary>
You can install Noctalia from the [AUR](https://aur.archlinux.org/packages/noctalia-shell). This method will install the shell system-wide.
```bash
paru -S noctalia-shell
```
If you want the latest development version directly from the git repository, you can use the `noctalia-shell-git` package:
```bash
paru -S noctalia-shell-git
```
This will always pull the most recent commit from the Noctalia repository. Note that it may be less stable than the release version.
</details>
<details>
<summary><strong>Manual Installation</strong></summary>
This method installs the shell to your local user configuration.
Make sure you have Quickshell installed:
```bash
paru -S quickshell-git
```
Download and install Noctalia (latest release):
```bash
mkdir -p ~/.config/quickshell/noctalia-shell && curl -sL https://github.com/noctalia-dev/noctalia-shell/releases/latest/download/noctalia-latest.tar.gz | tar -xz --strip-components=1 -C ~/.config/quickshell/noctalia-shell
```
</details>
#### Nix
<details>
<summary><strong>Nix Installation</strong></summary>
You can run Noctalia directly using the `nix run` command:
```bash
nix run github:noctalia-dev/noctalia-shell
```
Alternatively, you can add it to your NixOS configuration or flake:
**Step 1**: Add Quickshell and Noctalia flakes to your `flake.nix`:
```nix
{
description = "Example Nix flake with Noctalia + Quickshell";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
noctalia = {
url = "github:noctalia-dev/noctalia-shell";
inputs.nixpkgs.follows = "nixpkgs";
};
quickshell = {
url = "github:outfoxxed/quickshell";
inputs.nixpkgs.follows = "nixpkgs";
inputs.quickshell.follows = "quickshell"
};
};
outputs = { self, nixpkgs, noctalia, quickshell, ... }:
{
nixosConfigurations.my-host = nixpkgs.lib.nixosSystem {
modules = [
./configuration.nix
];
};
};
}
```
**Step 2**: Add the packages to your `configuration.nix`:
```nix
{
environment.systemPackages = with pkgs; [
inputs.noctalia.packages.${system}.default
inputs.quickshell.packages.${system}.default
];
}
```
</details>
### Usage
```bash
# Start the shell
qs
<details>
<summary> Nix </summary>
The following commands apply to the Nix flake installation.
# Launcher
qs ipc call launcher toggle
| Action | Command |
| --------------------------- | ----------------------------------------------------- |
| Start the Shell | `noctalia-shell` |
| Toggle Application Launcher | `noctalia-shell ipc call launcher toggle` |
| Toggle Side Panel | `noctalia-shell ipc call sidePanel toggle` |
| Open Clipboard History | `noctalia-shell ipc call launcher clipboard` |
| Open Calculator | `noctalia-shell ipc call launcher calculator` |
| Increase Brightness | `noctalia-shell ipc call brightness increase` |
| Decrease Brightness | `noctalia-shell ipc call brightness decrease` |
| Increase Output Volume | `noctalia-shell ipc call volume increase` |
| Decrease Output Volume | `noctalia-shell ipc call volume decrease` |
| Toggle Mute Audio Output | `noctalia-shell ipc call volume muteOutput` |
| Toggle Mute Audio Input | `noctalia-shell ipc call volume muteInput` |
| Toggle Power Panel | `noctalia-shell ipc call powerPanel toggle` |
| Toggle Idle Inhibitor | `noctalia-shell ipc call idleInhibitor toggle` |
| Toggle Settings Window | `noctalia-shell ipc call settings toggle` |
| Toggle Lock Screen | `noctalia-shell ipc call lockScreen toggle` |
| Toggle Notification History | `noctalia-shell ipc call notifications toggleHistory` |
# SidePanel
qs ipc call sidePanel toggle
</details>
# Clipboard History
qs ipc call launcher clipboard
# Calculator
qs ipc call launcher calculator
<details>
<summary> AUR/Manual install </summary>
# Brightness
qs ipc call brightness increase
qs ipc call brightness decrease
The following commands apply to both AUR package and manual installation.
# Power Panel
qs ipc call powerPanel toggle
| Action | Command |
| --------------------------- | ----------------------------------------------------------- |
| Start the Shell | `qs -c noctalia-shell` |
| Toggle Application Launcher | `qs -c noctalia-shell ipc call launcher toggle` |
| Toggle Side Panel | `qs -c noctalia-shell ipc call sidePanel toggle` |
| Open Clipboard History | `qs -c noctalia-shell ipc call launcher clipboard` |
| Open Calculator | `qs -c noctalia-shell ipc call launcher calculator` |
| Increase Brightness | `qs -c noctalia-shell ipc call brightness increase` |
| Decrease Brightness | `qs -c noctalia-shell ipc call brightness decrease` |
| Increase Output Volume | `qs -c noctalia-shell ipc call volume increase` |
| Decrease Output Volume | `qs -c noctalia-shell ipc call volume decrease` |
| Toggle Mute Audio Output | `qs -c noctalia-shell ipc call volume muteOutput` |
| Toggle Mute Audio Input | `qs -c noctalia-shell ipc call volume muteInput` |
| Toggle Power Panel | `qs -c noctalia-shell ipc call powerPanel toggle` |
| Toggle Idle Inhibitor | `qs -c noctalia-shell ipc call idleInhibitor toggle` |
| Toggle Settings Window | `qs -c noctalia-shell ipc call settings toggle` |
| Toggle Lock Screen | `qs -c noctalia-shell ipc call lockScreen toggle` |
| Toggle Notification History | `qs -c noctalia-shell ipc call notifications toggleHistory` |
# Idle Inhibitor
qs ipc call idleInhibitor toggle
</details>
# Settings Window
qs ipc call settings toggle
# Toggle lock screen
qs ipc call lockScreen toggle
```
### Keybinds
| Action | Command |
|--------|---------|
| Toggle Application Launcher | `qs ipc call appLauncher toggle` |
| Toggle Lock Screen | `qs ipc call lockScreen toggle` |
| Toggle Notification History | `qs ipc call notifications toggleHistory` |
| Toggle Settings Panel | `qs ipc call settings toggle` |
| Increase Brightness | `qs ipc call brightness increase` |
| Decrease Brightness | `qs ipc call brightness decrease` |
### Configuration
Access settings through the side panel (top right button) to configure weather, wallpapers, screen recording, audio, network, and theme options.
Configuration is usually stored in ~/.config/noctalia
If you upgrade from v1, you can delete the old configuration folder at ~/.config/Noctalia (with capital N)
Configuration is usually stored in ~/.config/noctalia.
### Application Launcher
@@ -181,14 +283,6 @@ The launcher supports special commands for enhanced functionality:
## Advanced Configuration
### Niri Configuration
Add this to your `layout` section for proper swww integration:
```
background-color "transparent"
```
### Recommended Compositor Settings
For Niri:
@@ -199,11 +293,6 @@ window-rule {
clip-to-geometry true
}
layer-rule {
match namespace="^swww-daemon$"
place-within-backdrop true
}
layer-rule {
match namespace="^quickshell-wallpaper$"
}
@@ -273,6 +362,7 @@ While I actually didn't want to accept donations, more and more people are askin
Thank you to everyone who supports me and this project 💜!
* Gohma
* <a href="https://pika-os.com/" target="_blank">PikaOS</a>
* DiscoCevapi
---

View File

@@ -1,40 +1,300 @@
/*pragma Singleton
pragma Singleton
import Quickshell
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons
Singleton {
id: updateService
// Core properties
readonly property bool busy: checkupdatesProcess.running
readonly property int updates: updatePackages.length
property var updatePackages: []
// ============================================================================
// CORE PROPERTIES
// ============================================================================
// Package data
property var repoPackages: []
property var aurPackages: []
property var selectedPackages: []
property int selectedPackagesCount: 0
property bool updateInProgress: false
property string allUpdatesOutput: ""
// Process for checking updates
Process {
id: checkupdatesProcess
command: ["checkupdates"]
onExited: function (exitCode) {
if (exitCode !== 0 && exitCode !== 2) {
console.warn("[UpdateService] checkupdates failed (code:", exitCode, ")")
updatePackages = []
return
}
// Update state
property bool updateInProgress: false
property bool updateFailed: false
property string lastUpdateError: ""
property bool checkFailed: false
property string lastCheckError: ""
// Monitoring state
property string capturedErrorText: ""
property string capturedSuccessText: ""
// Computed properties
readonly property bool aurBusy: checkAurUpdatesProcess.running || checkAurOnlyProcess.running
readonly property int updates: repoPackages.length
readonly property int aurUpdates: aurPackages.length
readonly property int totalUpdates: updates + aurUpdates
// Terminal validation
readonly property bool terminalAvailable: Quickshell.env("TERMINAL") !== ""
readonly property string terminalError: "TERMINAL environment variable not set"
// AUR helper validation
readonly property bool aurHelperAvailable: cachedAurHelper !== ""
readonly property string aurHelperError: "No AUR helper found (yay or paru not installed)"
// Polling cooldown (prevent excessive polling)
property int lastPollTime: 0
readonly property int pollCooldownMs: 5 * 60 * 1000 // 5 minutes
readonly property bool canPoll: (Date.now() - lastPollTime) > pollCooldownMs
// ============================================================================
// TIMERS
// ============================================================================
// Refresh timer for post-update polling
Timer {
id: refreshTimer
interval: 5000
repeat: false
onTriggered: {
doPoll()
}
stdout: StdioCollector {
onStreamFinished: {
parseCheckupdatesOutput(text)
}
// Timer to mark update as complete - with error handling
Timer {
id: updateCompleteTimer
interval: 30000 // Increased to 30 seconds to allow more time
repeat: false
onTriggered: {
checkForUpdateFailures()
}
}
// Timer to check if update processes are still running
Timer {
id: updateMonitorTimer
interval: 2000
repeat: true
running: updateInProgress
onTriggered: {
// Check if any update-related processes might still be running
checkUpdateStatus()
}
}
// ============================================================================
// MONITORING PROCESSES
// ============================================================================
// Process to monitor update completion
Process {
id: updateStatusProcess
command: ["pgrep", "-f", "(yay|paru).*(-S|-Syu)"]
onExited: function (exitCode) {
if (exitCode !== 0 && updateInProgress) {
// No update processes found, update likely completed
updateInProgress = false
updateMonitorTimer.stop()
errorCheckTimer.stop()
successCheckTimer.stop()
// Don't stop the complete timer - let it handle failures
// If the update actually failed, the timer will trigger and set updateFailed = true
// Refresh package lists after a short delay
Qt.callLater(() => {
doPoll()
}, 2000)
}
}
}
// Parse checkupdates output
function parseCheckupdatesOutput(output) {
// Process to check for errors in log file (only when update is in progress)
Process {
id: errorCheckProcess
command: ["sh", "-c", "if [ -f /tmp/archupdater_output.log ]; then grep -i 'failed to build\\|could not resolve\\|unable to satisfy\\|failed to install\\|failed to upgrade\\|error:' /tmp/archupdater_output.log | grep -v 'ERROR_DETECTED' | tail -1; fi"]
onExited: function (exitCode) {
if (exitCode === 0 && updateInProgress && capturedErrorText.trim() !== "") {
// Error found in log
updateInProgress = false
updateFailed = true
updateCompleteTimer.stop()
updateMonitorTimer.stop()
errorCheckTimer.stop()
successCheckTimer.stop()
lastUpdateError = "Build or update error detected"
// Refresh to check actual state
Qt.callLater(() => {
doPoll()
}, 1000)
}
}
stdout: StdioCollector {
onStreamFinished: {
capturedErrorText = text || ""
}
}
}
// Process to check for successful completion
Process {
id: successCheckProcess
command: ["sh", "-c", "if [ -f /tmp/archupdater_output.log ]; then grep -i 'Update complete!\\|:: Running post-transaction hooks\\|:: Processing package changes\\|upgrading.*\\.\\.\\.\\|installing.*\\.\\.\\.\\|removing.*\\.\\.\\.' /tmp/archupdater_output.log | tail -1; fi"]
onExited: function (exitCode) {
if (exitCode === 0 && updateInProgress && capturedSuccessText.trim() !== "") {
// Success indicators found
updateInProgress = false
updateFailed = false
updateCompleteTimer.stop()
updateMonitorTimer.stop()
errorCheckTimer.stop()
successCheckTimer.stop()
lastUpdateError = ""
// Refresh to check actual state
Qt.callLater(() => {
doPoll()
}, 1000)
}
}
stdout: StdioCollector {
onStreamFinished: {
capturedSuccessText = text || ""
}
}
}
// Timer to check for success more frequently when update is in progress
Timer {
id: successCheckTimer
interval: 5000 // Check every 5 seconds
repeat: true
running: updateInProgress
onTriggered: {
if (updateInProgress && !successCheckProcess.running) {
successCheckProcess.running = true
}
}
}
// Timer to check for errors more frequently when update is in progress
Timer {
id: errorCheckTimer
interval: 5000 // Check every 5 seconds
repeat: true
running: updateInProgress
onTriggered: {
if (updateInProgress && !errorCheckProcess.running) {
errorCheckProcess.running = true
}
}
}
// ============================================================================
// MONITORING FUNCTIONS
// ============================================================================
function checkUpdateStatus() {
if (updateInProgress && !updateStatusProcess.running) {
updateStatusProcess.running = true
}
}
function checkForUpdateFailures() {
updateInProgress = false
updateFailed = true
updateCompleteTimer.stop()
updateMonitorTimer.stop()
// Refresh to check actual state after a delay
Qt.callLater(() => {
doPoll()
}, 2000)
}
// Initial check
Component.onCompleted: {
// Start AUR helper detection
getAurHelper()
// Set up a fallback timer in case detection takes too long
Qt.callLater(() => {
if (cachedAurHelper === "" && !yayCheckProcess.running && !paruCheckProcess.running) {
// No AUR helper found after reasonable time and processes have finished
checkFailed = true
lastCheckError = "No AUR helper found (yay or paru not installed)"
Logger.warn("ArchUpdater", "No AUR helper found (yay or paru)")
}
}, 5000) // 5 second fallback
}
// ============================================================================
// PACKAGE CHECKING PROCESSES
// ============================================================================
// Process for checking all updates with AUR helper (repo + AUR)
Process {
id: checkAurUpdatesProcess
command: []
onExited: function (exitCode) {
// For both yay and paru: exit code 0 = updates available, exit code 1 = no updates
if (exitCode !== 0 && exitCode !== 1) {
Logger.warn("ArchUpdater", "AUR helper check failed (code:", exitCode, ")")
checkFailed = true
lastCheckError = "Failed to check for updates (exit code: " + exitCode + ")"
aurPackages = []
repoPackages = []
}
// Don't clear checkFailed here - wait for the second process to complete
}
stdout: StdioCollector {
onStreamFinished: {
allUpdatesOutput = text
// Now get AUR-only updates to compare
checkAurOnlyProcess.running = true
}
}
}
// Process for checking AUR-only updates (to separate from repo updates)
Process {
id: checkAurOnlyProcess
command: []
onExited: function (exitCode) {
// For both yay and paru: exit code 0 = updates available, exit code 1 = no updates
if (exitCode !== 0 && exitCode !== 1) {
Logger.warn("ArchUpdater", "AUR helper AUR-only check failed (code:", exitCode, ")")
checkFailed = true
lastCheckError = "Failed to check AUR updates (exit code: " + exitCode + ")"
aurPackages = []
repoPackages = []
} else {
// Only clear checkFailed if both processes succeeded
// Check if the first process also succeeded (no error was set)
if (!checkFailed) {
checkFailed = false
lastCheckError = ""
}
}
}
stdout: StdioCollector {
onStreamFinished: {
parseAllUpdatesOutput(allUpdatesOutput, text)
Logger.log("ArchUpdater", "found", repoPackages.length, "repo package(s) and", aurPackages.length,
"AUR package(s) to upgrade")
}
}
}
// ============================================================================
// PARSING FUNCTIONS
// ============================================================================
// Generic package parsing function
function parsePackageOutput(output, source) {
const lines = output.trim().split('\n').filter(line => line.trim())
const packages = []
@@ -45,33 +305,146 @@ Singleton {
"name": m[1],
"oldVersion": m[2],
"newVersion": m[3],
"description": `${m[1]} ${m[2]} -> ${m[3]}`
"description": `${m[1]} ${m[2]} -> ${m[3]}`,
"source": source
})
}
}
updatePackages = packages
// Only update if we have new data or if this is a fresh check
if (packages.length > 0 || output.trim() === "") {
if (source === "repo") {
repoPackages = packages
} else {
aurPackages = packages
}
}
}
// Parse all updates output (repo + AUR packages)
function parseAllUpdatesOutput(allOutput, aurOnlyOutput) {
const allLines = allOutput.trim().split('\n').filter(line => line.trim())
const aurOnlyLines = aurOnlyOutput.trim().split('\n').filter(line => line.trim())
// Create a set of AUR package names for quick lookup
const aurPackageNames = new Set()
for (const line of aurOnlyLines) {
const m = line.match(/^(\S+)\s+([^\s]+)\s+->\s+([^\s]+)$/)
if (m) {
aurPackageNames.add(m[1])
}
}
const repoPackages = []
const aurPackages = []
for (const line of allLines) {
const m = line.match(/^(\S+)\s+([^\s]+)\s+->\s+([^\s]+)$/)
if (m) {
const packageInfo = {
"name": m[1],
"oldVersion": m[2],
"newVersion": m[3],
"description": `${m[1]} ${m[2]} -> ${m[3]}`
}
// Check if this package is in the AUR-only list
if (aurPackageNames.has(m[1])) {
packageInfo.source = "aur"
aurPackages.push(packageInfo)
} else {
packageInfo.source = "repo"
repoPackages.push(packageInfo)
}
}
}
// Update the package lists
if (repoPackages.length > 0 || aurPackages.length > 0 || allOutput.trim() === "") {
updateService.repoPackages = repoPackages
updateService.aurPackages = aurPackages
}
}
// Check for updates
function doPoll() {
if (busy)
// Prevent excessive polling
if (aurBusy || !canPoll) {
return
checkupdatesProcess.running = true
}
// Check if we have a cached AUR helper
if (cachedAurHelper !== "") {
// Clear error state when helper is available
if (checkFailed && lastCheckError.includes("No AUR helper found")) {
checkFailed = false
lastCheckError = ""
}
checkAurUpdatesProcess.command = [cachedAurHelper, "-Qu"]
checkAurOnlyProcess.command = [cachedAurHelper, getAurOnlyFlag()]
// Start AUR updates check (includes both repo and AUR packages)
checkAurUpdatesProcess.running = true
lastPollTime = Date.now()
} else {
// AUR helper detection is still in progress or failed
// Try to detect again if not already in progress
if (!yayCheckProcess.running && !paruCheckProcess.running) {
getAurHelper()
}
Logger.warn("ArchUpdater", "AUR helper detection in progress or failed")
}
}
// Update all packages
// ============================================================================
// UPDATE FUNCTIONS
// ============================================================================
// Helper function to generate update command with error detection
function generateUpdateCommand(baseCommand) {
return baseCommand + " 2>&1 | tee /tmp/archupdater_output.log; if [ $? -ne 0 ]; then echo 'ERROR_DETECTED'; fi; echo 'Update complete! Press Enter to close...'; read -p 'Press Enter to continue...'"
}
// Update all packages (repo + AUR)
function runUpdate() {
if (updates === 0) {
if (totalUpdates === 0) {
doPoll()
return
}
// Reset any previous error states
updateFailed = false
lastUpdateError = ""
updateInProgress = true
Quickshell.execDetached(["pkexec", "pacman", "-Syu", "--noconfirm"])
capturedErrorText = ""
capturedSuccessText = ""
// Refresh after updates with multiple attempts
refreshAfterUpdate()
const terminal = Quickshell.env("TERMINAL")
if (!terminal) {
updateInProgress = false
updateFailed = true
lastUpdateError = "TERMINAL environment variable not set"
ToastService.showWarning("ArchUpdater", "TERMINAL environment variable not set")
return
}
// Check if we have an AUR helper for full system update
if (cachedAurHelper !== "" && (aurUpdates > 0 || updates > 0)) {
// Use AUR helper for full system update (handles both repo and AUR)
const command = generateUpdateCommand(cachedAurHelper + " -Syu")
Quickshell.execDetached([terminal, "-e", "bash", "-c", command])
} else if (cachedAurHelper === "") {
// No AUR helper found
updateInProgress = false
updateFailed = true
lastUpdateError = "No AUR helper found (yay or paru not installed)"
Logger.warn("ArchUpdater", "No AUR helper found for update")
}
// Start monitoring and timeout timers
refreshTimer.start()
updateCompleteTimer.start()
updateMonitorTimer.start()
}
// Update selected packages
@@ -79,17 +452,204 @@ Singleton {
if (selectedPackages.length === 0)
return
// Reset any previous error states
updateFailed = false
lastUpdateError = ""
updateInProgress = true
const command = ["pkexec", "pacman", "-S", "--noconfirm"].concat(selectedPackages)
Quickshell.execDetached(command)
capturedErrorText = ""
capturedSuccessText = ""
// Clear selection and refresh
selectedPackages = []
selectedPackagesCount = 0
refreshAfterUpdate()
const terminal = Quickshell.env("TERMINAL")
if (!terminal) {
updateInProgress = false
updateFailed = true
lastUpdateError = "TERMINAL environment variable not set"
ToastService.showWarning("ArchUpdater", "TERMINAL environment variable not set")
return
}
// Update all packages with AUR helper (handles both repo and AUR)
if (selectedPackages.length > 0) {
if (cachedAurHelper !== "") {
const packageList = selectedPackages.join(" ")
// Handle ghostty terminal differently due to command parsing issues
if (terminal.includes("ghostty")) {
const simpleCommand = cachedAurHelper + " -S " + packageList
Quickshell.execDetached([terminal, "-e", simpleCommand])
} else {
const command = generateUpdateCommand(cachedAurHelper + " -S " + packageList)
Quickshell.execDetached([terminal, "-e", "bash", "-c", command])
}
} else {
updateInProgress = false
updateFailed = true
lastUpdateError = "No AUR helper found (yay or paru not installed)"
Logger.warn("ArchUpdater", "No AUR helper found for packages:", selectedPackages.join(", "))
}
}
// Start monitoring and timeout timers
refreshTimer.start()
updateCompleteTimer.start()
updateMonitorTimer.start()
}
// Package selection functions
// Reset update state (useful for manual recovery)
function resetUpdateState() {
// Clear all update states
updateInProgress = false
updateFailed = false
lastUpdateError = ""
checkFailed = false
lastCheckError = ""
updateCompleteTimer.stop()
updateMonitorTimer.stop()
refreshTimer.stop()
errorCheckTimer.stop()
successCheckTimer.stop()
// Refresh to get current state
doPoll()
}
// Manual refresh function (bypasses cooldown)
function forceRefresh() {
// Prevent multiple simultaneous refreshes
if (aurBusy) {
return
}
// Clear error states when refreshing
updateFailed = false
lastUpdateError = ""
checkFailed = false
lastCheckError = ""
// Check if we have a cached AUR helper
if (cachedAurHelper !== "") {
// Clear error state when helper is available
if (checkFailed && lastCheckError.includes("No AUR helper found")) {
checkFailed = false
lastCheckError = ""
}
checkAurUpdatesProcess.command = [cachedAurHelper, "-Qu"]
checkAurOnlyProcess.command = [cachedAurHelper, getAurOnlyFlag()]
// Force refresh by bypassing cooldown
checkAurUpdatesProcess.running = true
lastPollTime = Date.now()
} else {
// AUR helper detection is still in progress or failed
// Try to detect again if not already in progress
if (!yayCheckProcess.running && !paruCheckProcess.running) {
getAurHelper()
}
Logger.warn("ArchUpdater", "AUR helper detection in progress or failed")
}
}
// ============================================================================
// UTILITY PROCESSES
// ============================================================================
// Process for checking yay availability
Process {
id: yayCheckProcess
command: ["which", "yay"]
onExited: function (exitCode) {
if (exitCode === 0) {
cachedAurHelper = "yay"
Logger.log("ArchUpdater", "Found yay AUR helper (preferred)")
// Clear error state when helper is found
if (checkFailed && lastCheckError.includes("No AUR helper found")) {
checkFailed = false
lastCheckError = ""
}
// Trigger initial check when helper is found
triggerInitialCheck()
}
}
}
// Process for checking paru availability
Process {
id: paruCheckProcess
command: ["which", "paru"]
onExited: function (exitCode) {
if (exitCode === 0) {
// Only use paru if yay wasn't found (yay is preferred)
if (cachedAurHelper === "") {
cachedAurHelper = "paru"
Logger.log("ArchUpdater", "Found paru AUR helper")
// Clear error state when helper is found
if (checkFailed && lastCheckError.includes("No AUR helper found")) {
checkFailed = false
lastCheckError = ""
}
// Trigger initial check when helper is found
triggerInitialCheck()
} else {
Logger.log("ArchUpdater", "Found paru but using", cachedAurHelper, "(preferred)")
}
}
}
}
// Cached AUR helper detection
property string cachedAurHelper: ""
// Helper function to detect AUR helper
function getAurHelper() {
// Return cached result if available
if (cachedAurHelper !== "") {
return cachedAurHelper
}
// Check for AUR helpers using Process objects
Logger.log("ArchUpdater", "Detecting AUR helper...")
// Start the detection processes
yayCheckProcess.running = true
paruCheckProcess.running = true
// Return empty string to indicate no helper found yet
// The processes will update cachedAurHelper when they complete
return ""
}
// Helper function to get the correct AUR-only flag for the detected helper
function getAurOnlyFlag() {
if (cachedAurHelper === "yay") {
return "-Qua"
} else if (cachedAurHelper === "paru") {
return "-Qua" // paru uses the same flag but different exit code behavior
}
return "-Qua" // fallback
}
// Helper function to trigger the initial package check
function triggerInitialCheck() {
// Only trigger if this is the first time (no packages have been checked yet)
if (repoPackages.length === 0 && aurPackages.length === 0 && !aurBusy) {
// Clear any previous error state
checkFailed = false
lastCheckError = ""
// Wait a bit for the system to be ready before the first check
Qt.callLater(() => {
checkAurUpdatesProcess.command = [cachedAurHelper, "-Qu"]
checkAurOnlyProcess.command = [cachedAurHelper, getAurOnlyFlag()]
checkAurUpdatesProcess.running = true
lastPollTime = Date.now()
}, 1000)
}
}
// ============================================================================
// PACKAGE SELECTION FUNCTIONS
// ============================================================================
function togglePackageSelection(packageName) {
const index = selectedPackages.indexOf(packageName)
if (index > -1) {
@@ -101,7 +661,7 @@ Singleton {
}
function selectAllPackages() {
selectedPackages = updatePackages.map(pkg => pkg.name)
selectedPackages = [...repoPackages.map(pkg => pkg.name), ...aurPackages.map(pkg => pkg.name)]
selectedPackagesCount = selectedPackages.length
}
@@ -114,44 +674,32 @@ Singleton {
return selectedPackages.indexOf(packageName) > -1
}
// Robust refresh after updates
function refreshAfterUpdate() {
// First refresh attempt after 3 seconds
Qt.callLater(() => {
doPoll()
}, 3000)
// ============================================================================
// REFRESH FUNCTIONS
// ============================================================================
// Second refresh attempt after 8 seconds
Qt.callLater(() => {
doPoll()
}, 8000)
// Third refresh attempt after 15 seconds
Qt.callLater(() => {
doPoll()
updateInProgress = false
}, 15000)
// Final refresh attempt after 30 seconds
Qt.callLater(() => {
doPoll()
}, 30000)
}
// ============================================================================
// UTILITY FUNCTIONS
// ============================================================================
// Notification helper
function notify(title, body) {
Quickshell.execDetached(["notify-send", "-a", "UpdateService", "-i", "system-software-update", title, body])
}
// Auto-poll every 15 minutes
// ============================================================================
// AUTO-POLL TIMER
// ============================================================================
// Auto-poll every 15 minutes (respects cooldown)
Timer {
interval: 15 * 60 * 1000 // 15 minutes
repeat: true
running: true
onTriggered: doPoll()
onTriggered: {
if (!updateInProgress && canPoll) {
doPoll()
}
}
}
// Initial check
Component.onCompleted: doPoll()
}
*/

View File

@@ -35,6 +35,13 @@ Singleton {
readonly property alias muted: root._muted
property bool _muted: !!sink?.audio?.muted
// Input volume [0..1] is readonly from outside
readonly property alias inputVolume: root._inputVolume
property real _inputVolume: source?.audio?.volume ?? 0
readonly property alias inputMuted: root._inputMuted
property bool _inputMuted: !!source?.audio?.muted
readonly property real stepVolume: Settings.data.audio.volumeStep / 100.0
PwObjectTracker {
@@ -58,6 +65,23 @@ Singleton {
}
}
Connections {
target: source?.audio ? source?.audio : null
function onVolumeChanged() {
var vol = (source?.audio.volume ?? 0)
if (isNaN(vol)) {
vol = 0
}
root._inputVolume = vol
}
function onMutedChanged() {
root._inputMuted = (source?.audio.muted ?? true)
Logger.log("AudioService", "OnInputMuteChanged:", root._inputMuted)
}
}
function increaseVolume() {
setVolume(volume + stepVolume)
}
@@ -85,6 +109,24 @@ Singleton {
}
}
function setInputVolume(newVolume: real) {
if (source?.ready && source?.audio) {
// Clamp it accordingly
source.audio.muted = false
source.audio.volume = Math.max(0, Math.min(1, newVolume))
} else {
Logger.warn("AudioService", "No source available")
}
}
function setInputMuted(muted: bool) {
if (source?.ready && source?.audio) {
source.audio.muted = muted
} else {
Logger.warn("AudioService", "No source available")
}
}
function setAudioSink(newSink: PwNode): void {
Pipewire.preferredDefaultAudioSink = newSink
}

View File

@@ -2,6 +2,7 @@ pragma Singleton
import QtQuick
import Quickshell
import qs.Commons
import qs.Modules.Bar.Widgets
Singleton {
@@ -10,18 +11,21 @@ Singleton {
// Widget registry object mapping widget names to components
property var widgets: ({
"ActiveWindow": activeWindowComponent,
"Battery"// "ArchUpdater": archUpdaterComponent,
: batteryComponent,
"ArchUpdater": archUpdaterComponent,
"Battery": batteryComponent,
"Bluetooth": bluetoothComponent,
"Brightness": brightnessComponent,
"Clock": clockComponent,
"KeyboardLayout": keyboardLayoutComponent,
"MediaMini": mediaMiniComponent,
"Microphone": microphoneComponent,
"NightLight": nightLightComponent,
"NotificationHistory": notificationHistoryComponent,
"PowerProfile": powerProfileComponent,
"ScreenRecorderIndicator": screenRecorderIndicatorComponent,
"SidePanelToggle": sidePanelToggleComponent,
"SystemMonitor": systemMonitorComponent,
"Taskbar": taskbarComponent,
"Tray": trayComponent,
"Volume": volumeComponent,
"WiFi": wiFiComponent,
@@ -32,9 +36,9 @@ Singleton {
property Component activeWindowComponent: Component {
ActiveWindow {}
}
// property Component archUpdaterComponent: Component {
// ArchUpdater {}
// }
property Component archUpdaterComponent: Component {
ArchUpdater {}
}
property Component batteryComponent: Component {
Battery {}
}
@@ -53,6 +57,12 @@ Singleton {
property Component mediaMiniComponent: Component {
MediaMini {}
}
property Component microphoneComponent: Component {
Microphone {}
}
property Component nightLightComponent: Component {
NightLight {}
}
property Component notificationHistoryComponent: Component {
NotificationHistory {}
}
@@ -80,6 +90,9 @@ Singleton {
property Component workspaceComponent: Component {
Workspace {}
}
property Component taskbarComponent: Component {
Taskbar {}
}
// ------------------------------
// Helper function to get widget component by name
@@ -96,4 +109,24 @@ Singleton {
function getAvailableWidgets() {
return Object.keys(widgets)
}
function getNPillDirection(widget) {
try {
if (widget.barSection === "leftSection") {
return true
} else if (widget.barSection === "rightSection") {
return false
} else {
// middle section
if (widget.sectionWidgetIndex < widget.sectionWidgetsCount / 2) {
return false
} else {
return true
}
}
} catch (e) {
Logger.error(e)
}
return false
}
}

View File

@@ -3,6 +3,7 @@ pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Bluetooth
import qs.Commons
Singleton {
id: root
@@ -13,17 +14,17 @@ Singleton {
readonly property bool discovering: (adapter && adapter.discovering) ?? false
readonly property var devices: adapter ? adapter.devices : null
readonly property var pairedDevices: {
if (!adapter || !adapter.devices)
return []
if (!adapter || !adapter.devices) {
return []
}
return adapter.devices.values.filter(dev => {
return dev && (dev.paired || dev.trusted)
})
}
readonly property var allDevicesWithBattery: {
if (!adapter || !adapter.devices)
return []
if (!adapter || !adapter.devices) {
return []
}
return adapter.devices.values.filter(dev => {
return dev && dev.batteryAvailable && dev.battery > 0
})
@@ -49,34 +50,36 @@ Singleton {
}
function getDeviceIcon(device) {
if (!device)
if (!device) {
return "bluetooth"
}
var name = (device.name || device.deviceName || "").toLowerCase()
var icon = (device.icon || "").toLowerCase()
if (icon.includes("headset") || icon.includes("audio") || name.includes("headphone") || name.includes("airpod")
|| name.includes("headset") || name.includes("arctis"))
|| name.includes("headset") || name.includes("arctis")) {
return "headset"
}
if (icon.includes("mouse") || name.includes("mouse"))
if (icon.includes("mouse") || name.includes("mouse")) {
return "mouse"
if (icon.includes("keyboard") || name.includes("keyboard"))
}
if (icon.includes("keyboard") || name.includes("keyboard")) {
return "keyboard"
}
if (icon.includes("phone") || name.includes("phone") || name.includes("iphone") || name.includes("android")
|| name.includes("samsung"))
|| name.includes("samsung")) {
return "smartphone"
if (icon.includes("watch") || name.includes("watch"))
}
if (icon.includes("watch") || name.includes("watch")) {
return "watch"
if (icon.includes("speaker") || name.includes("speaker"))
}
if (icon.includes("speaker") || name.includes("speaker")) {
return "speaker"
if (icon.includes("display") || name.includes("tv"))
}
if (icon.includes("display") || name.includes("tv")) {
return "tv"
}
return "bluetooth"
}
@@ -84,67 +87,114 @@ Singleton {
if (!device)
return false
return !device.paired && !device.pairing && !device.blocked
/*
Paired
Means youve successfully exchanged keys with the device.
The devices remember each other and can authenticate without repeating the pairing process.
Example: once your headphones are paired, you dont need to type a PIN every time.
Hence, instead of !device.paired, should be device.connected
*/
return !device.connected && !device.pairing && !device.blocked
}
function canDisconnect(device) {
if (!device)
return false
return device.connected && !device.pairing && !device.blocked
}
function getSignalStrength(device) {
if (!device || device.signalStrength === undefined || device.signalStrength <= 0)
return "Unknown"
if (device.pairing) {
return "Pairing..."
}
if (device.blocked) {
return "Blocked"
}
if (!device || device.signalStrength === undefined || device.signalStrength <= 0) {
return "Signal: Unknown"
}
var signal = device.signalStrength
if (signal >= 80)
return "Excellent"
if (signal >= 80) {
return "Signal: Excellent"
}
if (signal >= 60) {
return "Signal: Good"
}
if (signal >= 40) {
return "Signal: Fair"
}
if (signal >= 20) {
return "Signal: Poor"
}
return "Signal: Very Poor"
}
if (signal >= 60)
return "Good"
if (signal >= 40)
return "Fair"
if (signal >= 20)
return "Poor"
return "Very Poor"
function getBattery(device) {
return `Battery: ${Math.round(device.battery * 100)}%`
}
function getSignalIcon(device) {
if (!device || device.signalStrength === undefined || device.signalStrength <= 0)
if (!device || device.signalStrength === undefined || device.signalStrength <= 0) {
return "signal_cellular_null"
}
var signal = device.signalStrength
if (signal >= 80)
if (signal >= 80) {
return "signal_cellular_4_bar"
if (signal >= 60)
}
if (signal >= 60) {
return "signal_cellular_3_bar"
if (signal >= 40)
}
if (signal >= 40) {
return "signal_cellular_2_bar"
if (signal >= 20)
}
if (signal >= 20) {
return "signal_cellular_1_bar"
}
return "signal_cellular_0_bar"
}
function isDeviceBusy(device) {
if (!device)
if (!device) {
return false
}
return device.pairing || device.state === BluetoothDeviceState.Disconnecting
|| device.state === BluetoothDeviceState.Connecting
}
function connectDeviceWithTrust(device) {
if (!device)
if (!device) {
return
}
device.trusted = true
device.connect()
}
function disconnectDevice(device) {
if (!device) {
return
}
device.disconnect()
}
function forgetDevice(device) {
if (!device) {
return
}
device.trusted = false
device.forget()
}
function setBluetoothEnabled(enabled) {
if (!adapter) {
console.warn("BluetoothService: No adapter available")
Logger.warn("Bluetooth", "No adapter available")
return
}

View File

@@ -69,19 +69,30 @@ Singleton {
// Detect DDC monitors
Process {
id: ddcProc
command: ["ddcutil", "detect", "--brief"]
property list<var> ddcMonitors: []
command: ["ddcutil", "detect", "--sleep-multiplier=0.5"]
stdout: StdioCollector {
onStreamFinished: {
// Do not filter out invalid displays. For some reason --brief returns some invalid which works fine
var displays = text.trim().split("\n\n")
root.ddcMonitors = displays.map(d => {
var modelMatch = d.match(/Monitor:.*:(.*):.*/)
var busMatch = d.match(/I2C bus:[ ]*\/dev\/i2c-([0-9]+)/)
return {
"model": modelMatch ? modelMatch[1] : "",
"busNum": busMatch ? busMatch[1] : ""
}
})
ddcProc.ddcMonitors = displays.map(d => {
var ddcModelMatc = d.match(/This monitor does not support DDC\/CI/)
var modelMatch = d.match(/Model:\s*(.*)/)
var busMatch = d.match(/I2C bus:[ ]*\/dev\/i2c-([0-9]+)/)
var ddcModel = ddcModelMatc ? ddcModelMatc.length > 0 : false
var model = modelMatch ? modelMatch[1] : "Unknown"
var bus = busMatch ? busMatch[1] : "Unknown"
Logger.log("Detected DDC Monitor:", model, "on bus", bus, "is DDC:",
!ddcModel)
return {
"model": model,
"busNum": bus,
"isDdc": !ddcModel
}
})
root.ddcMonitors = ddcProc.ddcMonitors.filter(m => m.isDdc)
}
}
}

View File

@@ -13,7 +13,7 @@ Singleton {
property var items: [] // [{id, preview, mime, isImage}]
property bool loading: false
// Active only when feature is enabled and settings have finished initial load
property bool active: Settings.data.appLauncher.enableClipboardHistory && !Settings.isInitialLoad
property bool active: Settings.data.appLauncher.enableClipboardHistory && Settings.isLoaded
// Optional automatic watchers to feed cliphist DB
property bool autoWatch: true

View File

@@ -5,6 +5,7 @@ import Qt.labs.folderlistmodel
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services
Singleton {
id: root
@@ -34,15 +35,6 @@ Singleton {
schemeReader.path = filePath
}
function changedWallpaper() {
if (Settings.data.colorSchemes.useWallpaperColors) {
Logger.log("ColorScheme", "Starting color generation from wallpaper")
generateColorsProcess.running = true
// Invalidate potential predefined scheme
Settings.data.colorSchemes.predefinedScheme = ""
}
}
FolderListModel {
id: folderModel
nameFilters: ["*.json"]
@@ -136,36 +128,4 @@ Singleton {
colorsWriter.path = colorsJsonFilePath
colorsWriter.writeAdapter()
}
Process {
id: generateColorsProcess
command: {
// Choose config based on external theming toggles
var cfg = Quickshell.shellDir + "/Assets/Matugen/matugen.toml"
if (!Settings.data.colorSchemes.themeApps) {
cfg = Quickshell.shellDir + "/Assets/Matugen/matugen.base.toml"
}
var cmd = ["matugen", "image", WallpaperService.currentWallpaper, "--config", cfg]
if (!Settings.data.colorSchemes.darkMode) {
cmd.push("--mode", "light")
} else {
cmd.push("--mode", "dark")
}
return cmd
}
workingDirectory: Quickshell.shellDir
running: false
stdout: StdioCollector {
onStreamFinished: {
Logger.log("ColorScheme", "Completed colors generation")
}
}
stderr: StdioCollector {
onStreamFinished: {
if (this.text !== "") {
Logger.error(this.text)
}
}
}
}
}

View File

@@ -166,10 +166,35 @@ Singleton {
for (var i = 0; i < hlToplevels.length; i++) {
const toplevel = hlToplevels[i]
// Try to get appId from various sources
let appId = ""
// First try the direct properties
if (toplevel.class) {
appId = toplevel.class
} else if (toplevel.initialClass) {
appId = toplevel.initialClass
} else if (toplevel.appId) {
appId = toplevel.appId
}
// If still no appId, try to get it from the lastIpcObject
if (!appId && toplevel.lastIpcObject) {
try {
const ipcData = toplevel.lastIpcObject
// Try different possible property names for the application identifier
appId = ipcData.class || ipcData.initialClass || ipcData.appId || ipcData.wm_class || ""
} catch (e) {
// Ignore errors when accessing lastIpcObject
}
}
windowsList.push({
"id": toplevel.address || "",
"title": toplevel.title || "",
"appId": toplevel.class || toplevel.initialClass || "",
"appId": appId,
"workspaceId": toplevel.workspace?.id || null,
"isFocused": toplevel.activated === true
})

View File

@@ -13,7 +13,7 @@ Singleton {
property string githubDataFile: Quickshell.env("NOCTALIA_GITHUB_FILE") || (Settings.cacheDir + "github.json")
property int githubUpdateFrequency: 60 * 60 // 1 hour expressed in seconds
property bool isFetchingData: false
property alias data: adapter // Used to access via GitHubService.data.xxx.yyy
readonly property alias data: adapter // Used to access via GitHubService.data.xxx.yyy
// Public properties for easy access
property string latestVersion: "Unknown"

View File

@@ -6,19 +6,36 @@ import Quickshell.Io
import qs.Commons
import qs.Services
// Weather logic and caching
// Weather logic and caching with stable UI properties
Singleton {
id: root
property string locationFile: Quickshell.env("NOCTALIA_WEATHER_FILE") || (Settings.cacheDir + "location.json")
property int weatherUpdateFrequency: 30 * 60 // 30 minutes expressed in seconds
property bool isFetchingWeather: false
property alias data: adapter // Used to access via LocationService.data.xxx
readonly property alias data: adapter // Used to access via LocationService.data.xxx from outside, best to use "adapter" inside the service.
// Stable UI properties - only updated when location is fully resolved
property bool coordinatesReady: false
property string stableLatitude: ""
property string stableLongitude: ""
property string stableName: ""
FileView {
id: locationFileView
path: locationFile
onAdapterUpdated: writeAdapter()
onAdapterUpdated: saveTimer.start()
onLoaded: {
Logger.log("Location", "Loaded cached data")
// Initialize stable properties on load
if (adapter.latitude !== "" && adapter.longitude !== "" && adapter.weatherLastFetch > 0) {
root.stableLatitude = adapter.latitude
root.stableLongitude = adapter.longitude
root.stableName = adapter.name
root.coordinatesReady = true
Logger.log("Location", "Coordinates ready")
}
updateWeather()
}
onLoadFailed: function (error) {
@@ -28,6 +45,7 @@ Singleton {
JsonAdapter {
id: adapter
// Core data properties
property string latitude: ""
property string longitude: ""
property string name: ""
@@ -36,6 +54,16 @@ Singleton {
}
}
// Helper property for UI components (outside JsonAdapter to avoid binding loops)
readonly property string displayCoordinates: {
if (!root.coordinatesReady || root.stableLatitude === "" || root.stableLongitude === "") {
return ""
}
const lat = parseFloat(root.stableLatitude).toFixed(4)
const lon = parseFloat(root.stableLongitude).toFixed(4)
return `${lat}, ${lon}`
}
// Every 20s check if we need to fetch new weather
Timer {
id: updateTimer
@@ -47,6 +75,13 @@ Singleton {
}
}
Timer {
id: saveTimer
running: false
interval: 1000
onTriggered: locationFileView.writeAdapter()
}
// --------------------------------
function init() {
// does nothing but ensure the singleton is created
@@ -58,11 +93,19 @@ Singleton {
function resetWeather() {
Logger.log("Location", "Resetting weather data")
data.latitude = ""
data.longitude = ""
data.name = ""
data.weatherLastFetch = 0
data.weather = null
// Mark as changing to prevent UI updates
root.coordinatesReady = false
// Reset stable properties
root.stableLatitude = ""
root.stableLongitude = ""
root.stableName = ""
// Reset core data
adapter.latitude = ""
adapter.longitude = ""
adapter.name = ""
adapter.weatherLastFetch = 0
adapter.weather = null
// Try to fetch immediately
updateWeather()
@@ -75,13 +118,9 @@ Singleton {
return
}
if (data.latitude === "") {
Logger.warn("Location", "Why is my latitude empty")
}
if ((data.weatherLastFetch === "") || (data.weather === null) || (data.latitude === "") || (data.longitude === "")
|| (data.name !== Settings.data.location.name)
|| (Time.timestamp >= data.weatherLastFetch + weatherUpdateFrequency)) {
if ((adapter.weatherLastFetch === "") || (adapter.weather === null) || (adapter.latitude === "")
|| (adapter.longitude === "") || (adapter.name !== Settings.data.location.name)
|| (Time.timestamp >= adapter.weatherLastFetch + weatherUpdateFrequency)) {
getFreshWeather()
}
}
@@ -89,22 +128,32 @@ Singleton {
// --------------------------------
function getFreshWeather() {
isFetchingWeather = true
if ((data.latitude === "") || (data.longitude === "") || (data.name !== Settings.data.location.name)) {
_geocodeLocation(Settings.data.location.name, function (latitude, longitude) {
// Check if location name has changed
const locationChanged = data.name !== Settings.data.location.name
if (locationChanged) {
root.coordinatesReady = false
Logger.log("Location", "Location changed from", adapter.name, "to", Settings.data.location.name)
}
if ((adapter.latitude === "") || (adapter.longitude === "") || locationChanged) {
_geocodeLocation(Settings.data.location.name, function (latitude, longitude, name, country) {
Logger.log("Location", "Geocoded", Settings.data.location.name, "to:", latitude, "/", longitude)
// Save location name
data.name = Settings.data.location.name
adapter.name = Settings.data.location.name
// Save GPS coordinates
data.latitude = latitude.toString()
data.longitude = longitude.toString()
adapter.latitude = latitude.toString()
adapter.longitude = longitude.toString()
root.stableName = `${name}, ${country}`
_fetchWeather(latitude, longitude, errorCallback)
}, errorCallback)
} else {
_fetchWeather(data.latitude, data.longitude, errorCallback)
_fetchWeather(adapter.latitude, adapter.longitude, errorCallback)
}
}
@@ -119,9 +168,8 @@ Singleton {
if (xhr.status === 200) {
try {
var geoData = JSON.parse(xhr.responseText)
// Logger.logJSON.stringify(geoData))
if (geoData.lat != null) {
callback(geoData.lat, geoData.lng)
callback(geoData.lat, geoData.lng, geoData.name, geoData.country)
} else {
errorCallback("Location", "could not resolve location name")
}
@@ -148,15 +196,19 @@ Singleton {
if (xhr.status === 200) {
try {
var weatherData = JSON.parse(xhr.responseText)
//console.log(JSON.stringify(weatherData))
// Save data
// Save core data
data.weather = weatherData
data.weatherLastFetch = Time.timestamp
data.latitude = weatherData.latitude.toString()
data.longitude = weatherData.longitude.toString()
// Update stable display values only when complete and successful
root.stableLatitude = data.latitude = weatherData.latitude.toString()
root.stableLongitude = data.longitude = weatherData.longitude.toString()
root.coordinatesReady = true
isFetchingWeather = false
Logger.log("Location", "Cached weather to disk")
Logger.log("Location", "Cached weather to disk - stable coordinates updated")
} catch (e) {
errorCallback("Location", "Failed to parse weather data")
}

View File

@@ -0,0 +1,79 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Assets.Matugen
import qs.Services
Singleton {
id: root
property string dynamicConfigPath: Settings.cacheDir + "matugen.dynamic.toml"
// External state management
Connections {
target: WallpaperService
function onWallpaperChanged(screenName, path) {
// Only detect changes on main screen
if (screenName === Screen.name && Settings.data.colorSchemes.useWallpaperColors) {
generateFromWallpaper()
}
}
}
// Build TOML content based on settings
function buildConfigToml() {
return Matugen.buildConfigToml()
}
// Generate colors using current wallpaper and settings
function generateFromWallpaper() {
// Ensure cache dir exists
Quickshell.execDetached(["mkdir", "-p", Settings.cacheDir])
Logger.log("Matugen", "Generating from wallpaper on screen:", Screen.name)
var wp = WallpaperService.getWallpaper(Screen.name).replace(/'/g, "'\\''")
var content = buildConfigToml()
var mode = Settings.data.colorSchemes.darkMode ? "dark" : "light"
var pathEsc = dynamicConfigPath.replace(/'/g, "'\\''")
var extraRepo = (Quickshell.shellDir + "/Assets/Matugen/extra").replace(/'/g, "'\\''")
var extraUser = (Settings.configDir + "matugen.d").replace(/'/g, "'\\''")
// Build the main script
var script = "cat > '" + pathEsc + "' << 'EOF'\n" + content + "EOF\n" + "for d in '" + extraRepo + "' '" + extraUser
+ "'; do\n" + " if [ -d \"$d\" ]; then\n"
+ " for f in \"$d\"/*.toml; do\n" + " [ -f \"$f\" ] && { echo; echo \"# extra: $f\"; cat \"$f\"; } >> '"
+ pathEsc + "'\n" + " done\n" + " fi\n" + "done\n" + "matugen image '" + wp + "' --config '" + pathEsc + "' --mode " + mode
// Add user config execution if enabled
if (Settings.data.matugen.enableUserTemplates) {
var userConfigDir = (Quickshell.env("HOME") + "/.config/matugen/").replace(/'/g, "'\\''")
script += "\n# Execute user config if it exists\nif [ -f '" + userConfigDir + "config.toml' ]; then\n"
script += " matugen image '" + wp + "' --config '" + userConfigDir + "config.toml' --mode " + mode + "\n"
script += "fi"
}
script += "\n"
generateProcess.command = ["bash", "-lc", script]
generateProcess.running = true
}
Process {
id: generateProcess
workingDirectory: Quickshell.shellDir
running: false
stderr: StdioCollector {
onStreamFinished: {
if (this.text !== "") {
Logger.warn("MatugenService", "Matugen stderr:", this.text)
}
}
}
}
// No separate writer; the write happens inline via bash heredoc
}

View File

@@ -11,8 +11,10 @@ Singleton {
property var currentPlayer: null
property real currentPosition: 0
property bool isSeeking: false
property int selectedPlayerIndex: 0
property bool isPlaying: currentPlayer ? currentPlayer.isPlaying : false
property bool isPlaying: currentPlayer ? (currentPlayer.playbackState === MprisPlaybackState.Playing
|| currentPlayer.isPlaying) : false
property string trackTitle: currentPlayer ? (currentPlayer.trackTitle || "") : ""
property string trackArtist: currentPlayer ? (currentPlayer.trackArtist || "") : ""
property string trackAlbum: currentPlayer ? (currentPlayer.trackAlbum || "") : ""
@@ -37,11 +39,26 @@ Singleton {
let allPlayers = Mpris.players.values
let controllablePlayers = []
// Apply blacklist and controllable filter
const blacklist = (Settings.data.audio
&& Settings.data.audio.mprisBlacklist) ? Settings.data.audio.mprisBlacklist : []
for (var i = 0; i < allPlayers.length; i++) {
let player = allPlayers[i]
if (player && player.canControl) {
if (!player)
continue
const identity = String(player.identity || "")
const busName = String(player.busName || "")
const desktop = String(player.desktopEntry || "")
const idKey = identity.toLowerCase()
const match = blacklist.find(b => {
const s = String(b || "").toLowerCase()
return s && (idKey.includes(s) || busName.toLowerCase().includes(s)
|| desktop.toLowerCase().includes(s))
})
if (match)
continue
if (player.canControl)
controllablePlayers.push(player)
}
}
return controllablePlayers
@@ -54,6 +71,22 @@ Singleton {
return null
}
// Preferred player logic (preferred > fallback)
const preferred = (Settings.data.audio.preferredPlayer || "")
if (preferred !== "") {
for (var i = 0; i < availablePlayers.length; i++) {
const p = availablePlayers[i]
const identity = String(p.identity || "").toLowerCase()
const busName = String(p.busName || "").toLowerCase()
const desktop = String(p.desktopEntry || "").toLowerCase()
const pref = preferred.toLowerCase()
if (identity.includes(pref) || busName.includes(pref) || desktop.includes(pref)) {
selectedPlayerIndex = i
return p
}
}
}
if (selectedPlayerIndex < availablePlayers.length) {
return availablePlayers[selectedPlayerIndex]
} else {
@@ -126,11 +159,12 @@ Singleton {
Timer {
id: positionTimer
interval: 1000
running: currentPlayer && currentPlayer.isPlaying && currentPlayer.length > 0
running: currentPlayer && !root.isSeeking && currentPlayer.isPlaying && currentPlayer.length > 0
&& currentPlayer.playbackState === MprisPlaybackState.Playing
repeat: true
onTriggered: {
if (currentPlayer && currentPlayer.isPlaying && currentPlayer.playbackState === MprisPlaybackState.Playing) {
if (currentPlayer && !root.isSeeking && currentPlayer.isPlaying
&& currentPlayer.playbackState === MprisPlaybackState.Playing) {
currentPosition = currentPlayer.position
} else {
running = false
@@ -138,6 +172,21 @@ Singleton {
}
}
// Avoid overwriting currentPosition while seeking due to backend position changes
Connections {
target: currentPlayer
function onPositionChanged() {
if (!root.isSeeking && currentPlayer) {
currentPosition = currentPlayer.position
}
}
function onPlaybackStateChanged() {
if (!root.isSeeking && currentPlayer) {
currentPosition = currentPlayer.position
}
}
}
// Reset position when switching to inactive player
onCurrentPlayerChanged: {
if (!currentPlayer || !currentPlayer.isPlaying || currentPlayer.playbackState !== MprisPlaybackState.Playing) {

View File

@@ -0,0 +1,82 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services
Singleton {
id: root
// Night Light properties - directly bound to settings
readonly property var params: Settings.data.nightLight
property var lastCommand: []
function apply() {
// If using LocationService, wait for it to be ready
if (params.autoSchedule && !LocationService.coordinatesReady) {
return
}
var command = buildCommand()
// Compare with previous command to avoid unecessary restart
if (JSON.stringify(command) !== JSON.stringify(lastCommand)) {
lastCommand = command
runner.command = command
// Set running to false so it may restarts below if still enabled
runner.running = false
}
runner.running = params.enabled
}
function buildCommand() {
var cmd = ["wlsunset"]
cmd.push("-t", `${params.nightTemp}`, "-T", `${params.dayTemp}`)
if (params.autoSchedule) {
cmd.push("-l", `${LocationService.stableLatitude}`, "-L", `${LocationService.stableLongitude}`)
} else {
cmd.push("-S", params.manualSunrise)
cmd.push("-s", params.manualSunset)
}
cmd.push("-d", 60 * 15) // 15min progressive fade at sunset/sunrise
return cmd
}
// Observe setting changes and location readiness
Connections {
target: Settings.data.nightLight
function onEnabledChanged() {
apply()
}
function onNightTempChanged() {
apply()
}
function onDayTempChanged() {
apply()
}
}
Connections {
target: LocationService
function onCoordinatesReadyChanged() {
if (LocationService.coordinatesReady) {
apply()
}
}
}
// Foreground process runner
Process {
id: runner
running: false
onStarted: {
Logger.log("NightLight", "Wlsunset started:", runner.command)
}
onExited: function (code, status) {
Logger.log("NightLight", "Wlsunset exited:", code, status)
}
}
}

View File

@@ -1,17 +1,43 @@
pragma Singleton
import QtQuick
import Quickshell
import qs.Commons
Singleton {
id: root
// Cache for current scales - updated via signals
property var currentScales: ({})
// Signal emitted when scale changes
signal scaleChanged(string screenName, real scale)
Component.onCompleted: {
Logger.log("Scaling", "Service started")
}
Connections {
target: Settings
function onSettingsLoaded() {
// Initialize cache from Settings once they are loaded on startup
var monitors = Settings.data.ui.monitorsScaling || []
for (var i = 0; i < monitors.length; i++) {
if (monitors[i].name && monitors[i].scale !== undefined) {
currentScales[monitors[i].name] = monitors[i].scale
root.scaleChanged(monitors[i].name, monitors[i].scale)
Logger.log("Scaling", "Caching scaling for", monitors[i].name, ":", monitors[i].scale)
}
}
}
}
// -------------------------------------------
// Manual scaling via Settings
function scale(aScreen) {
function getScreenScale(aScreen) {
try {
if (aScreen !== undefined && aScreen.name !== undefined) {
return scaleByName(aScreen.name)
return getScreenScaleByName(aScreen.name)
}
} catch (e) {
@@ -20,28 +46,85 @@ Singleton {
return 1.0
}
function scaleByName(aScreenName) {
// -------------------------------------------
// Get scale from cache for better performance
function getScreenScaleByName(aScreenName) {
try {
if (Settings.data.monitorsScaling !== undefined) {
if (Settings.data.monitorsScaling[aScreenName] !== undefined) {
return Settings.data.monitorsScaling[aScreenName]
}
var scale = currentScales[aScreenName]
if ((scale !== undefined) && (scale != null)) {
return scale
}
} catch (e) {
//Logger.warn(e)
}
return 1.0
}
// -------------------------------------------
function setScreenScale(aScreen, scale) {
try {
if (aScreen !== undefined && aScreen.name !== undefined) {
return setScreenScaleByName(aScreen.name, scale)
}
} catch (e) {
//Logger.warn(e)
}
}
// -------------------------------------------
function setScreenScaleByName(aScreenName, scale) {
try {
// Check if scale actually changed
var oldScale = currentScales[aScreenName] || 1.0
if (oldScale === scale) {
return
// No change needed
}
// Update cache directly
currentScales[aScreenName] = scale
// Update Settings with immutable update for proper persistence
var monitors = Settings.data.ui.monitorsScaling || []
var found = false
var newMonitors = monitors.map(function (monitor) {
if (monitor.name === aScreenName) {
found = true
return {
"name": aScreenName,
"scale": scale
}
}
return monitor
})
if (!found) {
newMonitors.push({
"name": aScreenName,
"scale": scale
})
}
// Use slice() to ensure Settings detects the change
Settings.data.ui.monitorsScaling = newMonitors.slice()
// Emit signal for components to react
root.scaleChanged(aScreenName, scale)
Logger.log("Scaling", "Scale changed for", aScreenName, "to", scale)
} catch (e) {
Logger.warn("Scaling", "Error setting scale:", e)
}
}
// -------------------------------------------
// Dynamic scaling based on resolution
// Design reference resolution (for scale = 1.0)
readonly property int designScreenWidth: 2560
readonly property int designScreenHeight: 1440
function dynamicScale(aScreen) {
if (aScreen != null) {
var ratioW = aScreen.width / designScreenWidth

View File

@@ -2,6 +2,7 @@ pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services
@@ -10,19 +11,20 @@ Singleton {
readonly property var settings: Settings.data.screenRecorder
property bool isRecording: false
property bool isPending: false
property string outputPath: ""
// Start or Stop recording
function toggleRecording() {
isRecording ? stopRecording() : startRecording()
(isRecording || isPending) ? stopRecording() : startRecording()
}
// Start screen recording using Quickshell.execDetached
function startRecording() {
if (isRecording) {
if (isRecording || isPending) {
return
}
isRecording = true
isPending = true
var filename = Time.getFormattedTimestamp() + ".mp4"
var videoDir = settings.directory
@@ -43,23 +45,79 @@ Singleton {
notify-send "gpu-screen-recorder not installed!" -u critical
fi`
//Logger.log("ScreenRecorder", command)
Quickshell.execDetached(["sh", "-c", command])
Logger.log("ScreenRecorder", "Started recording")
// Use Process instead of execDetached so we can monitor it
recorderProcess.exec({
"command": ["sh", "-c", command]
})
// Start monitoring - if process ends quickly, it was likely cancelled
pendingTimer.running = true
}
// Stop recording using Quickshell.execDetached
function stopRecording() {
if (!isRecording) {
if (!isRecording && !isPending) {
return
}
Quickshell.execDetached(["sh", "-c", "pkill -SIGINT -f 'gpu-screen-recorder'"])
Logger.log("ScreenRecorder", "Finished recording:", outputPath)
Quickshell.execDetached(
["sh", "-c", "pkill -SIGINT -f 'gpu-screen-recorder' || pkill -SIGINT -f 'com.dec05eba.gpu_screen_recorder'"])
isRecording = false
isPending = false
pendingTimer.running = false
monitorTimer.running = false
// Just in case, force kill after 3 seconds
killTimer.running = true
isRecording = false
}
// Process to run and monitor gpu-screen-recorder
Process {
id: recorderProcess
onExited: function (exitCode, exitStatus) {
if (isPending) {
// Process ended while we were pending - likely cancelled or error
isPending = false
pendingTimer.running = false
} else if (isRecording) {
// Process ended normally while recording
isRecording = false
monitorTimer.running = false
}
}
}
Timer {
id: pendingTimer
interval: 2000 // Wait 2 seconds to see if process stays alive
running: false
repeat: false
onTriggered: {
if (isPending && recorderProcess.running) {
// Process is still running after 2 seconds - assume recording started successfully
isPending = false
isRecording = true
monitorTimer.running = true
} else if (isPending) {
// Process not running anymore - was cancelled or failed
isPending = false
}
}
}
// Monitor timer to periodically check if we're still recording
Timer {
id: monitorTimer
interval: 2000
running: false
repeat: true
onTriggered: {
if (!recorderProcess.running && isRecording) {
isRecording = false
running = false
}
}
}
Timer {
@@ -68,7 +126,8 @@ Singleton {
running: false
repeat: false
onTriggered: {
Quickshell.execDetached(["sh", "-c", "pkill -9 -f 'gpu-screen-recorder' 2>/dev/null || true"])
Quickshell.execDetached(
["sh", "-c", "pkill -9 -f 'gpu-screen-recorder' 2>/dev/null || pkill -9 -f 'com.dec05eba.gpu_screen_recorder' 2>/dev/null || true"])
}
}
}

View File

@@ -14,6 +14,21 @@ Singleton {
property real memoryUsageGb: 0
property real memoryUsagePer: 0
property real diskUsage: 0
property real rxSpeed: 0
property real txSpeed: 0
// Helper function to format network speeds
function formatSpeed(bytesPerSecond) {
if (bytesPerSecond < 1024) {
return bytesPerSecond.toFixed(0) + "B/s"
} else if (bytesPerSecond < 1024 * 1024) {
return (bytesPerSecond / 1024).toFixed(1) + "KB/s"
} else if (bytesPerSecond < 1024 * 1024 * 1024) {
return (bytesPerSecond / (1024 * 1024)).toFixed(1) + "MB/s"
} else {
return (bytesPerSecond / (1024 * 1024 * 1024)).toFixed(1) + "GB/s"
}
}
// Background process emitting one JSON line per sample
Process {
@@ -29,6 +44,8 @@ Singleton {
root.memoryUsageGb = data.memgb
root.memoryUsagePer = data.memper
root.diskUsage = data.diskper
root.rxSpeed = parseFloat(data.rx_speed) || 0
root.txSpeed = parseFloat(data.tx_speed) || 0
} catch (e) {
// ignore malformed lines

View File

@@ -12,7 +12,7 @@ Singleton {
property var messageQueue: []
property bool isShowingToast: false
// Reference to all toast instances (set by ToastManager)
// Reference to all toast instances (set by ToastOverlay)
property var allToasts: []
// Properties for command checking
@@ -197,6 +197,7 @@ Singleton {
toast.type = toastData.type
toast.persistent = toastData.persistent
toast.duration = toastData.duration
toast.show()
}
}
@@ -236,8 +237,4 @@ Singleton {
}
}
}
Component.onCompleted: {
}
}

View File

@@ -10,164 +10,374 @@ Singleton {
id: root
Component.onCompleted: {
Logger.log("Wallpapers", "Service started")
listWallpapers()
Logger.log("Wallpaper", "Service started")
// Wallpaper is set when the settings are loaded.
// Don't start random wallpaper during initialization
}
property var wallpaperList: []
property string currentWallpaper: Settings.data.wallpaper.current
property bool scanning: false
// SWWW
property string transitionType: Settings.data.wallpaper.swww.transitionType
property var randomChoices: ["simple", "fade", "left", "right", "top", "bottom", "wipe", "wave", "grow", "center", "any", "outer"]
function listWallpapers() {
Logger.log("Wallpapers", "Listing wallpapers")
scanning = true
wallpaperList = []
// Unsetting, then setting the folder will re-trigger the parsing!
folderModel.folder = ""
folderModel.folder = "file://" + (Settings.data.wallpaper.directory !== undefined ? Settings.data.wallpaper.directory : "")
}
function changeWallpaper(path) {
Logger.log("Wallpapers", "Changing to:", path)
setCurrentWallpaper(path, false)
}
function setCurrentWallpaper(path, isInitial) {
// Only regenerate colors if the wallpaper actually changed
var wallpaperChanged = currentWallpaper !== path
currentWallpaper = path
if (!isInitial) {
Settings.data.wallpaper.current = path
}
if (Settings.data.wallpaper.swww.enabled) {
if (Settings.data.wallpaper.swww.transitionType === "random") {
transitionType = randomChoices[Math.floor(Math.random() * randomChoices.length)]
} else {
transitionType = Settings.data.wallpaper.swww.transitionType
// Initialize cache from Settings on startup
var monitors = Settings.data.wallpaper.monitors || []
for (var i = 0; i < monitors.length; i++) {
if (monitors[i].name && monitors[i].wallpaper) {
currentWallpapers[monitors[i].name] = monitors[i].wallpaper
}
}
}
changeWallpaperProcess.running = true
} else {
// All available wallpaper transitions
readonly property ListModel transitionsModel: ListModel {
ListElement {
key: "none"
name: "None"
}
ListElement {
key: "random"
name: "Random"
}
ListElement {
key: "fade"
name: "Fade"
}
ListElement {
key: "disc"
name: "Disc"
}
ListElement {
key: "stripes"
name: "Stripes"
}
ListElement {
key: "wipe"
name: "Wipe"
}
}
// Fallback: update the settings directly for non-SWWW mode
//Logger.log("Wallpapers", "Not using Swww, setting wallpaper directly")
// All transition keys but filter out "none" and "random" so we are left with the real transitions
readonly property var allTransitions: Array.from({
"length": transitionsModel.count
}, (_, i) => transitionsModel.get(i).key).filter(
key => key !== "random" && key != "none")
property var wallpaperLists: ({})
property int scanningCount: 0
readonly property bool scanning: (scanningCount > 0)
// Cache for current wallpapers - can be updated directly since we use signals for notifications
property var currentWallpapers: ({})
// Signals for reactive UI updates
signal wallpaperChanged(string screenName, string path)
// Emitted when a wallpaper changes
signal wallpaperDirectoryChanged(string screenName, string directory)
// Emitted when a monitor's directory changes
signal wallpaperListChanged(string screenName, int count)
// Emitted when available wallpapers list changes
Connections {
target: Settings.data.wallpaper
function onDirectoryChanged() {
root.refreshWallpapersList()
// Emit directory change signals for monitors using the default directory
if (!Settings.data.wallpaper.enableMultiMonitorDirectories) {
// All monitors use the main directory
for (var i = 0; i < Quickshell.screens.length; i++) {
root.wallpaperDirectoryChanged(Quickshell.screens[i].name, Settings.data.wallpaper.directory)
}
} else {
// Only monitors without custom directories are affected
for (var i = 0; i < Quickshell.screens.length; i++) {
var screenName = Quickshell.screens[i].name
var monitor = root.getMonitorConfig(screenName)
if (!monitor || !monitor.directory) {
root.wallpaperDirectoryChanged(screenName, Settings.data.wallpaper.directory)
}
}
}
}
function onEnableMultiMonitorDirectoriesChanged() {
root.refreshWallpapersList()
// Notify all monitors about potential directory changes
for (var i = 0; i < Quickshell.screens.length; i++) {
var screenName = Quickshell.screens[i].name
root.wallpaperDirectoryChanged(screenName, root.getMonitorDirectory(screenName))
}
}
function onRandomEnabledChanged() {
root.toggleRandomWallpaper()
}
function onRandomIntervalSecChanged() {
root.restartRandomWallpaperTimer()
}
}
// -------------------------------------------------------------------
// Get specific monitor wallpaper data
function getMonitorConfig(screenName) {
var monitors = Settings.data.wallpaper.monitors
if (monitors !== undefined) {
for (var i = 0; i < monitors.length; i++) {
if (monitors[i].name !== undefined && monitors[i].name === screenName) {
return monitors[i]
}
}
}
}
// -------------------------------------------------------------------
// Get specific monitor directory
function getMonitorDirectory(screenName) {
if (!Settings.data.wallpaper.enableMultiMonitorDirectories) {
return Settings.data.wallpaper.directory
}
var monitor = getMonitorConfig(screenName)
if (monitor !== undefined && monitor.directory !== undefined) {
return monitor.directory
}
// Fall back to the main/single directory
return Settings.data.wallpaper.directory
}
// -------------------------------------------------------------------
// Set specific monitor directory
function setMonitorDirectory(screenName, directory) {
var monitors = Settings.data.wallpaper.monitors || []
var found = false
// Create a new array with updated values
var newMonitors = monitors.map(function (monitor) {
if (monitor.name === screenName) {
found = true
return {
"name": screenName,
"directory": directory,
"wallpaper": monitor.wallpaper || ""
}
}
return monitor
})
if (!found) {
newMonitors.push({
"name": screenName,
"directory": directory,
"wallpaper": ""
})
}
// Update Settings with new array to ensure proper persistence
Settings.data.wallpaper.monitors = newMonitors.slice()
root.wallpaperDirectoryChanged(screenName, directory)
}
// -------------------------------------------------------------------
// Get specific monitor wallpaper - now from cache
function getWallpaper(screenName) {
return currentWallpapers[screenName] || ""
}
// -------------------------------------------------------------------
function changeWallpaper(screenName, path) {
if (screenName !== undefined) {
_setWallpaper(screenName, path)
} else {
// If no screenName specified change for all screens
for (var i = 0; i < Quickshell.screens.length; i++) {
_setWallpaper(Quickshell.screens[i].name, path)
}
}
}
// -------------------------------------------------------------------
function _setWallpaper(screenName, path) {
if (path === "" || path === undefined) {
return
}
if (screenName === undefined) {
Logger.warn("Wallpaper", "setWallpaper", "no screen specified")
return
}
//Logger.log("Wallpaper", "setWallpaper on", screenName, ": ", path)
// Check if wallpaper actually changed
var oldPath = currentWallpapers[screenName] || ""
var wallpaperChanged = (oldPath !== path)
if (!wallpaperChanged) {
// No change needed
return
}
// Update cache directly
currentWallpapers[screenName] = path
// Update Settings - still need immutable update for Settings persistence
// The slice() ensures Settings detects the change and saves properly
var monitors = Settings.data.wallpaper.monitors || []
var found = false
var newMonitors = monitors.map(function (monitor) {
if (monitor.name === screenName) {
found = true
return {
"name": screenName,
"directory": monitor.directory || getMonitorDirectory(screenName),
"wallpaper": path
}
}
return monitor
})
if (!found) {
newMonitors.push({
"name": screenName,
"directory": getMonitorDirectory(screenName),
"wallpaper": path
})
}
Settings.data.wallpaper.monitors = newMonitors.slice()
// Emit signal for this specific wallpaper change
root.wallpaperChanged(screenName, path)
// Restart the random wallpaper timer
if (randomWallpaperTimer.running) {
randomWallpaperTimer.restart()
}
// Only notify ColorScheme service if the wallpaper actually changed
if (wallpaperChanged) {
ColorSchemeService.changedWallpaper()
}
}
// -------------------------------------------------------------------
function setRandomWallpaper() {
var randomIndex = Math.floor(Math.random() * wallpaperList.length)
var randomPath = wallpaperList[randomIndex]
if (!randomPath) {
return
Logger.log("Wallpaper", "setRandomWallpaper")
if (Settings.data.wallpaper.enableMultiMonitorDirectories) {
// Pick a random wallpaper per screen
for (var i = 0; i < Quickshell.screens.length; i++) {
var screenName = Quickshell.screens[i].name
var wallpaperList = getWallpapersList(screenName)
if (wallpaperList.length > 0) {
var randomIndex = Math.floor(Math.random() * wallpaperList.length)
var randomPath = wallpaperList[randomIndex]
changeWallpaper(screenName, randomPath)
}
}
} else {
// Pick a random wallpaper common to all screens
// We can use any screenName here, so we just pick the primary one.
var wallpaperList = getWallpapersList(Screen.name)
if (wallpaperList.length > 0) {
var randomIndex = Math.floor(Math.random() * wallpaperList.length)
var randomPath = wallpaperList[randomIndex]
changeWallpaper(undefined, randomPath)
}
}
setCurrentWallpaper(randomPath, false)
}
// -------------------------------------------------------------------
function toggleRandomWallpaper() {
if (Settings.data.wallpaper.isRandom && !randomWallpaperTimer.running) {
randomWallpaperTimer.start()
Logger.log("Wallpaper", "toggleRandomWallpaper")
if (Settings.data.wallpaper.randomEnabled) {
restartRandomWallpaperTimer()
setRandomWallpaper()
} else if (!Settings.data.randomWallpaper && randomWallpaperTimer.running) {
randomWallpaperTimer.stop()
}
}
// -------------------------------------------------------------------
function restartRandomWallpaperTimer() {
if (Settings.data.wallpaper.isRandom) {
randomWallpaperTimer.stop()
randomWallpaperTimer.start()
randomWallpaperTimer.restart()
}
}
function startSWWWDaemon() {
if (Settings.data.wallpaper.swww.enabled) {
Logger.log("Swww", "Requesting swww-daemon")
startDaemonProcess.running = true
// -------------------------------------------------------------------
function getWallpapersList(screenName) {
if (screenName != undefined && wallpaperLists[screenName] != undefined) {
return wallpaperLists[screenName]
}
return []
}
// -------------------------------------------------------------------
function refreshWallpapersList() {
Logger.log("Wallpaper", "refreshWallpapersList")
scanningCount = 0
// Force refresh by toggling the folder property on each FolderListModel
for (var i = 0; i < wallpaperScanners.count; i++) {
var scanner = wallpaperScanners.objectAt(i)
if (scanner) {
var currentFolder = scanner.folder
scanner.folder = ""
scanner.folder = currentFolder
}
}
}
// -------------------------------------------------------------------
// -------------------------------------------------------------------
// -------------------------------------------------------------------
Timer {
id: randomWallpaperTimer
interval: Settings.data.wallpaper.randomInterval * 1000
running: false
interval: Settings.data.wallpaper.randomIntervalSec * 1000
running: Settings.data.wallpaper.randomEnabled
repeat: true
onTriggered: setRandomWallpaper()
triggeredOnStart: false
}
FolderListModel {
id: folderModel
// Swww supports many images format but Quickshell only support a subset of those.
nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.pnm", "*.bmp"]
showDirs: false
sortField: FolderListModel.Name
onStatusChanged: {
if (status === FolderListModel.Ready) {
var files = []
for (var i = 0; i < count; i++) {
var directory = (Settings.data.wallpaper.directory !== undefined ? Settings.data.wallpaper.directory : "")
var filepath = directory + "/" + get(i, "fileName")
files.push(filepath)
// Instantiator (not Repeater) to create FolderListModel for each monitor
Instantiator {
id: wallpaperScanners
model: Quickshell.screens
delegate: FolderListModel {
property string screenName: modelData.name
property string currentDirectory: root.getMonitorDirectory(screenName)
folder: "file://" + currentDirectory
nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.pnm", "*.bmp"]
showDirs: false
sortField: FolderListModel.Name
// Watch for directory changes via property binding
onCurrentDirectoryChanged: {
folder = "file://" + currentDirectory
}
Component.onCompleted: {
// Connect to directory change signal
root.wallpaperDirectoryChanged.connect(function (screen, directory) {
if (screen === screenName) {
currentDirectory = directory
}
})
}
onStatusChanged: {
if (status === FolderListModel.Null) {
// Flush the list
root.wallpaperLists[screenName] = []
root.wallpaperListChanged(screenName, 0)
} else if (status === FolderListModel.Loading) {
// Flush the list
root.wallpaperLists[screenName] = []
scanningCount++
} else if (status === FolderListModel.Ready) {
var files = []
for (var i = 0; i < count; i++) {
var directory = root.getMonitorDirectory(screenName)
var filepath = directory + "/" + get(i, "fileName")
files.push(filepath)
}
// Update the list
root.wallpaperLists[screenName] = files
scanningCount--
Logger.log("Wallpaper", "List refreshed for", screenName, "count:", files.length)
root.wallpaperListChanged(screenName, files.length)
}
wallpaperList = files
scanning = false
Logger.log("Wallpapers", "List refreshed, count:", wallpaperList.length)
}
}
}
Process {
id: changeWallpaperProcess
command: ["swww", "img", "--resize", Settings.data.wallpaper.swww.resizeMethod, "--transition-fps", Settings.data.wallpaper.swww.transitionFps.toString(
), "--transition-type", transitionType, "--transition-duration", Settings.data.wallpaper.swww.transitionDuration.toString(
), currentWallpaper]
running: false
onStarted: {
}
onExited: function (exitCode, exitStatus) {
Logger.log("Swww", "Process finished with exit code:", exitCode, "status:", exitStatus)
if (exitCode !== 0) {
Logger.log("Swww", "Process failed. Make sure swww-daemon is running with: swww-daemon")
Logger.log("Swww", "You can start it with: swww-daemon --format xrgb")
}
}
}
Process {
id: startDaemonProcess
command: ["swww-daemon", "--format", "xrgb"]
running: false
onStarted: {
Logger.log("Swww", "Daemon start process initiated")
}
onExited: function (exitCode, exitStatus) {
Logger.log("Swww", "Daemon start process finished with exit code:", exitCode)
if (exitCode === 0) {
Logger.log("Swww", "Daemon started successfully")
} else {
Logger.log("Swww", "Failed to start daemon, may already be running")
}
}
}

57
Shaders/frag/wp_disc.frag Normal file
View File

@@ -0,0 +1,57 @@
// ===== wp_disc.frag =====
#version 450
layout(location = 0) in vec2 qt_TexCoord0;
layout(location = 0) out vec4 fragColor;
layout(binding = 1) uniform sampler2D source1; // Current wallpaper
layout(binding = 2) uniform sampler2D source2; // Next wallpaper
layout(std140, binding = 0) uniform buf {
mat4 qt_Matrix;
float qt_Opacity;
float progress; // Transition progress (0.0 to 1.0)
float centerX; // X coordinate of disc center (0.0 to 1.0)
float centerY; // Y coordinate of disc center (0.0 to 1.0)
float smoothness; // Edge smoothness (0.0 to 1.0, 0=sharp, 1=very smooth)
float aspectRatio; // Width / Height of the screen
} ubuf;
void main() {
vec2 uv = qt_TexCoord0;
vec4 color1 = texture(source1, uv); // Current (old) wallpaper
vec4 color2 = texture(source2, uv); // Next (new) wallpaper
// Map smoothness from 0.0-1.0 to 0.001-0.5 range
// Using a non-linear mapping for better control
float mappedSmoothness = mix(0.001, 0.5, ubuf.smoothness * ubuf.smoothness);
// Adjust UV coordinates to compensate for aspect ratio
// This makes distances circular instead of elliptical
vec2 adjustedUV = vec2(uv.x * ubuf.aspectRatio, uv.y);
vec2 adjustedCenter = vec2(ubuf.centerX * ubuf.aspectRatio, ubuf.centerY);
// Calculate distance in aspect-corrected space
float dist = distance(adjustedUV, adjustedCenter);
// Calculate the maximum possible distance (corner to corner)
// This ensures the disc can cover the entire screen
float maxDistX = max(ubuf.centerX * ubuf.aspectRatio,
(1.0 - ubuf.centerX) * ubuf.aspectRatio);
float maxDistY = max(ubuf.centerY, 1.0 - ubuf.centerY);
float maxDist = length(vec2(maxDistX, maxDistY));
// Scale progress to cover the maximum distance
// Add extra range for smoothness to ensure complete coverage
// Adjust smoothness for aspect ratio to maintain consistent visual appearance
float adjustedSmoothness = mappedSmoothness * max(1.0, ubuf.aspectRatio);
float radius = ubuf.progress * (maxDist + adjustedSmoothness);
// Use smoothstep for a smooth edge transition
float factor = smoothstep(radius - adjustedSmoothness, radius + adjustedSmoothness, dist);
// Mix the textures (factor = 0 inside disc, 1 outside)
fragColor = mix(color2, color1, factor);
fragColor *= ubuf.qt_Opacity;
}

19
Shaders/frag/wp_fade.frag Normal file
View File

@@ -0,0 +1,19 @@
#version 450
layout(location = 0) in vec2 qt_TexCoord0;
layout(location = 0) out vec4 fragColor;
layout(binding = 1) uniform sampler2D source1;
layout(binding = 2) uniform sampler2D source2;
layout(std140, binding = 0) uniform buf {
mat4 qt_Matrix;
float qt_Opacity;
float progress;
};
void main() {
vec4 color1 = texture(source1, qt_TexCoord0);
vec4 color2 = texture(source2, qt_TexCoord0);
// Mix the two textures based on progress value
fragColor = mix(color1, color2, progress) * qt_Opacity;
}

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