1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 21:42:51 -05:00

Compare commits

..

207 Commits

Author SHA1 Message Date
bbedward
7a5d612599 File browser improvements 2025-10-17 21:36:09 -04:00
github-actions[bot]
205c43181b Update VERSION to v0.1.17 (from DMS) 2025-10-17 20:03:12 +00:00
purian23
05a72abf41 Update Copr Architecture logic 2025-10-17 15:39:09 -04:00
purian23
14262ba510 Silence upgrade noise 2025-10-17 11:05:54 -04:00
github-actions[bot]
d847b1e09c i18n: update translations 2025-10-17 12:48:01 +00:00
bbedward
0086e42a86 i18n: don't rely on po webhooks 2025-10-17 08:47:18 -04:00
bbedward
7474d5a7bf poeditor: disable workflow
They paywalled it even for open source
2025-10-17 08:42:56 -04:00
bbedward
5696a36115 ws: disable window scrolling toggle when not niri 2025-10-17 08:40:26 -04:00
maggster165
3cdc1a9c81 Add Workspace Indicator scrolling (#475) 2025-10-17 08:37:28 -04:00
Massimo Branchini
b095fb9005 small fix: initial space does not allow correct alignment (#477) 2025-10-17 08:37:18 -04:00
bbedward
ce6c16214c lock: allow custom lock command 2025-10-17 08:36:11 -04:00
bbedward
b6f7f2734e network: hide eth/wifi preference when apiVersion < 5 2025-10-17 08:23:56 -04:00
bbedward
4db55e4d77 Bump expected API back down, doesn't really matter 2025-10-17 08:17:16 -04:00
Massimo Branchini
b21f6e80b3 enhancement: managed NetworkManager ethernet configurations connectio… (#473)
* enhancement: managed NetworkManager ethernet configurations connection from control panel

* server API minimal version
2025-10-17 08:05:42 -04:00
bokicoder
a804fb849e Update readme (#471) 2025-10-17 07:01:12 -04:00
purian23
4ca91cd9f7 SELinux & Path DIR updates 2025-10-17 01:28:59 -04:00
purian23
16e1b587b4 Added logic for PAM users / SELinux 2025-10-16 23:56:25 -04:00
bbedward
5e2756d200 theme: don't need portal for light/dark 2025-10-16 23:38:13 -04:00
bbedward
ce9ab22ae1 notepad: use ref system for service 2025-10-16 23:01:48 -04:00
bbedward
72ad35e1f9 theme: don't depend on dms for gsettings theme mode 2025-10-16 22:58:49 -04:00
bbedward
c0d110cde0 controlcenter: fix trigger position via IPC 2025-10-16 22:03:02 -04:00
bbedward
b9d5deb2ae notifications: fix dnd tooltip & silence sounds on do not disturb 2025-10-16 21:30:45 -04:00
Nasser Alshammari
d4b13ef46b Dropbox icon workaround when DankBar is vertical (#466) 2025-10-16 21:22:15 -04:00
BB
748d9e342e Update translations/poexports/pt.json (POEditor.com) 2025-10-16 21:18:14 -04:00
purian23
f49312fc0e Update ReadMe 2025-10-16 21:17:54 -04:00
BB
e0d8bbb243 Update translations/poexports/pt.json (POEditor.com) 2025-10-16 19:59:55 -04:00
purian23
153f2a49f8 Update Copr Workflow 2025-10-16 19:59:29 -04:00
github-actions[bot]
8b272dc2fd Update VERSION to v0.1.16 (from DMS) 2025-10-16 23:44:02 +00:00
purian23
87a919bbde Remove dupes 2025-10-16 18:07:21 -04:00
purian23
d3017e98c5 Suppress instructions after init install 2025-10-16 17:58:31 -04:00
Jaren Glenn
5758d7274e calendar: detect and parse 12-hour event start/end times (#464) 2025-10-16 16:10:05 -04:00
bbedward
0e215d69cb nm: revise enterprise flow 2025-10-16 15:53:38 -04:00
bbedward
cbaaa32ce8 matugen: prefix all internal templates with dms 2025-10-16 14:09:20 -04:00
bbedward
5c81646397 idle: remove lock before suspend, manually triggered
- depend on our dbus service telling us to lock
2025-10-16 13:52:09 -04:00
BB
30ca1fb14f Update translations/poexports/pt.json (POEditor.com) 2025-10-16 11:52:22 -04:00
bbedward
9fab49984a i18n: add portugese to export workflow 2025-10-16 11:51:52 -04:00
BB
696fa6e4f8 Update translations/poexports/ja.json (POEditor.com) 2025-10-16 11:49:03 -04:00
BB
921393e84e Update translations/poexports/ja.json (POEditor.com) 2025-10-16 11:48:43 -04:00
BB
13e894e910 Update translations/poexports/zh_CN.json (POEditor.com) 2025-10-16 11:48:39 -04:00
bbedward
7c7e8aaef3 i18n: add portugese 2025-10-16 11:48:10 -04:00
bbedward
7ea3bd9df9 animations: dynamic bar/dock animation durations 2025-10-16 11:44:05 -04:00
bbedward
7bf7d0afae lock: terminate fprint pam session when using password 2025-10-16 11:37:14 -04:00
bbedward
0d329baaca dock: restructure + icon size option + orientation improvements 2025-10-16 11:23:16 -04:00
enzi
941a87b59c fix crash in spacer (#461) 2025-10-16 10:25:29 -04:00
bbedward
a9e8ac46d8 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-10-16 09:58:55 -04:00
bbedward
0d3a294118 dankbar: allow layer override via env 2025-10-16 09:58:44 -04:00
purian23
7a27537632 Remove ghostty req 2025-10-16 09:55:04 -04:00
bbedward
287e778ddb clock: show seconds false by default 2025-10-16 09:52:16 -04:00
Oleksandr
ce44edb419 support for displaying seconds on a clock (#457)
Co-authored-by: Oleksandr <avktech@gmail.com>
2025-10-16 09:51:23 -04:00
bbedward
9dcd8af7a3 workspace: respect theme corner radius 2025-10-16 09:50:19 -04:00
bokicoder
76dfcd0ccb fix incorrect path in the greeter module (#459) 2025-10-16 08:20:05 -04:00
Massimo Branchini
13a188635d small fix: correct width for per-monitor dropdown (#454) 2025-10-16 08:19:30 -04:00
bbedward
cd18fd5aed theme: make portal prefer-dark sync optional 2025-10-16 00:16:34 -04:00
purian23
b277bd8014 Improve Fedora dms-greeter post install 2025-10-15 23:37:57 -04:00
purian23
daa0d368ab Update dms-greeter permissions & post install 2025-10-15 23:11:53 -04:00
purian23
2cc7777e16 DMS-Greeter spec update 2025-10-15 19:54:29 -04:00
bbedward
d276e31f7b niri: --no-preserve=mode on binds copy command 2025-10-15 18:23:53 -04:00
bbedward
7f35ba7e21 spotlight: fix context menu button click widths 2025-10-15 18:17:58 -04:00
bbedward
edd54dda84 dock: repair context menu screens & tooltips 2025-10-15 18:17:56 -04:00
purian23
a50a97314d Prefer quickshell-git 2025-10-15 18:01:05 -04:00
bbedward
4bc05e7083 idle: add lock before suspend to idle manager 2025-10-15 16:40:20 -04:00
bbedward
09a45b49a6 dock: fix show on overview logic 2025-10-15 16:14:20 -04:00
purian23
1c0b71436e Update dms-git priority 2025-10-15 16:11:50 -04:00
bbedward
24f5e9a7e6 plugins: add PluginGlobalVar
- Allow syncing vars to all widget instances, like a singleton
2025-10-15 16:08:24 -04:00
bbedward
59d123a4a1 common: dont require id in runCommand helper 2025-10-15 14:53:23 -04:00
bbedward
ed2afa03f9 revert useless changes 2025-10-15 14:52:53 -04:00
bbedward
3c531dc2ec bar: bind widget refresh to dpr changes 2025-10-15 14:43:47 -04:00
bbedward
83564bd03f media: disable layer on morphing blob
- already on the window
2025-10-15 14:38:34 -04:00
bbedward
7146d0d92d Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-10-15 14:23:25 -04:00
bbedward
e842d6761a theme: handle colors being deleted more gracefully 2025-10-15 14:23:02 -04:00
bbedward
17b405e9dc Also bind system tray visibility to # of items 2025-10-15 13:26:20 -04:00
bbedward
f281513a41 launcher: fix os logo on multi-monitor 2025-10-15 13:22:50 -04:00
bbedward
63b876479f Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-10-15 12:48:24 -04:00
bbedward
38b833c886 bar: dpr-aware canvas 2025-10-15 12:31:07 -04:00
BB
d75ea18e9f Update translations/poexports/zh_CN.json (POEditor.com) 2025-10-15 12:07:26 -04:00
bbedward
f311b20ef7 Fix zh-Hans code 2025-10-15 12:06:56 -04:00
bbedward
78f7237422 workflow: add poeditor job 2025-10-15 12:05:44 -04:00
purian23
726af3393b Copr Ghostty support 2025-10-15 11:55:13 -04:00
bbedward
c772331554 version: prefer git head over VERSION file 2025-10-15 11:36:36 -04:00
github-actions[bot]
80d257b94f Update VERSION to v0.1.15 (from DMS) 2025-10-15 15:34:20 +00:00
bbedward
e2db034959 try and fix version commit 2025-10-15 11:31:53 -04:00
bbedward
c4e88e5c05 hyprland: add keybinds cheatsheet 2025-10-15 11:18:02 -04:00
bbedward
e47e7667c6 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-10-15 10:33:32 -04:00
bbedward
8bb2a64663 cc: allow multiple brightness sliders 2025-10-15 10:31:55 -04:00
purian23
e056e08fc1 Prefer git build w/source 2025-10-15 10:14:34 -04:00
bbedward
342cd55bc0 loginctl: disable inhibit when loginctl integration disabled 2025-10-15 09:58:44 -04:00
bbedward
23ef19e683 bar: attempt to fix some nichhe binding issues 2025-10-15 09:16:53 -04:00
bbedward
437fd29e96 ipc: move mpris to global controller 2025-10-15 09:08:26 -04:00
bbedward
aa7a07fd99 bar: trigger repaint on opacity change 2025-10-15 09:03:31 -04:00
bbedward
5217006dec workflow: fix tag version generation 2025-10-15 09:00:58 -04:00
max72bra
ab4f6baae6 Improvement: Allow the user to perform custom power actions (#439) 2025-10-15 08:46:32 -04:00
Oleksandr
1976ea4d49 Multi batteries support (#431)
* Multi batteries support

* Multi batteries support DMS_PREFERRED_BATTERY fix

---------

Co-authored-by: Oleksandr <avktech@gmail.com>
2025-10-15 08:45:35 -04:00
bbedward
697fc4d2b7 Fix version detection 2025-10-15 08:40:12 -04:00
max72bra
38c1f7bbcb small fix: update popout column space (#441) 2025-10-15 08:16:05 -04:00
max72bra
8cbfaab807 small fix: right check on execution (#440) 2025-10-15 08:15:40 -04:00
Bruno Cesar Rocha
f4a4151632 docs: update PLUGIN docs (#443) 2025-10-15 08:14:49 -04:00
bbedward
5f810fe741 update locales 2025-10-15 00:25:48 -04:00
bbedward
adaa0caab8 fix: clip add widget listview in control center 2025-10-15 00:02:26 -04:00
bbedward
54ef14e765 ipc: Add openWithQuery and toggleWithQuery to spotlight 2025-10-14 23:56:43 -04:00
Body
d1383b5d1b remove screen dimming from dropdown menus (#435) 2025-10-14 23:44:29 -04:00
bbedward
caa703af99 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-10-14 23:23:42 -04:00
bbedward
90aab9f4db sounds: option to override system sound themes 2025-10-14 23:23:22 -04:00
purian23
3439030145 feat: Copr DMS-Greeter support 2025-10-14 23:11:11 -04:00
bbedward
058c7408d1 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-10-14 22:21:27 -04:00
purian23
a4f7fd58f6 Tag dms-git spec 2025-10-14 22:14:31 -04:00
bbedward
6f3024c90d Remove loader from Spotlight chain 2025-10-14 22:14:10 -04:00
bbedward
5f95fa5e79 Use proc helper in more places 2025-10-14 16:52:50 -04:00
bbedward
f9cb0506e9 fix session service connection 2025-10-14 15:58:10 -04:00
bbedward
2429401d0e Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-10-14 15:14:37 -04:00
bbedward
9ff0d7405f common: add Proc.runCommand helper 2025-10-14 15:14:20 -04:00
bbedward
5bb5cd296d config: restructure, migration system, cache data 2025-10-14 15:07:28 -04:00
purian23
273662e03e Update Copr Stable Workflow 2025-10-14 14:28:56 -04:00
bbedward
9c1a89d786 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-10-14 14:14:25 -04:00
bbedward
524d7ee5c0 systemupdate: use ref system to prevent executions 2025-10-14 14:14:06 -04:00
purian23
51d2bc9aae Spec update - DMS CLI Source 2025-10-14 14:12:34 -04:00
max72bra
0c8a7ff332 small detail: the timer must run on both pkgManager and updChecker (#428) 2025-10-14 14:11:33 -04:00
bbedward
309b8d9efe i18n: update terms
adjacently, fix the power menu confirm option
2025-10-14 14:06:46 -04:00
max72bra
c2f32b7bdc enhancement: let power actions confirmation optional (#427) 2025-10-14 13:59:12 -04:00
purian23
563bc7b359 Spec update 2025-10-14 13:44:05 -04:00
bbedward
94ca5a5bef set default env to prevent dGPU wakeups 2025-10-14 13:16:29 -04:00
bbedward
4464589c0f elide, dont wrap spotlight/launcher results 2025-10-14 12:58:13 -04:00
bbedward
748d4fe2ac Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-10-14 12:40:25 -04:00
bbedward
50b28dc8ca launcher: fix crash in plugin action execution 2025-10-14 12:39:51 -04:00
max72bra
118980a9fb bug fix: launcher settings tab -> recently used apps -> mass and single clear not saving changes (#426) 2025-10-14 12:17:26 -04:00
bbedward
8aff381676 launcher: add sort by alphabetically option
sounds: convert files to wav
2025-10-14 11:08:52 -04:00
bbedward
6d0fba1905 localization: wifi password modal strings 2025-10-14 10:57:48 -04:00
bbedward
7e885d3cee nm: enterprise options for domain and realm 2025-10-14 10:26:45 -04:00
bbedward
811daf74ff Fix spacing of night mode + auto wallpaper sections 2025-10-14 10:05:04 -04:00
bbedward
ee755b8bd6 lock: fprintd support 2025-10-14 09:18:57 -04:00
Moraxyc Xu
1019eb925a nix: add system sound support option (#422) 2025-10-14 08:18:51 -04:00
max72bra
061bb50b88 system updater: separated update finder from pkg manager (#419) 2025-10-14 08:18:34 -04:00
bbedward
eb7e665c86 Fix delegate on lock 2025-10-14 08:15:29 -04:00
bbedward
80301d1aab fix warning 2025-10-14 08:01:14 -04:00
bbedward
ea56fb5840 sounds: make qt6-multimedia optional 2025-10-14 07:58:16 -04:00
purian23
692b45c4f0 Finalize Copr Workflow 2025-10-14 01:28:58 -04:00
purian23
8d53a8826e Fix package dependencies for Ubuntu build environment 2025-10-14 00:46:15 -04:00
purian23
64ea115303 Copr Workflow & Spec 2025-10-14 00:25:10 -04:00
bbedward
b5e29cf50c Incorporate some system sounds 2025-10-13 22:38:13 -04:00
bbedward
381df1e949 fix bindings in Theme & Colors settings pane 2025-10-13 22:12:32 -04:00
bbedward
13a81eda6f calendar: fix binding loop 2025-10-13 22:05:12 -04:00
bbedward
a48c39642a Fix outdated oSD trigger 2025-10-13 22:02:34 -04:00
bbedward
3be3e622bc re-work mouse handling of dannkbar 2025-10-13 21:34:53 -04:00
bbedward
07fe2ca407 Restore correct workflow 2025-10-13 20:22:18 -04:00
bbedward
9b96dae744 Sync lock/unlock events with loginctl 2025-10-13 19:46:18 -04:00
bbedward
0e3d3d1a40 fix hyprland svg 2025-10-13 19:31:28 -04:00
Bruno Cesar Rocha
cb3274fb0c fix: get desktop entry back (#417) 2025-10-13 19:25:27 -04:00
bbedward
a3ada5b2bb Restructure lock screen 2025-10-13 17:44:09 -04:00
bbedward
3e167a2c52 Update localizations 2025-10-13 17:05:45 -04:00
bbedward
38b3ad2b31 niri: re-gen layout kdl on dbar spacing change 2025-10-13 17:03:08 -04:00
bbedward
56e5cd13b7 Add popup gaps override + math min 4 logic to match existing code 2025-10-13 16:57:18 -04:00
bbedward
d63c0fc6f0 Simplify font picking and elide contents 2025-10-13 14:58:24 -04:00
bbedward
6814b140fc bar: more repaint triggers + repaint debounce 2025-10-13 14:38:03 -04:00
max72bra
5f7e478118 Enhancement: custom system updater command (#414)
* enhancement: system updater custom command and custom additional terminal parameters

* removed console log

* minor: tabs
2025-10-13 14:25:25 -04:00
Bruno Cesar Rocha
7317024da5 feat: Launcher Plugin Component (#408)
Load launcher items from plugin.
2025-10-13 14:24:41 -04:00
Body
9b9fbabc3f tweak popout margins for consistency (#399) 2025-10-13 14:17:02 -04:00
bbedward
3c5a23799f Flip light/dark mode and icon themes properly 2025-10-13 14:16:36 -04:00
bbedward
3bfdc6163c Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-10-13 13:54:54 -04:00
purian23
cf2f74a38d Stock workflow release 2025-10-12 15:23:00 -04:00
bbedward
2a7f52c67e Update readme 2025-10-12 07:46:43 -04:00
Body
50fde1e308 Add niri overview toggle on launcher button rightclick. (#394) 2025-10-12 07:38:55 -04:00
max72bra
46fd0ae413 Hyprland: filter all spacial workspaces (#398)
Filters all workspaces with IDs less than 0, regardless of their name (which is user-defined)
2025-10-12 07:38:39 -04:00
bokicoder
65e32dc429 Fix keyboard focus issue in clipboard (#391) 2025-10-12 07:38:15 -04:00
Body
413675dfc1 Close ControlCenter popout on Settings open. (#396) 2025-10-12 07:34:06 -04:00
purian23
a17343f40e Enable Copr stable builds 2025-10-12 01:08:10 -04:00
bokicoder
5d023804c1 update flake.lock (#390) 2025-10-12 00:12:41 -04:00
bbedward
fa07a846b9 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-10-11 23:48:36 -04:00
bbedward
5df46b605e Call lockerReady to releasse inhibitor whenlock screen is drawn 2025-10-11 23:48:13 -04:00
bokicoder
77cf371a21 update flake.lock (#388) 2025-10-11 23:42:55 -04:00
bbedward
89802dd040 Presever user configs 2025-10-11 23:08:11 -04:00
bbedward
59b95e9dd6 Bundle distro package with dms releases 2025-10-11 22:21:55 -04:00
bbedward
b836db5252 Update copr-related build opts 2025-10-11 21:59:08 -04:00
purian23
4dc4b15925 Revert "Update spec to SRPM by default"
This reverts commit 5c3062e699.
2025-10-11 18:19:21 -04:00
purian23
5c3062e699 Update spec to SRPM by default 2025-10-11 17:52:26 -04:00
bbedward
3a7777c643 api: unify dms API clients
- Single subscribe socket
- Single req/callback socket
- Remove PrepareForSleep handling
- Dependency on dms API version enforcement
- Remove gdbus from portal and session
2025-10-11 14:37:18 -04:00
bbedward
71543c35d6 Re-organize settings 2025-10-11 13:04:26 -04:00
bbedward
f4cf66dc01 Update terms 2025-10-11 11:37:09 -04:00
bbedward
7870dff0fd matugen: opt to run user templates 2025-10-11 11:30:19 -04:00
bbedward
9fc9c1ed19 Update issue template 2025-10-11 10:39:34 -04:00
bbedward
4d0151350f Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-10-11 10:30:19 -04:00
bbedward
dff10c8d13 Update issue templates 2025-10-11 10:30:09 -04:00
purian23
362bcb9294 More cli - better spec 2025-10-11 02:14:48 -04:00
purian23
351b4f8a94 Remove CLI ref 2025-10-11 01:59:59 -04:00
bbedward
90955eb0a1 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-10-10 20:07:19 -04:00
bbedward
62669747ad lock/greeter: show full power menu 2025-10-10 20:07:01 -04:00
bokicoder
466d00c666 Modify the greeter module to use the dms-greeter wrapper (#373) 2025-10-10 19:20:17 -04:00
bbedward
63845ff875 Update vid 2025-10-10 18:53:52 -04:00
bbedward
d013748a51 plugins: fix variant syncing 2025-10-10 18:45:43 -04:00
bbedward
474af3bc07 Remove braindead mouse area arbitrary rules 2025-10-10 18:21:35 -04:00
bbedward
8e09e155fa greeter: update readme 2025-10-10 17:47:02 -04:00
bbedward
7f9f4f96b9 Update greeter readme 2025-10-10 17:44:06 -04:00
bbedward
cd488a8623 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-10-10 15:01:10 -04:00
bbedward
080b7a28b1 Fix force focus 2025-10-10 15:00:57 -04:00
purian23
6949ed0ebd No inline package comments 2025-10-10 14:57:32 -04:00
purian23
8465fa45bb Adjust inline spec comments 2025-10-10 14:49:34 -04:00
purian23
40835ffc89 Update copr dms spec 2025-10-10 14:20:47 -04:00
bbedward
01a42ff330 Fix key forward targets 2025-10-10 13:51:17 -04:00
bbedward
ba49654a64 Fix launcher context menu sizing 2025-10-10 13:42:55 -04:00
bbedward
bc6577fe18 Fix icon theme overflow 2025-10-10 12:47:22 -04:00
bbedward
4ca3f0da67 Fix tab/backtab in launchers 2025-10-10 12:40:02 -04:00
bbedward
7f2086488b Fix power menu focus activation 2025-10-10 12:29:14 -04:00
bbedward
3014fd8095 Fractional scaling fixes + bar border settings 2025-10-10 12:25:00 -04:00
bbedward
27885c8ac3 Update readme and about page 2025-10-10 00:05:49 -04:00
bbedward
d6be0509ac Put release bin in bin/ folder 2025-10-09 23:32:20 -04:00
bbedward
1c85f5e857 Make release assets more sane and understandable 2025-10-09 23:28:10 -04:00
bbedward
abe5515aca Expose ensureVisible to PluginSettings 2025-10-09 23:07:42 -04:00
bbedward
6fba975490 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-10-09 20:38:26 -04:00
bbedward
2de6798f45 Clean up variants from bar whhen removed from plugin data 2025-10-09 20:37:14 -04:00
179 changed files with 21727 additions and 9477 deletions

View File

@@ -26,6 +26,14 @@ assignees: ""
- [ ] Hyprland
- [ ] Other (specify)
## Distribution
<!-- Arch, Fedora, Debian, etc. -->
## dms version
<!-- Output of dms version command -->
## Description
<!-- Brief description of the issue -->
@@ -45,6 +53,14 @@ assignees: ""
## Error Messages/Logs
<!-- Please include any error messages, stack traces, or relevant logs -->
<!-- you can get a log file with the following steps:
dms kill
mkdir ~/dms_logs
nohup dms run > ~/dms_logs/dms-$(date +%s).txt 2>&1 &
Then trigger your issue, and share the contents of ~/dms_logs/dms-<timestamp>.txt
-->
```
Paste error messages or logs here

View File

@@ -12,6 +12,14 @@ assignees: ""
- [ ] Hyprland
- [ ] other
## Distribution
<!-- Arch, Fedora, Debian, etc. -->
## dms version
<!-- Output of dms version command -->
## Description
<!-- Brief description of the support needed -->

290
.github/workflows/copr-release.yml vendored Normal file
View File

@@ -0,0 +1,290 @@
name: DMS Copr Stable Release
on:
push:
tags:
- 'v*'
release:
types: [published]
workflow_dispatch:
inputs:
version:
description: 'Versioning (e.g., 0.1.14, leave empty for latest release)'
required: false
default: ''
jobs:
build-and-upload:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Determine version
id: version
run: |
if [ -n "${{ github.event.inputs.version }}" ]; then
VERSION="${{ github.event.inputs.version }}"
echo "Using manual version: $VERSION"
elif [ "${{ github.event_name }}" = "release" ]; then
VERSION="${{ github.event.release.tag_name }}"
VERSION="${VERSION#v}"
echo "Using release version: $VERSION"
elif [ "${{ github.event_name }}" = "push" ] && [[ "${{ github.ref }}" == refs/tags/* ]]; then
VERSION="${{ github.ref_name }}"
VERSION="${VERSION#v}"
echo "Using tag version: $VERSION"
else
# Fallback to latest release
VERSION=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | jq -r '.tag_name' | sed 's/^v//')
echo "Using latest release version: $VERSION"
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "✅ Building DMS stable version: $VERSION"
- name: Setup build environment
run: |
sudo apt-get update
sudo apt-get install -y rpm wget curl jq gzip
mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
echo "✅ RPM build environment ready"
- name: Download release assets
run: |
VERSION="${{ steps.version.outputs.version }}"
cd ~/rpmbuild/SOURCES
echo "📦 Downloading DMS QML source for v${VERSION}..."
# Download DMS QML source
wget "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${VERSION}/dms-qml.tar.gz" || {
echo "❌ Failed to download dms-qml.tar.gz for v${VERSION}"
exit 1
}
echo "✅ Source downloaded"
echo "Note: dms-cli and dgop binaries will be downloaded during build based on target architecture"
ls -lh
- name: Generate stable spec file
run: |
VERSION="${{ steps.version.outputs.version }}"
CHANGELOG_DATE="$(date '+%a %b %d %Y')"
cat > ~/rpmbuild/SPECS/dms.spec <<'SPECEOF'
# Spec for DMS stable releases - Generated by GitHub Actions
%global debug_package %{nil}
%global version VERSION_PLACEHOLDER
%global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors
Name: dms
Version: %{version}
Release: 1%{?dist}
Summary: %{pkg_summary}
License: GPL-3.0-only
URL: https://github.com/AvengeMedia/DankMaterialShell
Source0: dms-qml.tar.gz
BuildRequires: gzip
BuildRequires: wget
Requires: (quickshell or quickshell-git)
Requires: dms-cli
Requires: dgop
Requires: fira-code-fonts
Requires: material-symbols-fonts
Requires: rsms-inter-fonts
Recommends: brightnessctl
Recommends: cava
Recommends: cliphist
Recommends: hyprpicker
Recommends: matugen
Recommends: wl-clipboard
Recommends: gammastep
Recommends: NetworkManager
Recommends: qt6-qtmultimedia
Suggests: qt6ct
%description
DankMaterialShell (DMS) is a modern Wayland desktop shell built with Quickshell
and optimized for the niri and hyprland compositors. Features notifications,
app launcher, wallpaper customization, and fully customizable with plugins.
Includes auto-theming for GTK/Qt apps with matugen, 20+ customizable widgets,
process monitoring, notification center, clipboard history, dock, control center,
lock screen, and comprehensive plugin system.
%package -n dms-cli
Summary: DankMaterialShell CLI tool
License: GPL-3.0-only
URL: https://github.com/AvengeMedia/danklinux
%description -n dms-cli
Command-line interface for DankMaterialShell configuration and management.
Provides native DBus bindings, NetworkManager integration, and system utilities.
%package -n dgop
Summary: Stateless CPU/GPU monitor for DankMaterialShell
License: MIT
URL: https://github.com/AvengeMedia/dgop
Provides: dgop
%description -n dgop
DGOP is a stateless system monitoring tool that provides CPU, GPU, memory, and
network statistics. Designed for integration with DankMaterialShell but can be
used standalone. This package always includes the latest stable dgop release.
%prep
%setup -q -c -n dms-qml
# Download architecture-specific binaries during build
# This ensures the correct architecture is used for each build target
case "%{_arch}" in
x86_64)
ARCH_SUFFIX="amd64"
;;
aarch64)
ARCH_SUFFIX="arm64"
;;
*)
echo "Unsupported architecture: %{_arch}"
exit 1
;;
esac
# Download dms-cli for target architecture
wget -O %{_builddir}/dms-cli.gz "https://github.com/AvengeMedia/danklinux/releases/latest/download/dms-distropkg-${ARCH_SUFFIX}.gz" || {
echo "Failed to download dms-cli for architecture %{_arch}"
exit 1
}
gunzip -c %{_builddir}/dms-cli.gz > %{_builddir}/dms-cli
chmod +x %{_builddir}/dms-cli
# Download dgop for target architecture
wget -O %{_builddir}/dgop.gz "https://github.com/AvengeMedia/dgop/releases/latest/download/dgop-linux-${ARCH_SUFFIX}.gz" || {
echo "Failed to download dgop for architecture %{_arch}"
exit 1
}
gunzip -c %{_builddir}/dgop.gz > %{_builddir}/dgop
chmod +x %{_builddir}/dgop
%build
%install
install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms
install -Dm755 %{_builddir}/dgop %{buildroot}%{_bindir}/dgop
install -dm755 %{buildroot}%{_sysconfdir}/xdg/quickshell/dms
cp -r %{_builddir}/dms-qml/* %{buildroot}%{_sysconfdir}/xdg/quickshell/dms/
rm -rf %{buildroot}%{_sysconfdir}/xdg/quickshell/dms/.git*
rm -f %{buildroot}%{_sysconfdir}/xdg/quickshell/dms/.gitignore
rm -rf %{buildroot}%{_sysconfdir}/xdg/quickshell/dms/.github
rm -f %{buildroot}%{_sysconfdir}/xdg/quickshell/dms/*.spec
%files
%license LICENSE
%doc README.md CONTRIBUTING.md
%{_sysconfdir}/xdg/quickshell/dms/
%files -n dms-cli
%{_bindir}/dms
%files -n dgop
%{_bindir}/dgop
%changelog
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-1
- Stable release VERSION_PLACEHOLDER
- Built from GitHub release
- Includes latest dms-cli and dgop binaries
SPECEOF
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/dms.spec
echo "✅ Spec file generated for v${VERSION}"
echo ""
echo "=== Spec file preview ==="
head -40 ~/rpmbuild/SPECS/dms.spec
- name: Build SRPM
id: build
run: |
cd ~/rpmbuild/SPECS
echo "🔨 Building SRPM..."
rpmbuild -bs dms.spec
SRPM=$(ls ~/rpmbuild/SRPMS/*.src.rpm | tail -n 1)
SRPM_NAME=$(basename "$SRPM")
echo "srpm_path=$SRPM" >> $GITHUB_OUTPUT
echo "srpm_name=$SRPM_NAME" >> $GITHUB_OUTPUT
echo "✅ SRPM built: $SRPM_NAME"
echo ""
echo "=== SRPM Info ==="
rpm -qpi "$SRPM"
- name: Upload SRPM artifact
uses: actions/upload-artifact@v4
with:
name: dms-stable-srpm-${{ steps.version.outputs.version }}
path: ${{ steps.build.outputs.srpm_path }}
retention-days: 90
- name: Install Copr CLI
run: |
sudo apt-get install -y python3-pip
pip3 install copr-cli
mkdir -p ~/.config
cat > ~/.config/copr << EOF
[copr-cli]
login = ${{ secrets.COPR_LOGIN }}
username = avengemedia
token = ${{ secrets.COPR_TOKEN }}
copr_url = https://copr.fedorainfracloud.org
EOF
chmod 600 ~/.config/copr
echo "✅ Copr CLI configured"
- name: Upload to Copr
run: |
SRPM="${{ steps.build.outputs.srpm_path }}"
VERSION="${{ steps.version.outputs.version }}"
echo "🚀 Uploading SRPM to avengemedia/dms..."
echo " SRPM: $(basename $SRPM)"
echo " Version: $VERSION"
BUILD_OUTPUT=$(copr-cli build avengemedia/dms "$SRPM" --nowait 2>&1)
echo "$BUILD_OUTPUT"
BUILD_ID=$(echo "$BUILD_OUTPUT" | grep -oP 'Build was added to.*\K[0-9]+' || echo "unknown")
if [ "$BUILD_ID" != "unknown" ]; then
echo "✅ Build submitted successfully!"
echo "🔗 https://copr.fedorainfracloud.org/coprs/avengemedia/dms/build/$BUILD_ID/"
else
echo "⚠️ Could not extract build ID, but upload may have succeeded"
fi
- name: Build summary
if: always()
run: |
echo "### 🎉 DMS Stable Build Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Version:** ${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "- **SRPM:** ${{ steps.build.outputs.srpm_name }}" >> $GITHUB_STEP_SUMMARY
echo "- **Project:** https://copr.fedorainfracloud.org/coprs/avengemedia/dms/" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Stable release has been built and uploaded to Copr!" >> $GITHUB_STEP_SUMMARY

107
.github/workflows/poeditor-export.yml vendored Normal file
View File

@@ -0,0 +1,107 @@
name: POEditor Diff & Sync
on:
push:
branches: [ master ]
workflow_dispatch: {}
concurrency:
group: poeditor-sync
cancel-in-progress: false
jobs:
sync-translations:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- name: Install jq
run: sudo apt-get update && sudo apt-get install -y jq
- name: Export and update translations from POEditor
env:
API_TOKEN: ${{ secrets.POEDITOR_API_TOKEN }}
PROJECT_ID: ${{ secrets.POEDITOR_PROJECT_ID }}
run: |
set -euo pipefail
LANGUAGES=(
"ja:translations/poexports/ja.json"
"zh-Hans:translations/poexports/zh_CN.json"
"pt-br:translations/poexports/pt.json"
)
ANY_CHANGED=false
for lang_pair in "${LANGUAGES[@]}"; do
IFS=':' read -r PO_LANG REPO_FILE <<< "$lang_pair"
echo "::group::Processing $PO_LANG"
RESP=$(curl -sS -X POST https://api.poeditor.com/v2/projects/export \
-d api_token="$API_TOKEN" \
-d id="$PROJECT_ID" \
-d language="$PO_LANG" \
-d type="key_value_json")
STATUS=$(echo "$RESP" | jq -r '.response.status')
if [[ "$STATUS" != "success" ]]; then
echo "POEditor export request failed for $PO_LANG: $RESP" >&2
continue
fi
URL=$(echo "$RESP" | jq -r '.result.url')
if [[ -z "$URL" || "$URL" == "null" ]]; then
echo "No export URL returned for $PO_LANG" >&2
continue
fi
curl -sS -L "$URL" -o "/tmp/po_export_${PO_LANG}.json"
jq -S . "/tmp/po_export_${PO_LANG}.json" > "/tmp/po_export_${PO_LANG}.norm.json"
if [[ -f "$REPO_FILE" ]]; then
jq -S . "$REPO_FILE" > "/tmp/repo_${PO_LANG}.norm.json" || echo "{}" > "/tmp/repo_${PO_LANG}.norm.json"
else
echo "{}" > "/tmp/repo_${PO_LANG}.norm.json"
fi
if diff -q "/tmp/po_export_${PO_LANG}.norm.json" "/tmp/repo_${PO_LANG}.norm.json" >/dev/null; then
echo "No changes for $PO_LANG"
else
echo "Detected changes for $PO_LANG"
mkdir -p "$(dirname "$REPO_FILE")"
cp "/tmp/po_export_${PO_LANG}.norm.json" "$REPO_FILE"
ANY_CHANGED=true
fi
echo "::endgroup::"
done
echo "any_changed=$ANY_CHANGED" >> "$GITHUB_OUTPUT"
id: export
- name: Commit and push translation updates
if: steps.export.outputs.any_changed == 'true'
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add translations/poexports/*.json
git commit -m "i18n: update translations"
for attempt in 1 2 3; do
if git push; then
echo "Successfully pushed translation updates"
exit 0
fi
echo "Push attempt $attempt failed, pulling and retrying..."
git pull --rebase
sleep $((attempt*2))
done
echo "Failed to push after retries" >&2
exit 1

View File

@@ -29,22 +29,18 @@ jobs:
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# create/update VERSION file to match incoming tag
echo "${TAG}" > VERSION
if ! git diff --quiet -- VERSION; then
git add VERSION
git commit -m "Add VERSION file for ${TAG} (from DMS)"
git add -A VERSION
if ! git diff --cached --quiet; then
git commit -m "Update VERSION to ${TAG} (from DMS)"
fi
# If tag doesn't exist (or differs), (re)create it
if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
echo "Tag ${TAG} already exists"
else
git tag "${TAG}"
fi
git tag -f "${TAG}"
# Push commit (if any) and tag
git push --follow-tags origin HEAD
git push origin HEAD
git push -f origin "${TAG}"
- name: Generate Changelog
id: changelog
@@ -57,7 +53,30 @@ jobs:
CHANGELOG=$(git log --oneline --pretty=format:"- %s (%h)" "${PREVIOUS_TAG}..${TAG}")
fi
cat > CHANGELOG.md << EOF
cat > RELEASE_BODY.md << 'EOF'
## Assets
### Complete Packages
- **`dms-full-amd64.tar.gz`** - Complete package for x86_64 systems (CLI binaries + QML source + installation guide)
- **`dms-full-arm64.tar.gz`** - Complete package for ARM64 systems (CLI binaries + QML source + installation guide)
### Individual Components
- **`dms-cli-amd64.gz`** - DMS CLI binary for x86_64 systems
- **`dms-cli-arm64.gz`** - DMS CLI binary for ARM64 systems
- **`dms-distropkg-amd64.gz`** - DMS CLI binary built with distro_package tag for AMD64 systems
- **`dms-distropkg-arm64.gz`** - DMS CLI binary built with distro_package tag for ARM64 systems
- **`dms-qml.tar.gz`** - QML source code only
### Checksums
- **`*.sha256`** - SHA256 checksums for verifying download integrity
**Installation:** Extract the `dms-full-*.tar.gz` package for your architecture and follow the `INSTALL.md` instructions inside.
---
EOF
cat >> RELEASE_BODY.md << EOF
## What's Changed
$CHANGELOG
@@ -66,7 +85,7 @@ jobs:
EOF
echo "changelog<<EOF" >> $GITHUB_OUTPUT
cat CHANGELOG.md >> $GITHUB_OUTPUT
cat RELEASE_BODY.md >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Create/Update DankMaterialShell Release
@@ -80,18 +99,121 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Download DMS release assets
- name: Download and prepare release assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euxo pipefail
# gh is preinstalled on ubuntu-24.04; auth via GH_TOKEN env
mkdir -p _release_assets
# Download DMS CLI binaries from the danklinux repo
gh release download "${TAG}" -R "${DMS_REPO}" --dir ./_dms_assets
- name: Attach DMS assets to DankMaterialShell release
# Rename CLI binaries to dms-cli-* format and copy distropkg binaries
for file in _dms_assets/dms-*.gz*; do
if [ -f "$file" ]; then
basename=$(basename "$file")
if [[ "$basename" == dms-distropkg-* ]]; then
cp "$file" "_release_assets/$basename"
else
newname=$(echo "$basename" | sed 's/^dms-/dms-cli-/')
cp "$file" "_release_assets/$newname"
fi
fi
done
# Create QML source package (exclude .git, .github, build artifacts)
tar --exclude='.git' \
--exclude='.github' \
--exclude='_dms_assets' \
--exclude='_release_assets' \
--exclude='*.tar.gz' \
-czf _release_assets/dms-qml.tar.gz .
# Generate checksum for QML package
(cd _release_assets && sha256sum dms-qml.tar.gz > dms-qml.tar.gz.sha256)
# Create full packages for each architecture
for arch in amd64 arm64; do
mkdir -p _temp_full/dms
mkdir -p _temp_full/bin
# Extract QML source to temp directory
tar -xzf _release_assets/dms-qml.tar.gz -C _temp_full/dms
# Copy CLI binary if it exists
if [ -f "_dms_assets/dms-${arch}.gz" ]; then
gunzip -c "_dms_assets/dms-${arch}.gz" > _temp_full/bin/dms
chmod +x _temp_full/bin/dms
fi
# Copy distropkg binary if it exists
if [ -f "_dms_assets/dms-distropkg-${arch}.gz" ]; then
gunzip -c "_dms_assets/dms-distropkg-${arch}.gz" > _temp_full/bin/dms-distropkg
chmod +x _temp_full/bin/dms-distropkg
fi
# Create INSTALL.md
cat > _temp_full/INSTALL.md << 'EOF'
# DankMaterialShell Installation
## Requirements
- Wayland compositor (niri or Hyprland recommended)
- Quickshell framework
- Qt6
## Installation Steps
1. **Install quickshell assets:**
```bash
mkdir -p ~/.config/quickshell
cp -r dms ~/.config/quickshell/
```
2. **Install the DMS CLI binaries:**
```bash
sudo install -m 755 bin/dms /usr/local/bin/dms
# or install to a local directory:
mkdir -p ~/.local/bin
install -m 755 bin/dms ~/.local/bin/dms
```
3. **Start the shell:**
```bash
dms run
# or directly with quickshell (will lack some dbus integrations & plugin management):
quickshell -p ~/.config/quickshell/dms
```
## Configuration
- Settings are stored in `~/.config/DankMaterialShell/settings.json`
- Plugins go in `~/.config/DankMaterialShell/plugins/`
- See the documentation in the `dms/` directory for more details
## Troubleshooting
- Run with verbose output: `quickshell -v -p ~/.config/quickshell/dms`
- Check logs in `~/.local/state/DankMaterialShell/`
- Ensure all dependencies are installed
EOF
# Create the full package
(cd _temp_full && tar -czf "../_release_assets/dms-full-${arch}.tar.gz" .)
# Generate checksum
(cd _release_assets && sha256sum "dms-full-${arch}.tar.gz" > "dms-full-${arch}.tar.gz.sha256")
# Cleanup
rm -rf _temp_full
done
- name: Attach all assets to release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.TAG }}
files: _dms_assets/**
files: _release_assets/**
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

122
Common/CacheData.qml Normal file
View File

@@ -0,0 +1,122 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtCore
import QtQuick
import Quickshell
import Quickshell.Io
Singleton {
id: root
readonly property int cacheConfigVersion: 1
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericCacheLocation)
readonly property string _stateDir: Paths.strip(_stateUrl)
property bool _loading: false
property string wallpaperLastPath: ""
property string profileLastPath: ""
Component.onCompleted: {
if (!isGreeterMode) {
loadCache()
}
}
function loadCache() {
_loading = true
parseCache(cacheFile.text())
_loading = false
}
function parseCache(content) {
_loading = true
try {
if (content && content.trim()) {
const cache = JSON.parse(content)
wallpaperLastPath = cache.wallpaperLastPath !== undefined ? cache.wallpaperLastPath : ""
profileLastPath = cache.profileLastPath !== undefined ? cache.profileLastPath : ""
if (cache.configVersion === undefined) {
migrateFromUndefinedToV1(cache)
cleanupUnusedKeys()
saveCache()
}
}
} catch (e) {
console.warn("CacheData: Failed to parse cache:", e.message)
} finally {
_loading = false
}
}
function saveCache() {
if (_loading)
return
cacheFile.setText(JSON.stringify({
"wallpaperLastPath": wallpaperLastPath,
"profileLastPath": profileLastPath,
"configVersion": cacheConfigVersion
}, null, 2))
}
function migrateFromUndefinedToV1(cache) {
console.log("CacheData: Migrating configuration from undefined to version 1")
}
function cleanupUnusedKeys() {
const validKeys = [
"wallpaperLastPath",
"profileLastPath",
"configVersion"
]
try {
const content = cacheFile.text()
if (!content || !content.trim()) return
const cache = JSON.parse(content)
let needsSave = false
for (const key in cache) {
if (!validKeys.includes(key)) {
console.log("CacheData: Removing unused key:", key)
delete cache[key]
needsSave = true
}
}
if (needsSave) {
cacheFile.setText(JSON.stringify(cache, null, 2))
}
} catch (e) {
console.warn("CacheData: Failed to cleanup unused keys:", e.message)
}
}
FileView {
id: cacheFile
path: isGreeterMode ? "" : _stateDir + "/DankMaterialShell/cache.json"
blockLoading: true
blockWrites: true
atomicWrites: true
watchChanges: !isGreeterMode
onLoaded: {
if (!isGreeterMode) {
parseCache(cacheFile.text())
}
}
onLoadFailed: error => {
if (!isGreeterMode) {
console.log("CacheData: No cache file found, starting fresh")
}
}
}
}

70
Common/Proc.qml Normal file
View File

@@ -0,0 +1,70 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
Singleton {
id: root
property int defaultDebounceMs: 50
property var _procDebouncers: ({}) // id -> { timer, command, callback, waitMs }
function runCommand(id, command, callback, debounceMs) {
const wait = (typeof debounceMs === "number" && debounceMs >= 0) ? debounceMs : defaultDebounceMs
let procId = id ? id : Math.random()
if (!_procDebouncers[procId]) {
const t = Qt.createQmlObject('import QtQuick; Timer { repeat: false }', root)
t.triggered.connect(function() { _launchProc(procId) })
_procDebouncers[procId] = { timer: t, command: command, callback: callback, waitMs: wait }
} else {
_procDebouncers[procId].command = command
_procDebouncers[procId].callback = callback
_procDebouncers[procId].waitMs = wait
}
const entry = _procDebouncers[procId]
entry.timer.interval = entry.waitMs
entry.timer.restart()
}
function _launchProc(id) {
const entry = _procDebouncers[id]
if (!entry) return
const proc = Qt.createQmlObject('import Quickshell.Io; Process { running: false }', root)
const out = Qt.createQmlObject('import Quickshell.Io; StdioCollector {}', proc)
const err = Qt.createQmlObject('import Quickshell.Io; StdioCollector {}', proc)
proc.stdout = out
proc.stderr = err
proc.command = entry.command
let capturedOut = ""
let exitSeen = false
let exitCodeValue = -1
out.streamFinished.connect(function() {
capturedOut = out.text || ""
maybeComplete()
})
proc.exited.connect(function(code) {
exitSeen = true
exitCodeValue = code
maybeComplete()
})
function maybeComplete() {
if (!exitSeen) return
if (typeof entry.callback === "function") {
try { entry.callback(capturedOut, exitCodeValue) } catch (e) { console.warn("runCommand callback error:", e) }
}
try { proc.destroy() } catch (_) {}
}
proc.running = true
}
}

View File

@@ -9,15 +9,19 @@ import qs.Common
import qs.Services
Singleton {
id: root
readonly property int sessionConfigVersion: 1
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
property bool hasTriedDefaultSession: false
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation)
readonly property string _stateDir: Paths.strip(_stateUrl)
property bool isLightMode: false
property bool doNotDisturb: false
property string wallpaperPath: ""
property string wallpaperLastPath: ""
property string profileLastPath: ""
property bool perMonitorWallpaper: false
property var monitorWallpapers: ({})
property bool perModeWallpaper: false
@@ -25,15 +29,20 @@ Singleton {
property string wallpaperPathDark: ""
property var monitorWallpapersLight: ({})
property var monitorWallpapersDark: ({})
property bool doNotDisturb: false
property string wallpaperTransition: "fade"
readonly property var availableWallpaperTransitions: ["none", "fade", "wipe", "disc", "stripes", "iris bloom", "pixelate", "portal"]
property var includedTransitions: availableWallpaperTransitions.filter(t => t !== "none")
property bool wallpaperCyclingEnabled: false
property string wallpaperCyclingMode: "interval"
property int wallpaperCyclingInterval: 300
property string wallpaperCyclingTime: "06:00"
property var monitorCyclingSettings: ({})
property bool nightModeEnabled: false
property int nightModeTemperature: 4500
property bool nightModeAutoEnabled: false
property string nightModeAutoMode: "time"
property bool hasTriedDefaultSession: false
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation)
readonly property string _stateDir: Paths.strip(_stateUrl)
property int nightModeStartHour: 18
property int nightModeStartMinute: 0
property int nightModeEndHour: 6
@@ -41,39 +50,17 @@ Singleton {
property real latitude: 0.0
property real longitude: 0.0
property string nightModeLocationProvider: ""
property var pinnedApps: []
property var recentColors: []
property bool showThirdPartyPlugins: false
property string launchPrefix: ""
property string lastBrightnessDevice: ""
property int selectedGpuIndex: 0
property bool nvidiaGpuTempEnabled: false
property bool nonNvidiaGpuTempEnabled: false
property var enabledGpuPciIds: []
property bool wallpaperCyclingEnabled: false
property string wallpaperCyclingMode: "interval" // "interval" or "time"
property int wallpaperCyclingInterval: 300 // seconds (5 minutes)
property string wallpaperCyclingTime: "06:00" // HH:mm format
property var monitorCyclingSettings: ({})
property string lastBrightnessDevice: ""
property string launchPrefix: ""
property string wallpaperTransition: "fade"
readonly property var availableWallpaperTransitions: ["none", "fade", "wipe", "disc", "stripes", "iris bloom", "pixelate", "portal"]
property var includedTransitions: availableWallpaperTransitions.filter(t => t !== "none")
// Power management settings - AC Power
property int acMonitorTimeout: 0 // Never
property int acLockTimeout: 0 // Never
property int acSuspendTimeout: 0 // Never
property int acHibernateTimeout: 0 // Never
// Power management settings - Battery
property int batteryMonitorTimeout: 0 // Never
property int batteryLockTimeout: 0 // Never
property int batterySuspendTimeout: 0 // Never
property int batteryHibernateTimeout: 0 // Never
property bool lockBeforeSuspend: false
property bool loginctlLockIntegration: true
property var recentColors: []
property bool showThirdPartyPlugins: false
Component.onCompleted: {
if (!isGreeterMode) {
@@ -95,8 +82,6 @@ Singleton {
var settings = JSON.parse(content)
isLightMode = settings.isLightMode !== undefined ? settings.isLightMode : false
wallpaperPath = settings.wallpaperPath !== undefined ? settings.wallpaperPath : ""
wallpaperLastPath = settings.wallpaperLastPath !== undefined ? settings.wallpaperLastPath : ""
profileLastPath = settings.profileLastPath !== undefined ? settings.profileLastPath : ""
perMonitorWallpaper = settings.perMonitorWallpaper !== undefined ? settings.perMonitorWallpaper : false
monitorWallpapers = settings.monitorWallpapers !== undefined ? settings.monitorWallpapers : {}
perModeWallpaper = settings.perModeWallpaper !== undefined ? settings.perModeWallpaper : false
@@ -109,7 +94,6 @@ Singleton {
nightModeTemperature = settings.nightModeTemperature !== undefined ? settings.nightModeTemperature : 4500
nightModeAutoEnabled = settings.nightModeAutoEnabled !== undefined ? settings.nightModeAutoEnabled : false
nightModeAutoMode = settings.nightModeAutoMode !== undefined ? settings.nightModeAutoMode : "time"
// Handle legacy time format
if (settings.nightModeStartTime !== undefined) {
const parts = settings.nightModeStartTime.split(":")
nightModeStartHour = parseInt(parts[0]) || 18
@@ -143,20 +127,16 @@ Singleton {
launchPrefix = settings.launchPrefix !== undefined ? settings.launchPrefix : ""
wallpaperTransition = settings.wallpaperTransition !== undefined ? settings.wallpaperTransition : "fade"
includedTransitions = settings.includedTransitions !== undefined ? settings.includedTransitions : availableWallpaperTransitions.filter(t => t !== "none")
acMonitorTimeout = settings.acMonitorTimeout !== undefined ? settings.acMonitorTimeout : 0
acLockTimeout = settings.acLockTimeout !== undefined ? settings.acLockTimeout : 0
acSuspendTimeout = settings.acSuspendTimeout !== undefined ? settings.acSuspendTimeout : 0
acHibernateTimeout = settings.acHibernateTimeout !== undefined ? settings.acHibernateTimeout : 0
batteryMonitorTimeout = settings.batteryMonitorTimeout !== undefined ? settings.batteryMonitorTimeout : 0
batteryLockTimeout = settings.batteryLockTimeout !== undefined ? settings.batteryLockTimeout : 0
batterySuspendTimeout = settings.batterySuspendTimeout !== undefined ? settings.batterySuspendTimeout : 0
batteryHibernateTimeout = settings.batteryHibernateTimeout !== undefined ? settings.batteryHibernateTimeout : 0
lockBeforeSuspend = settings.lockBeforeSuspend !== undefined ? settings.lockBeforeSuspend : false
loginctlLockIntegration = settings.loginctlLockIntegration !== undefined ? settings.loginctlLockIntegration : true
recentColors = settings.recentColors !== undefined ? settings.recentColors : []
showThirdPartyPlugins = settings.showThirdPartyPlugins !== undefined ? settings.showThirdPartyPlugins : false
if (settings.configVersion === undefined) {
migrateFromUndefinedToV1(settings)
saveSettings()
} else if (settings.configVersion === sessionConfigVersion) {
cleanupUnusedKeys()
}
if (!isGreeterMode) {
if (typeof Theme !== "undefined") {
Theme.generateSystemThemesFromCurrentTheme()
@@ -173,8 +153,6 @@ Singleton {
settingsFile.setText(JSON.stringify({
"isLightMode": isLightMode,
"wallpaperPath": wallpaperPath,
"wallpaperLastPath": wallpaperLastPath,
"profileLastPath": profileLastPath,
"perMonitorWallpaper": perMonitorWallpaper,
"monitorWallpapers": monitorWallpapers,
"perModeWallpaper": perModeWallpaper,
@@ -208,101 +186,109 @@ Singleton {
"launchPrefix": launchPrefix,
"wallpaperTransition": wallpaperTransition,
"includedTransitions": includedTransitions,
"acMonitorTimeout": acMonitorTimeout,
"acLockTimeout": acLockTimeout,
"acSuspendTimeout": acSuspendTimeout,
"acHibernateTimeout": acHibernateTimeout,
"batteryMonitorTimeout": batteryMonitorTimeout,
"batteryLockTimeout": batteryLockTimeout,
"batterySuspendTimeout": batterySuspendTimeout,
"batteryHibernateTimeout": batteryHibernateTimeout,
"lockBeforeSuspend": lockBeforeSuspend,
"loginctlLockIntegration": loginctlLockIntegration,
"recentColors": recentColors,
"showThirdPartyPlugins": showThirdPartyPlugins
"showThirdPartyPlugins": showThirdPartyPlugins,
"configVersion": sessionConfigVersion
}, null, 2))
}
function migrateFromUndefinedToV1(settings) {
console.log("SessionData: Migrating configuration from undefined to version 1")
if (typeof SettingsData !== "undefined") {
if (settings.acMonitorTimeout !== undefined) {
SettingsData.setAcMonitorTimeout(settings.acMonitorTimeout)
}
if (settings.acLockTimeout !== undefined) {
SettingsData.setAcLockTimeout(settings.acLockTimeout)
}
if (settings.acSuspendTimeout !== undefined) {
SettingsData.setAcSuspendTimeout(settings.acSuspendTimeout)
}
if (settings.acHibernateTimeout !== undefined) {
SettingsData.setAcHibernateTimeout(settings.acHibernateTimeout)
}
if (settings.batteryMonitorTimeout !== undefined) {
SettingsData.setBatteryMonitorTimeout(settings.batteryMonitorTimeout)
}
if (settings.batteryLockTimeout !== undefined) {
SettingsData.setBatteryLockTimeout(settings.batteryLockTimeout)
}
if (settings.batterySuspendTimeout !== undefined) {
SettingsData.setBatterySuspendTimeout(settings.batterySuspendTimeout)
}
if (settings.batteryHibernateTimeout !== undefined) {
SettingsData.setBatteryHibernateTimeout(settings.batteryHibernateTimeout)
}
if (settings.lockBeforeSuspend !== undefined) {
SettingsData.setLockBeforeSuspend(settings.lockBeforeSuspend)
}
if (settings.loginctlLockIntegration !== undefined) {
SettingsData.setLoginctlLockIntegration(settings.loginctlLockIntegration)
}
if (settings.launchPrefix !== undefined) {
SettingsData.setLaunchPrefix(settings.launchPrefix)
}
}
if (typeof CacheData !== "undefined") {
if (settings.wallpaperLastPath !== undefined) {
CacheData.wallpaperLastPath = settings.wallpaperLastPath
}
if (settings.profileLastPath !== undefined) {
CacheData.profileLastPath = settings.profileLastPath
}
CacheData.saveCache()
}
}
function cleanupUnusedKeys() {
const validKeys = [
"isLightMode", "wallpaperPath", "perMonitorWallpaper", "monitorWallpapers", "perModeWallpaper",
"wallpaperPathLight", "wallpaperPathDark", "monitorWallpapersLight",
"monitorWallpapersDark", "doNotDisturb", "nightModeEnabled",
"nightModeTemperature", "nightModeAutoEnabled", "nightModeAutoMode",
"nightModeStartHour", "nightModeStartMinute", "nightModeEndHour",
"nightModeEndMinute", "latitude", "longitude", "nightModeLocationProvider",
"pinnedApps", "selectedGpuIndex", "nvidiaGpuTempEnabled",
"nonNvidiaGpuTempEnabled", "enabledGpuPciIds", "wallpaperCyclingEnabled",
"wallpaperCyclingMode", "wallpaperCyclingInterval", "wallpaperCyclingTime",
"monitorCyclingSettings", "lastBrightnessDevice", "wallpaperTransition",
"includedTransitions", "recentColors", "showThirdPartyPlugins", "configVersion"
]
try {
const content = settingsFile.text()
if (!content || !content.trim()) return
const settings = JSON.parse(content)
let needsSave = false
for (const key in settings) {
if (!validKeys.includes(key)) {
console.log("SessionData: Removing unused key:", key)
delete settings[key]
needsSave = true
}
}
if (needsSave) {
settingsFile.setText(JSON.stringify(settings, null, 2))
}
} catch (e) {
console.warn("SessionData: Failed to cleanup unused keys:", e.message)
}
}
function setLightMode(lightMode) {
isLightMode = lightMode
syncWallpaperForCurrentMode()
saveSettings()
}
function syncWallpaperForCurrentMode() {
if (!perModeWallpaper) return
if (perMonitorWallpaper) {
monitorWallpapers = isLightMode ? Object.assign({}, monitorWallpapersLight) : Object.assign({}, monitorWallpapersDark)
return
}
wallpaperPath = isLightMode ? wallpaperPathLight : wallpaperPathDark
}
function setDoNotDisturb(enabled) {
doNotDisturb = enabled
saveSettings()
}
function setNightModeEnabled(enabled) {
nightModeEnabled = enabled
saveSettings()
}
function setNightModeTemperature(temperature) {
nightModeTemperature = temperature
saveSettings()
}
function setNightModeAutoEnabled(enabled) {
console.log("SessionData: Setting nightModeAutoEnabled to", enabled)
nightModeAutoEnabled = enabled
saveSettings()
}
function setNightModeAutoMode(mode) {
nightModeAutoMode = mode
saveSettings()
}
function setNightModeStartHour(hour) {
nightModeStartHour = hour
saveSettings()
}
function setNightModeStartMinute(minute) {
nightModeStartMinute = minute
saveSettings()
}
function setNightModeEndHour(hour) {
nightModeEndHour = hour
saveSettings()
}
function setNightModeEndMinute(minute) {
nightModeEndMinute = minute
saveSettings()
}
function setLatitude(lat) {
console.log("SessionData: Setting latitude to", lat)
latitude = lat
saveSettings()
}
function setLongitude(lng) {
console.log("SessionData: Setting longitude to", lng)
longitude = lng
saveSettings()
}
function setNightModeLocationProvider(provider) {
nightModeLocationProvider = provider
saveSettings()
}
function setWallpaperPath(path) {
wallpaperPath = path
saveSettings()
@@ -353,141 +339,6 @@ Singleton {
}
}
function setWallpaperLastPath(path) {
wallpaperLastPath = path
saveSettings()
}
function setProfileLastPath(path) {
profileLastPath = path
saveSettings()
}
function setPinnedApps(apps) {
pinnedApps = apps
saveSettings()
}
function addRecentColor(color) {
const colorStr = color.toString()
let recent = recentColors.slice()
recent = recent.filter(c => c !== colorStr)
recent.unshift(colorStr)
if (recent.length > 5) recent = recent.slice(0, 5)
recentColors = recent
saveSettings()
}
function addPinnedApp(appId) {
if (!appId)
return
var currentPinned = [...pinnedApps]
if (currentPinned.indexOf(appId) === -1) {
currentPinned.push(appId)
setPinnedApps(currentPinned)
}
}
function removePinnedApp(appId) {
if (!appId)
return
var currentPinned = pinnedApps.filter(id => id !== appId)
setPinnedApps(currentPinned)
}
function isPinnedApp(appId) {
return appId && pinnedApps.indexOf(appId) !== -1
}
function setSelectedGpuIndex(index) {
selectedGpuIndex = index
saveSettings()
}
function setNvidiaGpuTempEnabled(enabled) {
nvidiaGpuTempEnabled = enabled
saveSettings()
}
function setNonNvidiaGpuTempEnabled(enabled) {
nonNvidiaGpuTempEnabled = enabled
saveSettings()
}
function setEnabledGpuPciIds(pciIds) {
enabledGpuPciIds = pciIds
saveSettings()
}
function setWallpaperCyclingEnabled(enabled) {
wallpaperCyclingEnabled = enabled
saveSettings()
}
function setWallpaperCyclingMode(mode) {
wallpaperCyclingMode = mode
saveSettings()
}
function setWallpaperCyclingInterval(interval) {
wallpaperCyclingInterval = interval
saveSettings()
}
function setWallpaperCyclingTime(time) {
wallpaperCyclingTime = time
saveSettings()
}
function getMonitorCyclingSettings(screenName) {
return monitorCyclingSettings[screenName] || {
enabled: false,
mode: "interval",
interval: 300,
time: "06:00"
}
}
function setMonitorCyclingEnabled(screenName, enabled) {
var newSettings = Object.assign({}, monitorCyclingSettings)
if (!newSettings[screenName]) {
newSettings[screenName] = { enabled: false, mode: "interval", interval: 300, time: "06:00" }
}
newSettings[screenName].enabled = enabled
monitorCyclingSettings = newSettings
saveSettings()
}
function setMonitorCyclingMode(screenName, mode) {
var newSettings = Object.assign({}, monitorCyclingSettings)
if (!newSettings[screenName]) {
newSettings[screenName] = { enabled: false, mode: "interval", interval: 300, time: "06:00" }
}
newSettings[screenName].mode = mode
monitorCyclingSettings = newSettings
saveSettings()
}
function setMonitorCyclingInterval(screenName, interval) {
var newSettings = Object.assign({}, monitorCyclingSettings)
if (!newSettings[screenName]) {
newSettings[screenName] = { enabled: false, mode: "interval", interval: 300, time: "06:00" }
}
newSettings[screenName].interval = interval
monitorCyclingSettings = newSettings
saveSettings()
}
function setMonitorCyclingTime(screenName, time) {
var newSettings = Object.assign({}, monitorCyclingSettings)
if (!newSettings[screenName]) {
newSettings[screenName] = { enabled: false, mode: "interval", interval: 300, time: "06:00" }
}
newSettings[screenName].time = time
monitorCyclingSettings = newSettings
saveSettings()
}
function setPerMonitorWallpaper(enabled) {
perMonitorWallpaper = enabled
if (enabled && perModeWallpaper) {
@@ -579,15 +430,167 @@ Singleton {
}
}
function getMonitorWallpaper(screenName) {
if (!perMonitorWallpaper) {
return wallpaperPath
}
return monitorWallpapers[screenName] || wallpaperPath
function setWallpaperTransition(transition) {
wallpaperTransition = transition
saveSettings()
}
function setLastBrightnessDevice(device) {
lastBrightnessDevice = device
function setWallpaperCyclingEnabled(enabled) {
wallpaperCyclingEnabled = enabled
saveSettings()
}
function setWallpaperCyclingMode(mode) {
wallpaperCyclingMode = mode
saveSettings()
}
function setWallpaperCyclingInterval(interval) {
wallpaperCyclingInterval = interval
saveSettings()
}
function setWallpaperCyclingTime(time) {
wallpaperCyclingTime = time
saveSettings()
}
function setMonitorCyclingEnabled(screenName, enabled) {
var newSettings = Object.assign({}, monitorCyclingSettings)
if (!newSettings[screenName]) {
newSettings[screenName] = { enabled: false, mode: "interval", interval: 300, time: "06:00" }
}
newSettings[screenName].enabled = enabled
monitorCyclingSettings = newSettings
saveSettings()
}
function setMonitorCyclingMode(screenName, mode) {
var newSettings = Object.assign({}, monitorCyclingSettings)
if (!newSettings[screenName]) {
newSettings[screenName] = { enabled: false, mode: "interval", interval: 300, time: "06:00" }
}
newSettings[screenName].mode = mode
monitorCyclingSettings = newSettings
saveSettings()
}
function setMonitorCyclingInterval(screenName, interval) {
var newSettings = Object.assign({}, monitorCyclingSettings)
if (!newSettings[screenName]) {
newSettings[screenName] = { enabled: false, mode: "interval", interval: 300, time: "06:00" }
}
newSettings[screenName].interval = interval
monitorCyclingSettings = newSettings
saveSettings()
}
function setMonitorCyclingTime(screenName, time) {
var newSettings = Object.assign({}, monitorCyclingSettings)
if (!newSettings[screenName]) {
newSettings[screenName] = { enabled: false, mode: "interval", interval: 300, time: "06:00" }
}
newSettings[screenName].time = time
monitorCyclingSettings = newSettings
saveSettings()
}
function setNightModeEnabled(enabled) {
nightModeEnabled = enabled
saveSettings()
}
function setNightModeTemperature(temperature) {
nightModeTemperature = temperature
saveSettings()
}
function setNightModeAutoEnabled(enabled) {
console.log("SessionData: Setting nightModeAutoEnabled to", enabled)
nightModeAutoEnabled = enabled
saveSettings()
}
function setNightModeAutoMode(mode) {
nightModeAutoMode = mode
saveSettings()
}
function setNightModeStartHour(hour) {
nightModeStartHour = hour
saveSettings()
}
function setNightModeStartMinute(minute) {
nightModeStartMinute = minute
saveSettings()
}
function setNightModeEndHour(hour) {
nightModeEndHour = hour
saveSettings()
}
function setNightModeEndMinute(minute) {
nightModeEndMinute = minute
saveSettings()
}
function setLatitude(lat) {
console.log("SessionData: Setting latitude to", lat)
latitude = lat
saveSettings()
}
function setLongitude(lng) {
console.log("SessionData: Setting longitude to", lng)
longitude = lng
saveSettings()
}
function setNightModeLocationProvider(provider) {
nightModeLocationProvider = provider
saveSettings()
}
function setPinnedApps(apps) {
pinnedApps = apps
saveSettings()
}
function addPinnedApp(appId) {
if (!appId)
return
var currentPinned = [...pinnedApps]
if (currentPinned.indexOf(appId) === -1) {
currentPinned.push(appId)
setPinnedApps(currentPinned)
}
}
function removePinnedApp(appId) {
if (!appId)
return
var currentPinned = pinnedApps.filter(id => id !== appId)
setPinnedApps(currentPinned)
}
function isPinnedApp(appId) {
return appId && pinnedApps.indexOf(appId) !== -1
}
function addRecentColor(color) {
const colorStr = color.toString()
let recent = recentColors.slice()
recent = recent.filter(c => c !== colorStr)
recent.unshift(colorStr)
if (recent.length > 5) recent = recent.slice(0, 5)
recentColors = recent
saveSettings()
}
function setShowThirdPartyPlugins(enabled) {
showThirdPartyPlugins = enabled
saveSettings()
}
@@ -596,64 +599,56 @@ Singleton {
saveSettings()
}
function setWallpaperTransition(transition) {
wallpaperTransition = transition
function setLastBrightnessDevice(device) {
lastBrightnessDevice = device
saveSettings()
}
function setAcMonitorTimeout(timeout) {
acMonitorTimeout = timeout
function setSelectedGpuIndex(index) {
selectedGpuIndex = index
saveSettings()
}
function setAcLockTimeout(timeout) {
acLockTimeout = timeout
function setNvidiaGpuTempEnabled(enabled) {
nvidiaGpuTempEnabled = enabled
saveSettings()
}
function setAcSuspendTimeout(timeout) {
acSuspendTimeout = timeout
function setNonNvidiaGpuTempEnabled(enabled) {
nonNvidiaGpuTempEnabled = enabled
saveSettings()
}
function setBatteryMonitorTimeout(timeout) {
batteryMonitorTimeout = timeout
function setEnabledGpuPciIds(pciIds) {
enabledGpuPciIds = pciIds
saveSettings()
}
function setBatteryLockTimeout(timeout) {
batteryLockTimeout = timeout
saveSettings()
function syncWallpaperForCurrentMode() {
if (!perModeWallpaper) return
if (perMonitorWallpaper) {
monitorWallpapers = isLightMode ? Object.assign({}, monitorWallpapersLight) : Object.assign({}, monitorWallpapersDark)
return
}
wallpaperPath = isLightMode ? wallpaperPathLight : wallpaperPathDark
}
function setBatterySuspendTimeout(timeout) {
batterySuspendTimeout = timeout
saveSettings()
function getMonitorWallpaper(screenName) {
if (!perMonitorWallpaper) {
return wallpaperPath
}
return monitorWallpapers[screenName] || wallpaperPath
}
function setAcHibernateTimeout(timeout) {
acHibernateTimeout = timeout
saveSettings()
}
function setBatteryHibernateTimeout(timeout) {
batteryHibernateTimeout = timeout
saveSettings()
}
function setLockBeforeSuspend(enabled) {
lockBeforeSuspend = enabled
saveSettings()
}
function setLoginctlLockIntegration(enabled) {
loginctlLockIntegration = enabled
saveSettings()
}
function setShowThirdPartyPlugins(enabled) {
showThirdPartyPlugins = enabled
saveSettings()
function getMonitorCyclingSettings(screenName) {
return monitorCyclingSettings[screenName] || {
enabled: false,
mode: "interval",
interval: 300,
time: "06:00"
}
}
FileView {

File diff suppressed because it is too large Load Diff

View File

@@ -14,14 +14,19 @@ import "StockThemes.js" as StockThemes
Singleton {
id: root
readonly property string stateDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.GenericCacheLocation).toString()) + "/DankMaterialShell"
readonly property bool envDisableMatugen: Quickshell.env("DMS_DISABLE_MATUGEN") === "1" || Quickshell.env("DMS_DISABLE_MATUGEN") === "true"
// ! TODO - Synchronize with niri/hyprland gaps?
readonly property real popupDistance: 2
readonly property real popupDistance: {
if (typeof SettingsData === "undefined") return 4
return SettingsData.popupGapsAuto ? Math.max(4, SettingsData.dankBarSpacing) : SettingsData.popupGapsManual
}
property string currentTheme: "blue"
property string currentThemeCategory: "generic"
property bool isLightMode: typeof SessionData !== "undefined" ? SessionData.isLightMode : false
property bool colorsFileLoadFailed: false
readonly property string dynamic: "dynamic"
readonly property string custom : "custom"
@@ -76,11 +81,62 @@ Singleton {
property var matugenColors: ({})
property var customThemeData: null
readonly property string stateDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.CacheLocation).toString()) + "/dankshell"
Component.onCompleted: {
Quickshell.execDetached(["mkdir", "-p", stateDir])
matugenCheck.running = true
Proc.runCommand("matugenCheck", ["which", "matugen"], (output, code) => {
matugenAvailable = (code === 0) && !envDisableMatugen
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode)
if (!matugenAvailable || isGreeterMode) {
return
}
if (colorsFileLoadFailed && currentTheme === dynamic && wallpaperPath) {
console.log("Theme: Matugen now available, regenerating colors for dynamic theme")
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode)
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default"
Quickshell.execDetached(["rm", "-f", stateDir + "/matugen.key"])
const selectedMatugenType = (typeof SettingsData !== "undefined" && SettingsData.matugenScheme) ? SettingsData.matugenScheme : "scheme-tonal-spot"
if (wallpaperPath.startsWith("#")) {
setDesiredTheme("hex", wallpaperPath, isLight, iconTheme, selectedMatugenType)
} else {
setDesiredTheme("image", wallpaperPath, isLight, iconTheme, selectedMatugenType)
}
return
}
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode)
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default"
if (currentTheme === dynamic) {
if (wallpaperPath) {
Quickshell.execDetached(["rm", "-f", stateDir + "/matugen.key"])
const selectedMatugenType = (typeof SettingsData !== "undefined" && SettingsData.matugenScheme) ? SettingsData.matugenScheme : "scheme-tonal-spot"
if (wallpaperPath.startsWith("#")) {
setDesiredTheme("hex", wallpaperPath, isLight, iconTheme, selectedMatugenType)
} else {
setDesiredTheme("image", wallpaperPath, isLight, iconTheme, selectedMatugenType)
}
}
} else {
let primaryColor
let matugenType
if (currentTheme === "custom") {
if (customThemeData && customThemeData.primary) {
primaryColor = customThemeData.primary
matugenType = customThemeData.matugen_type
}
} else {
primaryColor = currentThemeData.primary
matugenType = currentThemeData.matugen_type
}
if (primaryColor) {
Quickshell.execDetached(["rm", "-f", stateDir + "/matugen.key"])
setDesiredTheme("hex", primaryColor, isLight, iconTheme, matugenType)
}
}
}, 0)
if (typeof SessionData !== "undefined") {
SessionData.isLightModeChanged.connect(root.onLightModeChanged)
}
@@ -325,11 +381,10 @@ Singleton {
}
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode)
isLightMode = light
if (savePrefs && typeof SessionData !== "undefined" && !isGreeterMode)
SessionData.setLightMode(isLightMode)
SessionData.setLightMode(light)
if (!isGreeterMode) {
PortalService.setLightMode(isLightMode)
PortalService.setLightMode(light)
generateSystemThemesFromCurrentTheme()
}
}
@@ -582,10 +637,12 @@ Singleton {
function setDesiredTheme(kind, value, isLight, iconTheme, matugenType) {
if (!matugenAvailable) {
console.warn("matugen not available or disabled - cannot set system theme")
console.warn("Theme: matugen not available or disabled - cannot set system theme")
return
}
console.log("Theme: Setting desired theme -", kind, "mode:", isLight ? "light" : "dark", "type:", matugenType)
if (typeof NiriService !== "undefined" && CompositorService.isNiri) {
NiriService.suppressNextToast()
}
@@ -596,7 +653,8 @@ Singleton {
"mode": isLight ? "light" : "dark",
"iconTheme": iconTheme || "System Default",
"matugenType": matugenType || "scheme-tonal-spot",
"surfaceBase": (typeof SettingsData !== "undefined" && SettingsData.surfaceBase) ? SettingsData.surfaceBase : "sc"
"surfaceBase": (typeof SettingsData !== "undefined" && SettingsData.surfaceBase) ? SettingsData.surfaceBase : "sc",
"runUserTemplates": (typeof SettingsData !== "undefined") ? SettingsData.runUserMatugenTemplates : true
}
const json = JSON.stringify(desired)
@@ -605,12 +663,13 @@ Singleton {
Quickshell.execDetached(["sh", "-c", `mkdir -p '${stateDir}' && cat > '${desiredPath}' << 'EOF'\n${json}\nEOF`])
workerRunning = true
if (rawWallpaperPath.startsWith("we:")) {
console.log("calling matugen worker")
console.log("Theme: Starting matugen worker (WE wallpaper)")
systemThemeGenerator.command = [
"sh", "-c",
`sleep 1 && ${shellDir}/scripts/matugen-worker.sh '${stateDir}' '${shellDir}' '${configDir}' --run`
]
} else {
console.log("Theme: Starting matugen worker")
systemThemeGenerator.command = [shellDir + "/scripts/matugen-worker.sh", stateDir, shellDir, configDir, "--run"]
}
systemThemeGenerator.running = true
@@ -666,8 +725,17 @@ Singleton {
}
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "true" : "false"
gtkApplier.command = [shellDir + "/scripts/gtk.sh", configDir, isLight, shellDir]
gtkApplier.running = true
Proc.runCommand("gtkApplier", [shellDir + "/scripts/gtk.sh", configDir, isLight, shellDir], (output, exitCode) => {
if (exitCode === 0) {
if (typeof ToastService !== "undefined" && typeof NiriService !== "undefined" && !NiriService.matugenSuppression) {
ToastService.showInfo("GTK colors applied successfully")
}
} else {
if (typeof ToastService !== "undefined") {
ToastService.showError("Failed to apply GTK colors")
}
}
})
}
function applyQtColors() {
@@ -678,14 +746,33 @@ Singleton {
return
}
qtApplier.command = [shellDir + "/scripts/qt.sh", configDir]
qtApplier.running = true
Proc.runCommand("qtApplier", [shellDir + "/scripts/qt.sh", configDir], (output, exitCode) => {
if (exitCode === 0) {
if (typeof ToastService !== "undefined") {
ToastService.showInfo("Qt colors applied successfully")
}
} else {
if (typeof ToastService !== "undefined") {
ToastService.showError("Failed to apply Qt colors")
}
}
})
}
function withAlpha(c, a) { return Qt.rgba(c.r, c.g, c.b, a); }
function snap(value, dpr) {
return Math.round(value * dpr) / dpr
const s = dpr || 1
return Math.round(value * s) / s
}
function px(value, dpr) {
const s = dpr || 1
return Math.round(value * s) / s
}
function hairline(dpr) {
return 1 / (dpr || 1)
}
function invertHex(hex) {
@@ -737,57 +824,6 @@ Singleton {
Process {
id: matugenCheck
command: ["which", "matugen"]
onExited: code => {
matugenAvailable = (code === 0) && !envDisableMatugen
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode)
if (!matugenAvailable || isGreeterMode) {
return
}
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode)
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default"
if (currentTheme === dynamic) {
if (wallpaperPath) {
Quickshell.execDetached(["rm", "-f", stateDir + "/matugen.key"])
const selectedMatugenType = (typeof SettingsData !== "undefined" && SettingsData.matugenScheme) ? SettingsData.matugenScheme : "scheme-tonal-spot"
if (wallpaperPath.startsWith("#")) {
setDesiredTheme("hex", wallpaperPath, isLight, iconTheme, selectedMatugenType)
} else {
setDesiredTheme("image", wallpaperPath, isLight, iconTheme, selectedMatugenType)
}
}
} else {
let primaryColor
let matugenType
if (currentTheme === "custom") {
if (customThemeData && customThemeData.primary) {
primaryColor = customThemeData.primary
matugenType = customThemeData.matugen_type
}
} else {
primaryColor = currentThemeData.primary
matugenType = currentThemeData.matugen_type
}
if (primaryColor) {
Quickshell.execDetached(["rm", "-f", stateDir + "/matugen.key"])
setDesiredTheme("hex", primaryColor, isLight, iconTheme, matugenType)
}
}
}
}
Process {
id: ensureStateDir
}
Process {
id: systemThemeGenerator
running: false
@@ -795,61 +831,19 @@ Singleton {
onExited: exitCode => {
workerRunning = false
if (exitCode !== 0 && exitCode !== 2) {
if (exitCode === 0) {
console.log("Theme: Matugen worker completed successfully")
if (currentTheme === dynamic) {
console.log("Theme: Reloading dynamic colors file")
dynamicColorsFileView.reload()
}
} else if (exitCode === 2) {
console.log("Theme: Matugen worker completed with code 2 (no changes needed)")
} else {
if (typeof ToastService !== "undefined") {
ToastService.showError("Theme worker failed (" + exitCode + ")")
}
console.warn("Theme worker failed with exit code:", exitCode)
}
}
}
Process {
id: gtkApplier
running: false
stdout: StdioCollector {
id: gtkStdout
}
stderr: StdioCollector {
id: gtkStderr
}
onExited: exitCode => {
if (exitCode === 0) {
if (typeof ToastService !== "undefined" && typeof NiriService !== "undefined" && !NiriService.matugenSuppression) {
ToastService.showInfo("GTK colors applied successfully")
}
} else {
if (typeof ToastService !== "undefined") {
ToastService.showError("Failed to apply GTK colors: " + gtkStderr.text)
}
}
}
}
Process {
id: qtApplier
running: false
stdout: StdioCollector {
id: qtStdout
}
stderr: StdioCollector {
id: qtStderr
}
onExited: exitCode => {
if (exitCode === 0) {
if (typeof ToastService !== "undefined") {
ToastService.showInfo("Qt colors applied successfully")
}
} else {
if (typeof ToastService !== "undefined") {
ToastService.showError("Failed to apply Qt colors: " + qtStderr.text)
}
console.warn("Theme: Matugen worker failed with exit code:", exitCode)
}
}
}
@@ -913,6 +907,8 @@ Singleton {
onLoaded: {
if (currentTheme === dynamic) {
console.log("Theme: Dynamic colors file loaded successfully")
colorsFileLoadFailed = false
parseAndLoadColors()
}
}
@@ -924,10 +920,20 @@ Singleton {
}
onLoadFailed: function (error) {
if (currentTheme === dynamic && typeof ToastService !== "undefined") {
ToastService.showError("Failed to read dynamic colors: " + error)
if (currentTheme === dynamic) {
console.log("Theme: Dynamic colors file load failed, marking for regeneration")
colorsFileLoadFailed = true
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode)
if (!isGreeterMode && matugenAvailable && wallpaperPath) {
console.log("Theme: Matugen available, triggering immediate regeneration")
generateSystemThemesFromCurrentTheme()
}
}
}
onPathChanged: {
colorsFileLoadFailed = false
}
}
IpcHandler {

File diff suppressed because it is too large Load Diff

View File

@@ -13,6 +13,8 @@ Item {
required property var controlCenterLoader
required property var dankDashPopoutLoader
required property var notepadSlideoutVariants
required property var hyprKeybindsModalLoader
required property var dankBarLoader
IpcHandler {
function open() {
@@ -75,9 +77,8 @@ Item {
IpcHandler {
function open(): string {
root.controlCenterLoader.active = true
if (root.controlCenterLoader.item) {
root.controlCenterLoader.item.open()
if (root.dankBarLoader.item) {
root.dankBarLoader.item.triggerControlCenterOnFocusedScreen()
return "CONTROL_CENTER_OPEN_SUCCESS"
}
return "CONTROL_CENTER_OPEN_FAILED"
@@ -92,9 +93,8 @@ Item {
}
function toggle(): string {
root.controlCenterLoader.active = true
if (root.controlCenterLoader.item) {
root.controlCenterLoader.item.toggle()
if (root.dankBarLoader.item) {
root.dankBarLoader.item.triggerControlCenterOnFocusedScreen()
return "CONTROL_CENTER_TOGGLE_SUCCESS"
}
return "CONTROL_CENTER_TOGGLE_FAILED"
@@ -262,4 +262,91 @@ Item {
target: "inhibit"
}
IpcHandler {
function list(): string {
return MprisController.availablePlayers.map(p => p.identity).join("\n")
}
function play(): void {
if (MprisController.activePlayer && MprisController.activePlayer.canPlay) {
MprisController.activePlayer.play()
}
}
function pause(): void {
if (MprisController.activePlayer && MprisController.activePlayer.canPause) {
MprisController.activePlayer.pause()
}
}
function playPause(): void {
if (MprisController.activePlayer && MprisController.activePlayer.canTogglePlaying) {
MprisController.activePlayer.togglePlaying()
}
}
function previous(): void {
if (MprisController.activePlayer && MprisController.activePlayer.canGoPrevious) {
MprisController.activePlayer.previous()
}
}
function next(): void {
if (MprisController.activePlayer && MprisController.activePlayer.canGoNext) {
MprisController.activePlayer.next()
}
}
function stop(): void {
if (MprisController.activePlayer) {
MprisController.activePlayer.stop()
}
}
target: "mpris"
}
IpcHandler {
function openBinds(): string {
if (!CompositorService.isHyprland) {
return "HYPR_NOT_AVAILABLE"
}
root.hyprKeybindsModalLoader.active = true
if (root.hyprKeybindsModalLoader.item) {
root.hyprKeybindsModalLoader.item.open()
return "HYPR_KEYBINDS_OPEN_SUCCESS"
}
return "HYPR_KEYBINDS_OPEN_FAILED"
}
function closeBinds(): string {
if (!CompositorService.isHyprland) {
return "HYPR_NOT_AVAILABLE"
}
if (root.hyprKeybindsModalLoader.item) {
root.hyprKeybindsModalLoader.item.close()
return "HYPR_KEYBINDS_CLOSE_SUCCESS"
}
return "HYPR_KEYBINDS_CLOSE_FAILED"
}
function toggleBinds(): string {
if (!CompositorService.isHyprland) {
return "HYPR_NOT_AVAILABLE"
}
root.hyprKeybindsModalLoader.active = true
if (root.hyprKeybindsModalLoader.item) {
if (root.hyprKeybindsModalLoader.item.shouldBeVisible) {
root.hyprKeybindsModalLoader.item.close()
} else {
root.hyprKeybindsModalLoader.item.open()
}
return "HYPR_KEYBINDS_TOGGLE_SUCCESS"
}
return "HYPR_KEYBINDS_TOGGLE_FAILED"
}
target: "hypr"
}
}

View File

@@ -46,6 +46,7 @@ Item {
leftIconName: "search"
showClearButton: true
focus: true
ignoreTabKeys: true
keyForwardTargets: [modal.modalFocusScope]
onTextChanged: {
modal.searchText = text

View File

@@ -60,6 +60,7 @@ DankModal {
open()
clipboardHistoryModal.searchText = ""
clipboardHistoryModal.activeImageLoads = 0
clipboardHistoryModal.shouldHaveFocus = true
refreshClipboard()
keyboardController.reset()

View File

@@ -68,9 +68,11 @@ DankModal {
}
}
onOpened: {
modalFocusScope.forceActiveFocus()
modalFocusScope.focus = true
shouldHaveFocus = true
Qt.callLater(function () {
modalFocusScope.forceActiveFocus()
modalFocusScope.focus = true
shouldHaveFocus = true
})
}
modalFocusScope.Keys.onPressed: function (event) {
switch (event.key) {

View File

@@ -1,7 +1,9 @@
import QtQuick
import Quickshell
import Quickshell.Hyprland
import Quickshell.Wayland
import qs.Common
import qs.Services
PanelWindow {
id: root
@@ -10,18 +12,21 @@ PanelWindow {
property alias content: contentLoader.sourceComponent
property alias contentLoader: contentLoader
property Item directContent: null
property real width: 400
property real height: 300
readonly property real screenWidth: screen ? screen.width : 1920
readonly property real screenHeight: screen ? screen.height : 1080
readonly property real dpr: (screen && screen.devicePixelRatio) || 1
function snap(v) {
return Math.round(v * dpr) / dpr
}
function px(v) {
return Math.round(v)
readonly property real dpr: {
if (CompositorService.isNiri && screen) {
const niriScale = NiriService.displayScales[screen.name]
if (niriScale !== undefined) return niriScale
}
if (CompositorService.isHyprland && screen) {
const hyprlandMonitor = Hyprland.monitors.values.find(m => m.name === screen.name)
if (hyprlandMonitor?.scale !== undefined) return hyprlandMonitor.scale
}
return (screen?.devicePixelRatio) || 1
}
property bool showBackground: true
property real backgroundOpacity: 0.5
@@ -142,26 +147,26 @@ PanelWindow {
Rectangle {
id: contentContainer
width: px(root.width)
height: px(root.height)
width: Theme.px(root.width, dpr)
height: Theme.px(root.height, dpr)
anchors.centerIn: undefined
x: {
if (positioning === "center") {
return snap((root.screenWidth - width) / 2)
return Theme.snap((root.screenWidth - width) / 2, dpr)
} else if (positioning === "top-right") {
return px(Math.max(Theme.spacingL, root.screenWidth - width - Theme.spacingL))
return Theme.px(Math.max(Theme.spacingL, root.screenWidth - width - Theme.spacingL), dpr)
} else if (positioning === "custom") {
return snap(root.customPosition.x)
return Theme.snap(root.customPosition.x, dpr)
}
return 0
}
y: {
if (positioning === "center") {
return snap((root.screenHeight - height) / 2)
return Theme.snap((root.screenHeight - height) / 2, dpr)
} else if (positioning === "top-right") {
return px(Theme.barHeight + Theme.spacingXS)
return Theme.px(Theme.barHeight + Theme.spacingXS, dpr)
} else if (positioning === "custom") {
return snap(root.customPosition.y)
return Theme.snap(root.customPosition.y, dpr)
}
return 0
}
@@ -170,6 +175,7 @@ PanelWindow {
border.color: root.borderColor
border.width: root.borderWidth
clip: false
layer.enabled: true
opacity: root.shouldBeVisible ? 1 : 0
transform: root.animationType === "slide" ? slideTransform : null
@@ -179,8 +185,8 @@ PanelWindow {
readonly property real rawX: root.shouldBeVisible ? 0 : 15
readonly property real rawY: root.shouldBeVisible ? 0 : -30
x: snap(rawX)
y: snap(rawY)
x: Theme.snap(rawX, root.dpr)
y: Theme.snap(rawY, root.dpr)
}
Behavior on opacity {
@@ -195,14 +201,44 @@ PanelWindow {
focus: root.shouldBeVisible
clip: false
Item {
id: directContentWrapper
anchors.fill: parent
visible: root.directContent !== null
focus: true
clip: false
Component.onCompleted: {
if (root.directContent) {
root.directContent.parent = directContentWrapper
root.directContent.anchors.fill = directContentWrapper
Qt.callLater(() => root.directContent.forceActiveFocus())
}
}
Connections {
function onDirectContentChanged() {
if (root.directContent) {
root.directContent.parent = directContentWrapper
root.directContent.anchors.fill = directContentWrapper
Qt.callLater(() => root.directContent.forceActiveFocus())
}
}
target: root
}
}
Loader {
id: contentLoader
anchors.fill: parent
active: root.keepContentLoaded || root.shouldBeVisible || root.visible
active: root.directContent === null && (root.keepContentLoaded || root.shouldBeVisible || root.visible)
asynchronous: false
focus: true
clip: false
visible: root.directContent === null
onLoaded: {
if (item) {

View File

@@ -2,17 +2,16 @@ import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import qs.Common
import qs.Modals.Common
import qs.Services
import qs.Widgets
PanelWindow {
DankModal {
id: root
property string pickerTitle: "Choose Color"
property color selectedColor: Theme.primary
property bool shouldBeVisible: false
property color selectedColor: SessionData.recentColors.length > 0 ? SessionData.recentColors[0] : Theme.primary
property var onColorSelectedCallback: null
signal colorSelected(color selectedColor)
@@ -25,23 +24,24 @@ PanelWindow {
property real gradientX: 0
property real gradientY: 0
function open() {
currentColor = selectedColor
updateFromColor(currentColor)
shouldBeVisible = true
Qt.callLater(() => colorContent.forceActiveFocus())
}
function close() {
shouldBeVisible = false
onColorSelectedCallback = null
}
readonly property var standardColors: [
"#f44336", "#e91e63", "#9c27b0", "#673ab7", "#3f51b5", "#2196f3", "#03a9f4", "#00bcd4",
"#009688", "#4caf50", "#8bc34a", "#cddc39", "#ffeb3b", "#ffc107", "#ff9800", "#ff5722",
"#d32f2f", "#c2185b", "#7b1fa2", "#512da8", "#303f9f", "#1976d2", "#0288d1", "#0097a7",
"#00796b", "#388e3c", "#689f38", "#afb42b", "#fbc02d", "#ffa000", "#f57c00", "#e64a19",
"#c62828", "#ad1457", "#6a1b9a", "#4527a0", "#283593", "#1565c0", "#0277bd", "#00838f",
"#00695c", "#2e7d32", "#558b2f", "#9e9d24", "#f9a825", "#ff8f00", "#ef6c00", "#d84315",
"#ffffff", "#9e9e9e", "#212121"
]
function show() {
currentColor = selectedColor
updateFromColor(currentColor)
open()
}
function hide() {
onColorSelectedCallback = null
close()
}
@@ -74,96 +74,50 @@ PanelWindow {
saturation = Math.max(0, Math.min(1, x))
value = Math.max(0, Math.min(1, 1 - y))
updateColor()
selectedColor = currentColor
}
function pickColorFromScreen() {
close()
hyprpickerProcess.running = true
}
Process {
id: hyprpickerProcess
running: false
command: ["hyprpicker", "--format=hex"]
stdout: SplitParser {
onRead: data => {
const colorStr = data.trim()
if (colorStr.length >= 7 && colorStr.startsWith('#')) {
root.currentColor = colorStr
root.updateFromColor(root.currentColor)
hexInput.text = root.currentColor.toString()
copyColorToClipboard(colorStr)
root.open()
}
hide()
Proc.runCommand("hyprpicker", ["hyprpicker", "--format=hex"], (output, errorCode) => {
if (errorCode !== 0) {
console.warn("hyprpicker exited with code:", errorCode)
root.show()
return
}
}
onExited: (exitCode, exitStatus) => {
if (exitCode !== 0) {
console.warn("hyprpicker exited with code:", exitCode)
const colorStr = output.trim()
if (colorStr.length >= 7 && colorStr.startsWith('#')) {
const pickedColor = Qt.color(colorStr)
root.selectedColor = pickedColor
root.currentColor = pickedColor
root.updateFromColor(pickedColor)
copyColorToClipboard(colorStr)
root.show()
}
root.open()
}
})
}
readonly property var standardColors: [
"#f44336", "#e91e63", "#9c27b0", "#673ab7", "#3f51b5", "#2196f3", "#03a9f4", "#00bcd4",
"#009688", "#4caf50", "#8bc34a", "#cddc39", "#ffeb3b", "#ffc107", "#ff9800", "#ff5722",
"#d32f2f", "#c2185b", "#7b1fa2", "#512da8", "#303f9f", "#1976d2", "#0288d1", "#0097a7",
"#00796b", "#388e3c", "#689f38", "#afb42b", "#fbc02d", "#ffa000", "#f57c00", "#e64a19",
"#c62828", "#ad1457", "#6a1b9a", "#4527a0", "#283593", "#1565c0", "#0277bd", "#00838f",
"#00695c", "#2e7d32", "#558b2f", "#9e9d24", "#f9a825", "#ff8f00", "#ef6c00", "#d84315",
"#ffffff", "#9e9e9e", "#212121"
]
width: 680
height: 680
backgroundColor: Theme.surfaceContainer
cornerRadius: Theme.cornerRadius
borderColor: Theme.outlineMedium
borderWidth: 1
keepContentLoaded: true
visible: shouldBeVisible
WlrLayershell.namespace: "quickshell:color-picker"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
MouseArea {
anchors.fill: parent
onClicked: root.close()
Rectangle {
color: "#80000000"
anchors.fill: parent
}
}
Rectangle {
anchors.centerIn: parent
width: 680
height: 680
radius: Theme.cornerRadius
color: Theme.surfaceContainer
border.color: Theme.outlineMedium
border.width: 1
MouseArea {
anchors.fill: parent
onClicked: {} // Prevent clicks from propagating to background
}
onBackgroundClicked: hide()
content: Component {
FocusScope {
id: colorContent
property alias hexInput: hexInput
anchors.fill: parent
focus: root.shouldBeVisible
focus: true
Keys.onEscapePressed: event => {
root.close()
root.hide()
event.accepted = true
}
@@ -199,7 +153,7 @@ PanelWindow {
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: () => {
pickColorFromScreen()
root.pickColorFromScreen()
}
}
@@ -208,7 +162,7 @@ PanelWindow {
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: () => {
root.close()
root.hide()
}
}
}
@@ -329,12 +283,14 @@ PanelWindow {
const h = Math.max(0, Math.min(1, mouse.y / height))
root.hue = h
root.updateColor()
root.selectedColor = root.currentColor
}
onPositionChanged: mouse => {
if (pressed) {
const h = Math.max(0, Math.min(1, mouse.y / height))
root.hue = h
root.updateColor()
root.selectedColor = root.currentColor
}
}
}
@@ -373,8 +329,10 @@ PanelWindow {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: () => {
root.currentColor = modelData
root.updateFromColor(root.currentColor)
const pickedColor = Qt.color(modelData)
root.selectedColor = pickedColor
root.currentColor = pickedColor
root.updateFromColor(pickedColor)
}
}
}
@@ -429,8 +387,10 @@ PanelWindow {
enabled: index < SessionData.recentColors.length
onClicked: () => {
if (index < SessionData.recentColors.length) {
root.currentColor = SessionData.recentColors[index]
root.updateFromColor(root.currentColor)
const pickedColor = SessionData.recentColors[index]
root.selectedColor = pickedColor
root.currentColor = pickedColor
root.updateFromColor(pickedColor)
}
}
}
@@ -459,6 +419,7 @@ PanelWindow {
onSliderValueChanged: (newValue) => {
root.alpha = newValue / 100
root.updateColor()
root.selectedColor = root.currentColor
}
}
}
@@ -509,6 +470,7 @@ PanelWindow {
if (!hexPattern.test(text)) return
const color = Qt.color(text)
if (color) {
root.selectedColor = color
root.currentColor = color
root.updateFromColor(color)
}
@@ -530,9 +492,9 @@ PanelWindow {
root.currentColor = color
root.updateFromColor(color)
root.selectedColor = root.currentColor
colorSelected(root.currentColor)
root.colorSelected(root.currentColor)
SessionData.addRecentColor(root.currentColor)
root.close()
root.hide()
}
}
}
@@ -549,8 +511,8 @@ PanelWindow {
backgroundColor: "transparent"
textColor: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
onClicked: root.close()
onClicked: root.hide()
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
@@ -570,7 +532,7 @@ PanelWindow {
anchors.verticalCenter: parent.verticalCenter
onClicked: {
const colorString = root.currentColor.toString()
copyColorToClipboard(colorString)
root.copyColorToClipboard(colorString)
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,266 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Modals.Common
import qs.Services
import qs.Widgets
DankModal {
id: root
width: 1400
height: 900
onBackgroundClicked: close()
function categorizeKeybinds() {
const categories = {
"Workspace": [],
"Window": [],
"Monitor": [],
"Execute": [],
"System": [],
"Other": []
}
function addKeybind(keybind) {
const dispatcher = keybind.dispatcher || ""
if (dispatcher.includes("workspace")) {
categories["Workspace"].push(keybind)
} else if (dispatcher.includes("monitor")) {
categories["Monitor"].push(keybind)
} else if (dispatcher.includes("window") || dispatcher.includes("focus") || dispatcher.includes("move") || dispatcher.includes("swap") || dispatcher.includes("resize") || dispatcher === "killactive" || dispatcher === "fullscreen" || dispatcher === "togglefloating") {
categories["Window"].push(keybind)
} else if (dispatcher === "exec") {
categories["Execute"].push(keybind)
} else if (dispatcher === "exit" || dispatcher.includes("dpms")) {
categories["System"].push(keybind)
} else {
categories["Other"].push(keybind)
}
}
const allKeybinds = HyprKeybindsService.keybinds.keybinds || []
for (let i = 0; i < allKeybinds.length; i++) {
addKeybind(allKeybinds[i])
}
const children = HyprKeybindsService.keybinds.children || []
for (let i = 0; i < children.length; i++) {
const child = children[i]
const childKeybinds = child.keybinds || []
for (let j = 0; j < childKeybinds.length; j++) {
addKeybind(childKeybinds[j])
}
}
categories["Workspace"].sort((a, b) => {
const dispA = a.dispatcher || ""
const dispB = b.dispatcher || ""
return dispA.localeCompare(dispB)
})
categories["Window"].sort((a, b) => {
const dispA = a.dispatcher || ""
const dispB = b.dispatcher || ""
return dispA.localeCompare(dispB)
})
categories["Monitor"].sort((a, b) => {
const dispA = a.dispatcher || ""
const dispB = b.dispatcher || ""
return dispA.localeCompare(dispB)
})
categories["Execute"].sort((a, b) => {
const modsA = a.mods || []
const keyA = a.key || ""
const bindA = [...modsA, keyA].join("+")
const modsB = b.mods || []
const keyB = b.key || ""
const bindB = [...modsB, keyB].join("+")
return bindA.localeCompare(bindB)
})
return categories
}
content: Component {
Item {
anchors.fill: parent
DankFlickable {
id: mainFlickable
anchors.fill: parent
anchors.margins: Theme.spacingL
contentWidth: rowLayout.implicitWidth
contentHeight: rowLayout.implicitHeight
clip: true
Row {
id: rowLayout
spacing: Theme.spacingM
property var categories: root.categorizeKeybinds()
property real columnWidth: (mainFlickable.width - spacing * 2) / 3
Column {
width: rowLayout.columnWidth
spacing: Theme.spacingXS
StyledText {
text: "Window / Monitor"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
color: Theme.primary
}
Rectangle {
width: parent.width
height: 1
color: Theme.primary
opacity: 0.3
}
Item { width: 1; height: Theme.spacingXS }
Column {
width: parent.width
spacing: Theme.spacingXS
Repeater {
model: [...(rowLayout.categories["Window"] || []), ...(rowLayout.categories["Monitor"] || [])]
Row {
width: parent.width
spacing: Theme.spacingS
StyledRect {
width: Math.min(140, parent.width * 0.42)
height: 22
radius: 4
opacity: 0.3
StyledText {
anchors.centerIn: parent
anchors.margins: 2
width: parent.width - 4
text: {
const mods = modelData.mods || []
const key = modelData.key || ""
const parts = [...mods, key]
return parts.join("+")
}
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
isMonospace: true
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
}
}
StyledText {
width: parent.width - 150
text: {
const comment = modelData.comment || ""
if (comment) return comment
const dispatcher = modelData.dispatcher || ""
const params = modelData.params || ""
return params ? `${dispatcher} ${params}` : dispatcher
}
font.pixelSize: Theme.fontSizeSmall
opacity: 0.9
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
Repeater {
model: ["Workspace", "Execute"]
Column {
width: rowLayout.columnWidth
spacing: Theme.spacingXS
StyledText {
text: modelData
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
color: Theme.primary
}
Rectangle {
width: parent.width
height: 1
color: Theme.primary
opacity: 0.3
}
Item { width: 1; height: Theme.spacingXS }
Column {
width: parent.width
spacing: Theme.spacingXS
Repeater {
model: rowLayout.categories[modelData] || []
Row {
width: parent.width
spacing: Theme.spacingS
StyledRect {
width: Math.min(140, parent.width * 0.42)
height: 22
radius: 4
opacity: 0.3
StyledText {
anchors.centerIn: parent
anchors.margins: 2
width: parent.width - 4
text: {
const mods = modelData.mods || []
const key = modelData.key || ""
const parts = [...mods, key]
return parts.join("+")
}
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
isMonospace: true
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
}
}
StyledText {
width: parent.width - 150
text: {
const comment = modelData.comment || ""
if (comment) return comment
const dispatcher = modelData.dispatcher || ""
const params = modelData.params || ""
return params ? `${dispatcher} ${params}` : dispatcher
}
font.pixelSize: Theme.fontSizeSmall
opacity: 0.9
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,162 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Modals.Common
import qs.Services
import qs.Widgets
DankModal {
id: root
property bool networkWiredInfoModalVisible: false
property string networkID: ""
property var networkData: null
function showNetworkInfo(id, data) {
networkID = id
networkData = data
networkWiredInfoModalVisible = true
open()
NetworkService.fetchWiredNetworkInfo(data.uuid)
}
function hideDialog() {
networkWiredInfoModalVisible = false
close()
networkID = ""
networkData = null
}
visible: networkWiredInfoModalVisible
width: 600
height: 500
enableShadow: true
onBackgroundClicked: hideDialog()
onVisibleChanged: {
if (!visible) {
networkID = ""
networkData = null
}
}
content: Component {
Item {
anchors.fill: parent
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
Row {
width: parent.width
Column {
width: parent.width - 40
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Network Information")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: `Details for "${networkID}"`
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium
width: parent.width
elide: Text.ElideRight
}
}
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: root.hideDialog()
}
}
Rectangle {
id: detailsRect
width: parent.width
height: parent.height - 140
radius: Theme.cornerRadius
color: Theme.surfaceHover
border.color: Theme.outlineStrong
border.width: 1
clip: true
DankFlickable {
anchors.fill: parent
anchors.margins: Theme.spacingM
contentHeight: detailsText.contentHeight
StyledText {
id: detailsText
width: parent.width
text: NetworkService.networkWiredInfoDetails && NetworkService.networkWiredInfoDetails.replace(/\\n/g, '\n') || "No information available"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
wrapMode: Text.WordWrap
}
}
}
Item {
width: parent.width
height: 40
Rectangle {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
width: Math.max(70, closeText.contentWidth + Theme.spacingM * 2)
height: 36
radius: Theme.cornerRadius
color: closeArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
StyledText {
id: closeText
anchors.centerIn: parent
text: I18n.tr("Close")
font.pixelSize: Theme.fontSizeMedium
color: Theme.background
font.weight: Font.Medium
}
MouseArea {
id: closeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.hideDialog()
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
}
}

View File

@@ -54,6 +54,9 @@ DankModal {
height: 700
visible: false
onBackgroundClicked: hide()
onOpened: () => {
Qt.callLater(() => modalFocusScope.forceActiveFocus());
}
onShouldBeVisibleChanged: (shouldBeVisible) => {
if (!shouldBeVisible) {
notificationModalOpen = false

View File

@@ -78,7 +78,7 @@ DankModal {
}
onOpened: () => {
selectedIndex = 0;
modalFocusScope.forceActiveFocus();
Qt.callLater(() => modalFocusScope.forceActiveFocus());
}
modalFocusScope.Keys.onPressed: (event) => {
switch (event.key) {

View File

@@ -25,6 +25,90 @@ Item {
visible: !BatteryService.batteryAvailable
}
StyledRect {
width: parent.width
height: lockScreenSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0
Column {
id: lockScreenSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "lock"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Lock Screen")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
width: parent.width
text: I18n.tr("Show Power Actions")
description: I18n.tr("Show power, restart, and logout buttons on the lock screen")
checked: SettingsData.lockScreenShowPowerActions
onToggled: checked => SettingsData.setLockScreenShowPowerActions(checked)
}
StyledText {
text: I18n.tr("loginctl not available - lock integration requires DMS socket connection")
font.pixelSize: Theme.fontSizeSmall
color: Theme.warning
visible: !SessionService.loginctlAvailable
width: parent.width
wrapMode: Text.Wrap
}
DankToggle {
width: parent.width
text: I18n.tr("Enable loginctl lock integration")
description: I18n.tr("Bind lock screen to dbus signals from loginctl. Disable if using an external lock screen")
checked: SessionService.loginctlAvailable && SettingsData.loginctlLockIntegration
enabled: SessionService.loginctlAvailable
onToggled: checked => {
if (SessionService.loginctlAvailable) {
SettingsData.setLoginctlLockIntegration(checked)
}
}
}
DankToggle {
width: parent.width
text: I18n.tr("Lock before suspend")
description: I18n.tr("Automatically lock the screen when the system prepares to suspend")
checked: SettingsData.lockBeforeSuspend
visible: SessionService.loginctlAvailable && SettingsData.loginctlLockIntegration
onToggled: checked => SettingsData.setLockBeforeSuspend(checked)
}
DankToggle {
width: parent.width
text: I18n.tr("Enable fingerprint authentication")
description: I18n.tr("Use fingerprint reader for lock screen authentication (requires enrolled fingerprints)")
checked: SettingsData.enableFprint
visible: SettingsData.fprintdAvailable
onToggled: checked => SettingsData.setEnableFprint(checked)
}
}
}
StyledRect {
width: parent.width
height: timeoutSection.implicitHeight + Theme.spacingL * 2
@@ -85,14 +169,14 @@ Item {
Connections {
target: powerCategory
function onCurrentIndexChanged() {
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acLockTimeout : SessionData.batteryLockTimeout
const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acLockTimeout : SettingsData.batteryLockTimeout
const index = lockDropdown.timeoutValues.indexOf(currentTimeout)
lockDropdown.currentValue = index >= 0 ? lockDropdown.timeoutOptions[index] : "Never"
}
}
Component.onCompleted: {
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acLockTimeout : SessionData.batteryLockTimeout
const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acLockTimeout : SettingsData.batteryLockTimeout
const index = timeoutValues.indexOf(currentTimeout)
currentValue = index >= 0 ? timeoutOptions[index] : "Never"
}
@@ -102,9 +186,9 @@ Item {
if (index >= 0) {
const timeout = timeoutValues[index]
if (powerCategory.currentIndex === 0) {
SessionData.setAcLockTimeout(timeout)
SettingsData.setAcLockTimeout(timeout)
} else {
SessionData.setBatteryLockTimeout(timeout)
SettingsData.setBatteryLockTimeout(timeout)
}
}
}
@@ -121,14 +205,14 @@ Item {
Connections {
target: powerCategory
function onCurrentIndexChanged() {
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acMonitorTimeout : SessionData.batteryMonitorTimeout
const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acMonitorTimeout : SettingsData.batteryMonitorTimeout
const index = monitorDropdown.timeoutValues.indexOf(currentTimeout)
monitorDropdown.currentValue = index >= 0 ? monitorDropdown.timeoutOptions[index] : "Never"
}
}
Component.onCompleted: {
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acMonitorTimeout : SessionData.batteryMonitorTimeout
const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acMonitorTimeout : SettingsData.batteryMonitorTimeout
const index = timeoutValues.indexOf(currentTimeout)
currentValue = index >= 0 ? timeoutOptions[index] : "Never"
}
@@ -138,9 +222,9 @@ Item {
if (index >= 0) {
const timeout = timeoutValues[index]
if (powerCategory.currentIndex === 0) {
SessionData.setAcMonitorTimeout(timeout)
SettingsData.setAcMonitorTimeout(timeout)
} else {
SessionData.setBatteryMonitorTimeout(timeout)
SettingsData.setBatteryMonitorTimeout(timeout)
}
}
}
@@ -157,14 +241,14 @@ Item {
Connections {
target: powerCategory
function onCurrentIndexChanged() {
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acSuspendTimeout : SessionData.batterySuspendTimeout
const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acSuspendTimeout : SettingsData.batterySuspendTimeout
const index = suspendDropdown.timeoutValues.indexOf(currentTimeout)
suspendDropdown.currentValue = index >= 0 ? suspendDropdown.timeoutOptions[index] : "Never"
}
}
Component.onCompleted: {
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acSuspendTimeout : SessionData.batterySuspendTimeout
const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acSuspendTimeout : SettingsData.batterySuspendTimeout
const index = timeoutValues.indexOf(currentTimeout)
currentValue = index >= 0 ? timeoutOptions[index] : "Never"
}
@@ -174,9 +258,9 @@ Item {
if (index >= 0) {
const timeout = timeoutValues[index]
if (powerCategory.currentIndex === 0) {
SessionData.setAcSuspendTimeout(timeout)
SettingsData.setAcSuspendTimeout(timeout)
} else {
SessionData.setBatterySuspendTimeout(timeout)
SettingsData.setBatterySuspendTimeout(timeout)
}
}
}
@@ -194,14 +278,14 @@ Item {
Connections {
target: powerCategory
function onCurrentIndexChanged() {
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acHibernateTimeout : SessionData.batteryHibernateTimeout
const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acHibernateTimeout : SettingsData.batteryHibernateTimeout
const index = hibernateDropdown.timeoutValues.indexOf(currentTimeout)
hibernateDropdown.currentValue = index >= 0 ? hibernateDropdown.timeoutOptions[index] : "Never"
}
}
Component.onCompleted: {
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acHibernateTimeout : SessionData.batteryHibernateTimeout
const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acHibernateTimeout : SettingsData.batteryHibernateTimeout
const index = timeoutValues.indexOf(currentTimeout)
currentValue = index >= 0 ? timeoutOptions[index] : "Never"
}
@@ -211,31 +295,14 @@ Item {
if (index >= 0) {
const timeout = timeoutValues[index]
if (powerCategory.currentIndex === 0) {
SessionData.setAcHibernateTimeout(timeout)
SettingsData.setAcHibernateTimeout(timeout)
} else {
SessionData.setBatteryHibernateTimeout(timeout)
SettingsData.setBatteryHibernateTimeout(timeout)
}
}
}
}
DankToggle {
width: parent.width
text: I18n.tr("Enable loginctl lock integration")
description: "Bind lock screen to dbus signals from loginctl. Disable if using an external lock screen."
checked: SessionData.loginctlLockIntegration
onToggled: checked => SessionData.setLoginctlLockIntegration(checked)
}
DankToggle {
width: parent.width
text: I18n.tr("Lock before suspend")
description: "Automatically lock the screen when the system prepares to suspend"
checked: SessionData.lockBeforeSuspend
visible: SessionData.loginctlLockIntegration
onToggled: checked => SessionData.setLockBeforeSuspend(checked)
}
StyledText {
text: I18n.tr("Idle monitoring not supported - requires newer Quickshell version")
font.pixelSize: Theme.fontSizeSmall
@@ -246,6 +313,277 @@ Item {
}
}
StyledRect {
width: parent.width
height: powerCommandConfirmSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0
Column {
id: powerCommandConfirmSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "check_circle"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Power Action Confirmation")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
width: parent.width
text: I18n.tr("Show Confirmation on Power Actions")
description: I18n.tr("Request confirmation on power off, restart, suspend, hibernate and logout actions")
checked: SettingsData.powerActionConfirm
onToggled: checked => SettingsData.setPowerActionConfirm(checked)
}
}
}
StyledRect {
width: parent.width
height: powerCommandCustomization.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0
Column {
id: powerCommandCustomization
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "developer_mode"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Custom Power Actions")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
anchors.left: parent.left
StyledText {
text: I18n.tr("Command or script to run instead of the standard lock procedure")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: customLockCommand
width: parent.width
height: 48
placeholderText: "/usr/bin/myLock.sh"
backgroundColor: Theme.surfaceVariant
normalBorderColor: Theme.primarySelected
focusedBorderColor: Theme.primary
Component.onCompleted: {
if (SettingsData.customPowerActionLock) {
text = SettingsData.customPowerActionLock;
}
}
onTextEdited: {
SettingsData.setCustomPowerActionLock(text.trim());
}
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
anchors.left: parent.left
StyledText {
text: I18n.tr("Command or script to run instead of the standard logout procedure")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: customLogoutCommand
width: parent.width
height: 48
placeholderText: "/usr/bin/myLogout.sh"
backgroundColor: Theme.surfaceVariant
normalBorderColor: Theme.primarySelected
focusedBorderColor: Theme.primary
Component.onCompleted: {
if (SettingsData.customPowerActionLogout) {
text = SettingsData.customPowerActionLogout;
}
}
onTextEdited: {
SettingsData.setCustomPowerActionLogout(text.trim());
}
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
anchors.left: parent.left
StyledText {
text: I18n.tr("Command or script to run instead of the standard suspend procedure")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: customSuspendCommand
width: parent.width
height: 48
placeholderText: "/usr/bin/mySuspend.sh"
backgroundColor: Theme.surfaceVariant
normalBorderColor: Theme.primarySelected
focusedBorderColor: Theme.primary
Component.onCompleted: {
if (SettingsData.customPowerActionSuspend) {
text = SettingsData.customPowerActionSuspend;
}
}
onTextEdited: {
SettingsData.setCustomPowerActionSuspend(text.trim());
}
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
anchors.left: parent.left
StyledText {
text: I18n.tr("Command or script to run instead of the standard hibernate procedure")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: customHibernateCommand
width: parent.width
height: 48
placeholderText: "/usr/bin/myHibernate.sh"
backgroundColor: Theme.surfaceVariant
normalBorderColor: Theme.primarySelected
focusedBorderColor: Theme.primary
Component.onCompleted: {
if (SettingsData.customPowerActionHibernate) {
text = SettingsData.customPowerActionHibernate;
}
}
onTextEdited: {
SettingsData.setCustomPowerActionHibernate(text.trim());
}
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
anchors.left: parent.left
StyledText {
text: I18n.tr("Command or script to run instead of the standard reboot procedure")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: customRebootCommand
width: parent.width
height: 48
placeholderText: "/usr/bin/myReboot.sh"
backgroundColor: Theme.surfaceVariant
normalBorderColor: Theme.primarySelected
focusedBorderColor: Theme.primary
Component.onCompleted: {
if (SettingsData.customPowerActionReboot) {
text = SettingsData.customPowerActionReboot;
}
}
onTextEdited: {
SettingsData.setCustomPowerActionReboot(text.trim());
}
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
anchors.left: parent.left
StyledText {
text: I18n.tr("Command or script to run instead of the standard power off procedure")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: customPowerOffCommand
width: parent.width
height: 48
placeholderText: "/usr/bin/myPowerOff.sh"
backgroundColor: Theme.surfaceVariant
normalBorderColor: Theme.primarySelected
focusedBorderColor: Theme.primary
Component.onCompleted: {
if (SettingsData.customPowerActionPowerOff) {
text = SettingsData.customPowerActionPowerOff;
}
}
onTextEdited: {
SettingsData.setCustomPowerActionPowerOff(text.trim());
}
}
}
}
}
}
}
}
}

View File

@@ -35,27 +35,14 @@ FocusScope {
}
Loader {
id: timeLoader
id: timeWeatherLoader
anchors.fill: parent
active: root.currentIndex === 1
visible: active
asynchronous: true
sourceComponent: TimeTab {
}
}
Loader {
id: weatherLoader
anchors.fill: parent
active: root.currentIndex === 2
visible: active
asynchronous: true
sourceComponent: WeatherTab {
sourceComponent: TimeWeatherTab {
}
}
@@ -64,7 +51,7 @@ FocusScope {
id: topBarLoader
anchors.fill: parent
active: root.currentIndex === 3
active: root.currentIndex === 2
visible: active
asynchronous: true
@@ -78,7 +65,7 @@ FocusScope {
id: widgetsLoader
anchors.fill: parent
active: root.currentIndex === 4
active: root.currentIndex === 3
visible: active
asynchronous: true
@@ -91,7 +78,7 @@ FocusScope {
id: dockLoader
anchors.fill: parent
active: root.currentIndex === 5
active: root.currentIndex === 4
visible: active
asynchronous: true
@@ -107,7 +94,7 @@ FocusScope {
id: displaysLoader
anchors.fill: parent
active: root.currentIndex === 6
active: root.currentIndex === 5
visible: active
asynchronous: true
@@ -120,7 +107,7 @@ FocusScope {
id: launcherLoader
anchors.fill: parent
active: root.currentIndex === 7
active: root.currentIndex === 6
visible: active
asynchronous: true
@@ -133,7 +120,7 @@ FocusScope {
id: themeColorsLoader
anchors.fill: parent
active: root.currentIndex === 8
active: root.currentIndex === 7
visible: active
asynchronous: true
@@ -146,7 +133,7 @@ FocusScope {
id: powerLoader
anchors.fill: parent
active: root.currentIndex === 9
active: root.currentIndex === 8
visible: active
asynchronous: true
@@ -159,7 +146,7 @@ FocusScope {
id: pluginsLoader
anchors.fill: parent
active: root.currentIndex === 10
active: root.currentIndex === 9
visible: active
asynchronous: true
@@ -173,7 +160,7 @@ FocusScope {
id: aboutLoader
anchors.fill: parent
active: root.currentIndex === 11
active: root.currentIndex === 10
visible: active
asynchronous: true

View File

@@ -12,11 +12,8 @@ Rectangle {
"text": I18n.tr("Personalization"),
"icon": "person"
}, {
"text": I18n.tr("Time & Date"),
"text": I18n.tr("Time & Weather"),
"icon": "schedule"
}, {
"text": I18n.tr("Weather"),
"icon": "cloud"
}, {
"text": I18n.tr("Dank Bar"),
"icon": "toolbar"
@@ -36,8 +33,8 @@ Rectangle {
"text": I18n.tr("Theme & Colors"),
"icon": "palette"
}, {
"text": I18n.tr("Power"),
"icon": "power_settings_new"
"text": I18n.tr("Power & Security"),
"icon": "power"
}, {
"text": I18n.tr("Plugins"),
"icon": "extension"

View File

@@ -128,6 +128,7 @@ Item {
enabled: parentModal ? parentModal.spotlightOpen : true
placeholderText: ""
ignoreLeftRightKeys: appLauncher.viewMode !== "list"
ignoreTabKeys: true
keyForwardTargets: [spotlightKeyHandler]
text: appLauncher.searchQuery
onTextEdited: () => {

View File

@@ -12,6 +12,7 @@ Popup {
property var currentApp: null
property var appLauncher: null
property var parentHandler: null
readonly property var desktopEntry: (currentApp && !currentApp.isPlugin && appLauncher && appLauncher._uniqueApps && currentApp.appIndex >= 0 && currentApp.appIndex < appLauncher._uniqueApps.length) ? appLauncher._uniqueApps[currentApp.appIndex] : null
function show(x, y, app) {
currentApp = app
@@ -83,6 +84,7 @@ Popup {
color: pinMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
id: pinRow
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
@@ -90,10 +92,10 @@ Popup {
DankIcon {
name: {
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry)
if (!desktopEntry)
return "push_pin"
const appId = contextMenu.currentApp.desktopEntry.id || contextMenu.currentApp.desktopEntry.execString || ""
const appId = desktopEntry.id || desktopEntry.execString || ""
return SessionData.isPinnedApp(appId) ? "keep_off" : "push_pin"
}
size: Theme.iconSize - 2
@@ -104,10 +106,10 @@ Popup {
StyledText {
text: {
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry)
if (!desktopEntry)
return I18n.tr("Pin to Dock")
const appId = contextMenu.currentApp.desktopEntry.id || contextMenu.currentApp.desktopEntry.execString || ""
const appId = desktopEntry.id || desktopEntry.execString || ""
return SessionData.isPinnedApp(appId) ? I18n.tr("Unpin from Dock") : I18n.tr("Pin to Dock")
}
font.pixelSize: Theme.fontSizeSmall
@@ -124,10 +126,10 @@ Popup {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: () => {
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry)
if (!desktopEntry)
return
const appId = contextMenu.currentApp.desktopEntry.id || contextMenu.currentApp.desktopEntry.execString || ""
const appId = desktopEntry.id || desktopEntry.execString || ""
if (SessionData.isPinnedApp(appId))
SessionData.removePinnedApp(appId)
else
@@ -152,7 +154,7 @@ Popup {
}
Repeater {
model: contextMenu.currentApp && contextMenu.currentApp.desktopEntry && contextMenu.currentApp.desktopEntry.actions ? contextMenu.currentApp.desktopEntry.actions : []
model: desktopEntry && desktopEntry.actions ? desktopEntry.actions : []
Rectangle {
width: parent.width
@@ -161,10 +163,9 @@ Popup {
color: actionMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
id: actionRow
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
@@ -189,8 +190,6 @@ Popup {
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
width: parent.width - (modelData.icon && modelData.icon !== "" ? (Theme.iconSize - 2 + Theme.spacingS) : 0)
}
}
@@ -200,9 +199,9 @@ Popup {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData && contextMenu.currentApp && contextMenu.currentApp.desktopEntry) {
SessionService.launchDesktopAction(contextMenu.currentApp.desktopEntry, modelData)
if (appLauncher) {
if (modelData && desktopEntry) {
SessionService.launchDesktopAction(desktopEntry, modelData)
if (appLauncher && contextMenu.currentApp) {
appLauncher.appLaunched(contextMenu.currentApp)
}
}
@@ -213,7 +212,7 @@ Popup {
}
Rectangle {
visible: contextMenu.currentApp && contextMenu.currentApp.desktopEntry && contextMenu.currentApp.desktopEntry.actions && contextMenu.currentApp.desktopEntry.actions.length > 0
visible: desktopEntry && desktopEntry.actions && desktopEntry.actions.length > 0
width: parent.width - Theme.spacingS * 2
height: 5
anchors.horizontalCenter: parent.horizontalCenter
@@ -234,6 +233,7 @@ Popup {
color: launchMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
id: launchRow
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
@@ -294,6 +294,7 @@ Popup {
color: primeRunMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
id: primeRunRow
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
@@ -323,9 +324,9 @@ Popup {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: () => {
if (contextMenu.currentApp && contextMenu.currentApp.desktopEntry) {
SessionService.launchDesktopEntry(contextMenu.currentApp.desktopEntry, true)
if (appLauncher) {
if (desktopEntry) {
SessionService.launchDesktopEntry(desktopEntry, true)
if (appLauncher && contextMenu.currentApp) {
appLauncher.appLaunched(contextMenu.currentApp)
}
}

View File

@@ -13,15 +13,35 @@ DankModal {
id: spotlightModal
property bool spotlightOpen: false
property Component spotlightContent
property alias spotlightContent: spotlightContentInstance
function show() {
spotlightOpen = true
open()
Qt.callLater(() => {
if (contentLoader.item && contentLoader.item.searchField) {
contentLoader.item.searchField.forceActiveFocus()
if (spotlightContent && spotlightContent.searchField) {
spotlightContent.searchField.forceActiveFocus()
}
})
}
function showWithQuery(query) {
if (spotlightContent) {
if (spotlightContent.appLauncher) {
spotlightContent.appLauncher.searchQuery = query
}
if (spotlightContent.searchField) {
spotlightContent.searchField.text = query
}
}
spotlightOpen = true
open()
Qt.callLater(() => {
if (spotlightContent && spotlightContent.searchField) {
spotlightContent.searchField.forceActiveFocus()
}
})
}
@@ -32,17 +52,17 @@ DankModal {
}
onDialogClosed: {
if (contentLoader.item) {
if (contentLoader.item.appLauncher) {
contentLoader.item.appLauncher.searchQuery = ""
contentLoader.item.appLauncher.selectedIndex = 0
contentLoader.item.appLauncher.setCategory(I18n.tr("All"))
if (spotlightContent) {
if (spotlightContent.appLauncher) {
spotlightContent.appLauncher.searchQuery = ""
spotlightContent.appLauncher.selectedIndex = 0
spotlightContent.appLauncher.setCategory(I18n.tr("All"))
}
if (contentLoader.item.resetScroll) {
contentLoader.item.resetScroll()
if (spotlightContent.resetScroll) {
spotlightContent.resetScroll()
}
if (contentLoader.item.searchField) {
contentLoader.item.searchField.text = ""
if (spotlightContent.searchField) {
spotlightContent.searchField.text = ""
}
}
}
@@ -68,10 +88,10 @@ DankModal {
if (visible && !spotlightOpen) {
show()
}
if (visible && contentLoader.item) {
if (visible && spotlightContent) {
Qt.callLater(() => {
if (contentLoader.item.searchField) {
contentLoader.item.searchField.forceActiveFocus()
if (spotlightContent.searchField) {
spotlightContent.searchField.forceActiveFocus()
}
})
}
@@ -79,7 +99,6 @@ DankModal {
onBackgroundClicked: () => {
return hide()
}
content: spotlightContent
Connections {
function onCloseAllModalsExcept(excludedModal) {
@@ -107,12 +126,28 @@ DankModal {
return "SPOTLIGHT_TOGGLE_SUCCESS"
}
function openQuery(query: string): string {
spotlightModal.showWithQuery(query)
return "SPOTLIGHT_OPEN_QUERY_SUCCESS"
}
function toggleQuery(query: string): string {
if (spotlightModal.spotlightOpen) {
spotlightModal.hide()
} else {
spotlightModal.showWithQuery(query)
}
return "SPOTLIGHT_TOGGLE_QUERY_SUCCESS"
}
target: "spotlight"
}
spotlightContent: Component {
SpotlightContent {
parentModal: spotlightModal
}
SpotlightContent {
id: spotlightContentInstance
parentModal: spotlightModal
}
directContent: spotlightContentInstance
}

View File

@@ -130,6 +130,8 @@ Rectangle {
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
wrapMode: Text.NoWrap
maximumLineCount: 1
}
StyledText {
@@ -162,7 +164,7 @@ Rectangle {
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
resultsList.itemClicked(index, model)
} else if (mouse.button === Qt.RightButton) {
} else if (mouse.button === Qt.RightButton && !model.isPlugin) {
const globalPos = mapToItem(null, mouse.x, mouse.y)
const modalPos = resultsContainer.parent.mapFromItem(null, globalPos.x, globalPos.y)
resultsList.itemRightClicked(index, model, modalPos.x, modalPos.y)
@@ -291,8 +293,8 @@ Rectangle {
font.weight: Font.Medium
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
maximumLineCount: 2
wrapMode: Text.WordWrap
maximumLineCount: 1
wrapMode: Text.NoWrap
}
}
@@ -314,7 +316,7 @@ Rectangle {
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
resultsGrid.itemClicked(index, model)
} else if (mouse.button === Qt.RightButton) {
} else if (mouse.button === Qt.RightButton && !model.isPlugin) {
const globalPos = mapToItem(null, mouse.x, mouse.y)
const modalPos = resultsContainer.parent.mapFromItem(null, globalPos.x, globalPos.y)
resultsGrid.itemRightClicked(index, model, modalPos.x, modalPos.y)

View File

@@ -12,10 +12,15 @@ DankModal {
property string wifiUsernameInput: ""
property bool requiresEnterprise: false
property string wifiAnonymousIdentityInput: ""
property string wifiDomainInput: ""
function show(ssid) {
wifiPasswordSSID = ssid
wifiPasswordInput = ""
wifiUsernameInput = ""
wifiAnonymousIdentityInput = ""
wifiDomainInput = ""
const network = NetworkService.wifiNetworks.find(n => n.ssid === ssid)
requiresEnterprise = network?.enterprise || false
@@ -34,11 +39,13 @@ DankModal {
shouldBeVisible: false
width: 420
height: requiresEnterprise ? 310 : 230
height: requiresEnterprise ? 430 : 230
onShouldBeVisibleChanged: () => {
if (!shouldBeVisible) {
wifiPasswordInput = ""
wifiUsernameInput = ""
wifiAnonymousIdentityInput = ""
wifiDomainInput = ""
}
}
onOpened: {
@@ -56,6 +63,8 @@ DankModal {
close()
wifiPasswordInput = ""
wifiUsernameInput = ""
wifiAnonymousIdentityInput = ""
wifiDomainInput = ""
}
Connections {
@@ -84,6 +93,8 @@ DankModal {
close()
wifiPasswordInput = ""
wifiUsernameInput = ""
wifiAnonymousIdentityInput = ""
wifiDomainInput = ""
event.accepted = true
}
@@ -107,7 +118,7 @@ DankModal {
}
StyledText {
text: requiresEnterprise ? `Enter credentials for "${wifiPasswordSSID}"` : `Enter password for "${wifiPasswordSSID}"`
text: requiresEnterprise ? I18n.tr("Enter credentials for ") + wifiPasswordSSID : I18n.tr("Enter password for ") + wifiPasswordSSID
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium
width: parent.width
@@ -123,6 +134,8 @@ DankModal {
close()
wifiPasswordInput = ""
wifiUsernameInput = ""
wifiAnonymousIdentityInput = ""
wifiDomainInput = ""
}
}
}
@@ -150,7 +163,7 @@ DankModal {
font.pixelSize: Theme.fontSizeMedium
textColor: Theme.surfaceText
text: wifiUsernameInput
placeholderText: "Username"
placeholderText: I18n.tr("Username")
backgroundColor: "transparent"
enabled: root.shouldBeVisible
onTextEdited: () => {
@@ -187,7 +200,7 @@ DankModal {
textColor: Theme.surfaceText
text: wifiPasswordInput
echoMode: showPasswordCheckbox.checked ? TextInput.Normal : TextInput.Password
placeholderText: requiresEnterprise ? "Password" : ""
placeholderText: requiresEnterprise ? I18n.tr("Password") : ""
backgroundColor: "transparent"
focus: !requiresEnterprise
enabled: root.shouldBeVisible
@@ -196,10 +209,18 @@ DankModal {
}
onAccepted: () => {
const username = requiresEnterprise ? usernameInput.text : ""
NetworkService.connectToWifi(wifiPasswordSSID, passwordInput.text, username)
NetworkService.connectToWifi(
wifiPasswordSSID,
passwordInput.text,
username,
wifiAnonymousIdentityInput,
wifiDomainInput
)
close()
wifiPasswordInput = ""
wifiUsernameInput = ""
wifiAnonymousIdentityInput = ""
wifiDomainInput = ""
passwordInput.text = ""
if (requiresEnterprise) usernameInput.text = ""
}
@@ -235,6 +256,70 @@ DankModal {
}
}
Rectangle {
visible: requiresEnterprise
width: parent.width
height: 50
radius: Theme.cornerRadius
color: Theme.surfaceHover
border.color: anonInput.activeFocus ? Theme.primary : Theme.outlineStrong
border.width: anonInput.activeFocus ? 2 : 1
MouseArea {
anchors.fill: parent
onClicked: () => {
anonInput.forceActiveFocus()
}
}
DankTextField {
id: anonInput
anchors.fill: parent
font.pixelSize: Theme.fontSizeMedium
textColor: Theme.surfaceText
text: wifiAnonymousIdentityInput
placeholderText: I18n.tr("Anonymous Identity (optional)")
backgroundColor: "transparent"
enabled: root.shouldBeVisible
onTextEdited: () => {
wifiAnonymousIdentityInput = text
}
}
}
Rectangle {
visible: requiresEnterprise
width: parent.width
height: 50
radius: Theme.cornerRadius
color: Theme.surfaceHover
border.color: domainMatchInput.activeFocus ? Theme.primary : Theme.outlineStrong
border.width: domainMatchInput.activeFocus ? 2 : 1
MouseArea {
anchors.fill: parent
onClicked: () => {
domainMatchInput.forceActiveFocus()
}
}
DankTextField {
id: domainMatchInput
anchors.fill: parent
font.pixelSize: Theme.fontSizeMedium
textColor: Theme.surfaceText
text: wifiDomainInput
placeholderText: I18n.tr("Domain (optional)")
backgroundColor: "transparent"
enabled: root.shouldBeVisible
onTextEdited: () => {
wifiDomainInput = text
}
}
}
Row {
spacing: Theme.spacingS
@@ -313,6 +398,8 @@ DankModal {
close()
wifiPasswordInput = ""
wifiUsernameInput = ""
wifiAnonymousIdentityInput = ""
wifiDomainInput = ""
}
}
}
@@ -344,10 +431,18 @@ DankModal {
enabled: parent.enabled
onClicked: () => {
const username = requiresEnterprise ? usernameInput.text : ""
NetworkService.connectToWifi(wifiPasswordSSID, passwordInput.text, username)
NetworkService.connectToWifi(
wifiPasswordSSID,
passwordInput.text,
username,
wifiAnonymousIdentityInput,
wifiDomainInput
)
close()
wifiPasswordInput = ""
wifiUsernameInput = ""
wifiAnonymousIdentityInput = ""
wifiDomainInput = ""
passwordInput.text = ""
if (requiresEnterprise) usernameInput.text = ""
}

View File

@@ -112,6 +112,8 @@ DankPopout {
mappings[Qt.Key_Up] = () => appLauncher.selectPrevious()
mappings[Qt.Key_Return] = () => appLauncher.launchSelected()
mappings[Qt.Key_Enter] = () => appLauncher.launchSelected()
mappings[Qt.Key_Tab] = () => appLauncher.viewMode === "grid" ? appLauncher.selectNextInRow() : appLauncher.selectNext()
mappings[Qt.Key_Backtab] = () => appLauncher.viewMode === "grid" ? appLauncher.selectPreviousInRow() : appLauncher.selectPrevious()
if (appLauncher.viewMode === "grid") {
mappings[Qt.Key_Right] = () => appLauncher.selectNextInRow()
@@ -217,6 +219,7 @@ DankPopout {
font.pixelSize: Theme.fontSizeLarge
enabled: appDrawerPopout.shouldBeVisible
ignoreLeftRightKeys: appLauncher.viewMode !== "list"
ignoreTabKeys: true
keyForwardTargets: [keyHandler]
onTextEdited: {
appLauncher.searchQuery = text
@@ -241,7 +244,7 @@ DankPopout {
return
}
const navigationKeys = [Qt.Key_Down, Qt.Key_Up, Qt.Key_Left, Qt.Key_Right]
const navigationKeys = [Qt.Key_Down, Qt.Key_Up, Qt.Key_Left, Qt.Key_Right, Qt.Key_Tab, Qt.Key_Backtab]
const isNavigationKey = navigationKeys.includes(event.key)
const isEmptyEnter = isEnterKey && !hasText
@@ -446,6 +449,8 @@ DankPopout {
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
wrapMode: Text.NoWrap
maximumLineCount: 1
}
StyledText {
@@ -617,8 +622,8 @@ DankPopout {
font.weight: Font.Medium
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
maximumLineCount: 2
wrapMode: Text.WordWrap
maximumLineCount: 1
wrapMode: Text.NoWrap
}
}
@@ -662,8 +667,8 @@ DankPopout {
id: contextMenu
property var currentApp: null
readonly property string appId: (currentApp && currentApp.desktopEntry) ? (currentApp.desktopEntry.id || currentApp.desktopEntry.execString || "") : ""
readonly property var desktopEntry: (currentApp && !currentApp.isPlugin && appLauncher && appLauncher._uniqueApps && currentApp.appIndex >= 0 && currentApp.appIndex < appLauncher._uniqueApps.length) ? appLauncher._uniqueApps[currentApp.appIndex] : null
readonly property string appId: desktopEntry ? (desktopEntry.id || desktopEntry.execString || "") : ""
readonly property bool isPinned: appId && SessionData.isPinnedApp(appId)
function show(x, y, app) {
@@ -765,7 +770,7 @@ DankPopout {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry) {
if (!contextMenu.desktopEntry) {
return
}
@@ -794,15 +799,16 @@ DankPopout {
}
Repeater {
model: contextMenu.currentApp && contextMenu.currentApp.desktopEntry && contextMenu.currentApp.desktopEntry.actions ? contextMenu.currentApp.desktopEntry.actions : []
model: contextMenu.desktopEntry && contextMenu.desktopEntry.actions ? contextMenu.desktopEntry.actions : []
Rectangle {
width: parent.width
width: Math.max(parent.width, actionRow.implicitWidth + Theme.spacingS * 2)
height: 32
radius: Theme.cornerRadius
color: actionMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
id: actionRow
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
@@ -838,9 +844,11 @@ DankPopout {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData && contextMenu.currentApp && contextMenu.currentApp.desktopEntry) {
SessionService.launchDesktopAction(contextMenu.currentApp.desktopEntry, modelData)
appLauncher.appLaunched(contextMenu.currentApp)
if (modelData && contextMenu.desktopEntry) {
SessionService.launchDesktopAction(contextMenu.desktopEntry, modelData)
if (contextMenu.currentApp) {
appLauncher.appLaunched(contextMenu.currentApp)
}
}
contextMenu.hide()
}
@@ -849,7 +857,7 @@ DankPopout {
}
Rectangle {
visible: contextMenu.currentApp && contextMenu.currentApp.desktopEntry && contextMenu.currentApp.desktopEntry.actions && contextMenu.currentApp.desktopEntry.actions.length > 0
visible: contextMenu.desktopEntry && contextMenu.desktopEntry.actions && contextMenu.desktopEntry.actions.length > 0
width: parent.width - Theme.spacingS * 2
height: 5
anchors.horizontalCenter: parent.horizontalCenter
@@ -959,9 +967,11 @@ DankPopout {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (contextMenu.currentApp && contextMenu.currentApp.desktopEntry) {
SessionService.launchDesktopEntry(contextMenu.currentApp.desktopEntry, true)
appLauncher.appLaunched(contextMenu.currentApp)
if (contextMenu.desktopEntry) {
SessionService.launchDesktopEntry(contextMenu.desktopEntry, true)
if (contextMenu.currentApp) {
appLauncher.appLaunched(contextMenu.currentApp)
}
}
contextMenu.hide()
}

View File

@@ -18,7 +18,7 @@ Item {
property int debounceInterval: 50
property bool keyboardNavigationActive: false
property bool suppressUpdatesWhileLaunching: false
readonly property var categories: {
property var categories: {
const allCategories = AppSearchService.getAllCategories().filter(cat => cat !== "Education" && cat !== "Science")
const result = [I18n.tr("All")]
return result.concat(allCategories.filter(cat => cat !== I18n.tr("All")))
@@ -27,11 +27,37 @@ Item {
property var appUsageRanking: AppUsageHistoryData.appUsageRanking || {}
property alias model: filteredModel
property var _watchApplications: AppSearchService.applications
property var _uniqueApps: []
property bool _isTriggered: false
property string _triggeredCategory: ""
property bool _updatingFromTrigger: false
signal appLaunched(var app)
signal categorySelected(string category)
signal viewModeSelected(string mode)
function updateCategories() {
const allCategories = AppSearchService.getAllCategories().filter(cat => cat !== "Education" && cat !== "Science")
const result = [I18n.tr("All")]
categories = result.concat(allCategories.filter(cat => cat !== I18n.tr("All")))
}
Connections {
target: PluginService
function onPluginLoaded() { updateCategories() }
function onPluginUnloaded() { updateCategories() }
function onPluginListUpdated() { updateCategories() }
}
Connections {
target: SettingsData
function onSortAppsAlphabeticallyChanged() {
updateFilteredModel()
}
}
function updateFilteredModel() {
if (suppressUpdatesWhileLaunching) {
suppressUpdatesWhileLaunching = false
@@ -41,50 +67,112 @@ Item {
selectedIndex = 0
keyboardNavigationActive = false
const triggerResult = checkPluginTriggers(searchQuery)
if (triggerResult.triggered) {
console.log("AppLauncher: Plugin trigger detected:", triggerResult.trigger, "for plugin:", triggerResult.pluginId)
}
let apps = []
const allCategory = I18n.tr("All")
if (searchQuery.length === 0) {
apps = selectedCategory === allCategory ? AppSearchService.getAppsInCategory(allCategory) : AppSearchService.getAppsInCategory(selectedCategory).slice(0, maxResults)
const emptyTriggerPlugins = typeof PluginService !== "undefined" ? PluginService.getPluginsWithEmptyTrigger() : []
if (triggerResult.triggered) {
_isTriggered = true
_triggeredCategory = triggerResult.pluginCategory
_updatingFromTrigger = true
selectedCategory = triggerResult.pluginCategory
_updatingFromTrigger = false
apps = AppSearchService.getPluginItems(triggerResult.pluginCategory, triggerResult.query)
} else {
if (selectedCategory === allCategory) {
apps = AppSearchService.searchApplications(searchQuery)
} else {
const categoryApps = AppSearchService.getAppsInCategory(selectedCategory)
if (categoryApps.length > 0) {
const allSearchResults = AppSearchService.searchApplications(searchQuery)
const categoryNames = new Set(categoryApps.map(app => app.name))
apps = allSearchResults.filter(searchApp => categoryNames.has(searchApp.name)).slice(0, maxResults)
if (_isTriggered) {
_updatingFromTrigger = true
selectedCategory = allCategory
_updatingFromTrigger = false
_isTriggered = false
_triggeredCategory = ""
}
if (searchQuery.length === 0) {
if (selectedCategory === allCategory) {
let emptyTriggerItems = []
emptyTriggerPlugins.forEach(pluginId => {
const plugin = PluginService.getLauncherPlugin(pluginId)
const pluginCategory = plugin.name || pluginId
const items = AppSearchService.getPluginItems(pluginCategory, "")
emptyTriggerItems = emptyTriggerItems.concat(items)
})
apps = AppSearchService.applications.concat(emptyTriggerItems)
} else {
apps = []
apps = AppSearchService.getAppsInCategory(selectedCategory).slice(0, maxResults)
}
} else {
if (selectedCategory === allCategory) {
apps = AppSearchService.searchApplications(searchQuery)
let emptyTriggerItems = []
emptyTriggerPlugins.forEach(pluginId => {
const plugin = PluginService.getLauncherPlugin(pluginId)
const pluginCategory = plugin.name || pluginId
const items = AppSearchService.getPluginItems(pluginCategory, searchQuery)
emptyTriggerItems = emptyTriggerItems.concat(items)
})
apps = apps.concat(emptyTriggerItems)
} else {
const categoryApps = AppSearchService.getAppsInCategory(selectedCategory)
if (categoryApps.length > 0) {
const allSearchResults = AppSearchService.searchApplications(searchQuery)
const categoryNames = new Set(categoryApps.map(app => app.name))
apps = allSearchResults.filter(searchApp => categoryNames.has(searchApp.name)).slice(0, maxResults)
} else {
apps = []
}
}
}
}
if (searchQuery.length === 0) {
apps = apps.sort((a, b) => {
const aId = a.id || a.execString || a.exec || ""
const bId = b.id || b.execString || b.exec || ""
const aUsage = appUsageRanking[aId] ? appUsageRanking[aId].usageCount : 0
const bUsage = appUsageRanking[bId] ? appUsageRanking[bId].usageCount : 0
if (aUsage !== bUsage) {
return bUsage - aUsage
}
return (a.name || "").localeCompare(b.name || "")
})
if (SettingsData.sortAppsAlphabetically) {
apps = apps.sort((a, b) => {
return (a.name || "").localeCompare(b.name || "")
})
} else {
apps = apps.sort((a, b) => {
const aId = a.id || a.execString || a.exec || ""
const bId = b.id || b.execString || b.exec || ""
const aUsage = appUsageRanking[aId] ? appUsageRanking[aId].usageCount : 0
const bUsage = appUsageRanking[bId] ? appUsageRanking[bId].usageCount : 0
if (aUsage !== bUsage) {
return bUsage - aUsage
}
return (a.name || "").localeCompare(b.name || "")
})
}
}
const seenNames = new Set()
const uniqueApps = []
apps.forEach(app => {
if (app) {
const itemKey = app.name + "|" + (app.execString || app.exec || app.action || "")
if (seenNames.has(itemKey)) {
return
}
seenNames.add(itemKey)
uniqueApps.push(app)
const isPluginItem = app.action !== undefined
filteredModel.append({
"name": app.name || "",
"exec": app.execString || "",
"exec": app.execString || app.exec || app.action || "",
"icon": app.icon || "application-x-executable",
"comment": app.comment || "",
"categories": app.categories || [],
"desktopEntry": app
"isPlugin": isPluginItem,
"appIndex": uniqueApps.length - 1
})
}
})
root._uniqueApps = uniqueApps
}
function selectNext() {
@@ -128,13 +216,25 @@ Item {
}
function launchApp(appData) {
if (!appData) {
if (!appData || typeof appData.appIndex === "undefined" || appData.appIndex < 0 || appData.appIndex >= _uniqueApps.length) {
return
}
suppressUpdatesWhileLaunching = true
SessionService.launchDesktopEntry(appData.desktopEntry)
appLaunched(appData)
AppUsageHistoryData.addAppUsage(appData.desktopEntry)
const actualApp = _uniqueApps[appData.appIndex]
if (appData.isPlugin) {
const pluginId = getPluginIdForItem(actualApp)
if (pluginId) {
AppSearchService.executePluginItem(actualApp, pluginId)
appLaunched(appData)
return
}
} else {
SessionService.launchDesktopEntry(actualApp)
appLaunched(appData)
AppUsageHistoryData.addAppUsage(actualApp)
}
}
function setCategory(category) {
@@ -154,7 +254,12 @@ Item {
updateFilteredModel()
}
}
onSelectedCategoryChanged: updateFilteredModel()
onSelectedCategoryChanged: {
if (_updatingFromTrigger) {
return
}
updateFilteredModel()
}
onAppUsageRankingChanged: updateFilteredModel()
on_WatchApplicationsChanged: updateFilteredModel()
Component.onCompleted: {
@@ -172,4 +277,63 @@ Item {
repeat: false
onTriggered: updateFilteredModel()
}
// Plugin trigger system functions
function checkPluginTriggers(query) {
if (!query || typeof PluginService === "undefined") {
return { triggered: false, pluginCategory: "", query: "" }
}
const triggers = PluginService.getAllPluginTriggers()
for (const trigger in triggers) {
if (query.startsWith(trigger)) {
const pluginId = triggers[trigger]
const plugin = PluginService.getLauncherPlugin(pluginId)
if (plugin) {
const remainingQuery = query.substring(trigger.length).trim()
const result = {
triggered: true,
pluginId: pluginId,
pluginCategory: plugin.name || pluginId,
query: remainingQuery,
trigger: trigger
}
return result
}
}
}
return { triggered: false, pluginCategory: "", query: "" }
}
function getPluginIdForItem(item) {
if (!item || !item.categories || typeof PluginService === "undefined") {
return null
}
const launchers = PluginService.getLauncherPlugins()
for (const pluginId in launchers) {
const plugin = launchers[pluginId]
const pluginCategory = plugin.name || pluginId
let hasCategory = false
if (Array.isArray(item.categories)) {
hasCategory = item.categories.includes(pluginCategory)
} else if (item.categories && typeof item.categories.count !== "undefined") {
for (let i = 0; i < item.categories.count; i++) {
if (item.categories.get(i) === pluginCategory) {
hasCategory = true
break
}
}
}
if (hasCategory) {
return pluginId
}
}
return null
}
}

View File

@@ -13,6 +13,7 @@ Item {
property var pluginDetailInstance: null
property var widgetModel: null
property var collapseCallback: null
Loader {
id: pluginDetailLoader
@@ -32,6 +33,54 @@ Item {
sourceComponent: null
}
Connections {
target: coreDetailLoader.item
enabled: root.expandedSection.startsWith("brightnessSlider_")
ignoreUnknownSignals: true
function onDeviceNameChanged(newDeviceName) {
if (root.expandedWidgetData && root.expandedWidgetData.id === "brightnessSlider") {
const widgets = SettingsData.controlCenterWidgets || []
const newWidgets = widgets.map(w => {
if (w.id === "brightnessSlider" && w.instanceId === root.expandedWidgetData.instanceId) {
const updatedWidget = Object.assign({}, w)
updatedWidget.deviceName = newDeviceName
return updatedWidget
}
return w
})
SettingsData.setControlCenterWidgets(newWidgets)
if (root.collapseCallback) {
root.collapseCallback()
}
}
}
}
Connections {
target: coreDetailLoader.item
enabled: root.expandedSection.startsWith("diskUsage_")
ignoreUnknownSignals: true
function onMountPathChanged(newMountPath) {
if (root.expandedWidgetData && root.expandedWidgetData.id === "diskUsage") {
const widgets = SettingsData.controlCenterWidgets || []
const newWidgets = widgets.map(w => {
if (w.id === "diskUsage" && w.instanceId === root.expandedWidgetData.instanceId) {
const updatedWidget = Object.assign({}, w)
updatedWidget.mountPath = newMountPath
return updatedWidget
}
return w
})
SettingsData.setControlCenterWidgets(newWidgets)
if (root.collapseCallback) {
root.collapseCallback()
}
}
}
}
onExpandedSectionChanged: {
if (pluginDetailInstance) {
pluginDetailInstance.destroy()
@@ -91,6 +140,12 @@ Item {
return
}
if (root.expandedSection.startsWith("brightnessSlider_")) {
coreDetailLoader.sourceComponent = brightnessDetailComponent
coreDetailLoader.active = parent.height > 0
return
}
switch (root.expandedSection) {
case "network":
case "wifi": coreDetailLoader.sourceComponent = networkDetailComponent; break
@@ -144,22 +199,14 @@ Item {
DiskUsageDetail {
currentMountPath: root.expandedWidgetData?.mountPath || "/"
instanceId: root.expandedWidgetData?.instanceId || ""
}
}
onMountPathChanged: (newMountPath) => {
if (root.expandedWidgetData && root.expandedWidgetData.id === "diskUsage") {
const widgets = SettingsData.controlCenterWidgets || []
const newWidgets = widgets.map(w => {
if (w.id === "diskUsage" && w.instanceId === root.expandedWidgetData.instanceId) {
const updatedWidget = Object.assign({}, w)
updatedWidget.mountPath = newMountPath
return updatedWidget
}
return w
})
SettingsData.setControlCenterWidgets(newWidgets)
}
}
Component {
id: brightnessDetailComponent
BrightnessDetail {
currentDeviceName: root.expandedWidgetData?.deviceName || ""
instanceId: root.expandedWidgetData?.instanceId || ""
}
}
}

View File

@@ -20,6 +20,11 @@ Column {
signal removeWidget(int index)
signal moveWidget(int fromIndex, int toIndex)
signal toggleWidgetSize(int index)
signal collapseRequested()
function requestCollapse() {
collapseRequested()
}
spacing: editMode ? Theme.spacingL : Theme.spacingS
@@ -82,7 +87,7 @@ Column {
const widgets = SettingsData.controlCenterWidgets || []
for (var i = 0; i < widgets.length; i++) {
if (widgets[i].id === modelData.id) {
if (modelData.id === "diskUsage") {
if (modelData.id === "diskUsage" || modelData.id === "brightnessSlider") {
if (widgets[i].instanceId === modelData.instanceId) {
return i
}
@@ -164,6 +169,11 @@ Column {
return rowWidgets.some(w => w.id === "diskUsage" && w.instanceId === expandedInstanceId)
}
if (root.expandedSection.startsWith("brightnessSlider_") && root.expandedWidgetData) {
const expandedInstanceId = root.expandedWidgetData.instanceId
return rowWidgets.some(w => w.id === "brightnessSlider" && w.instanceId === expandedInstanceId)
}
return rowIndex === root.expandedRowIndex
}
visible: active
@@ -171,6 +181,7 @@ Column {
expandedWidgetData: root.expandedWidgetData
bluetoothCodecSelector: root.bluetoothCodecSelector
widgetModel: root.model
collapseCallback: root.requestCollapse
}
}
}
@@ -467,10 +478,19 @@ Column {
height: 16
BrightnessSliderRow {
id: brightnessSliderRow
anchors.centerIn: parent
width: parent.width
height: 14
deviceName: widgetData.deviceName || ""
instanceId: widgetData.instanceId || ""
property color sliderTrackColor: Theme.surfaceContainerHigh
onIconClicked: {
if (!root.editMode && DisplayService.devices && DisplayService.devices.length > 1) {
root.expandClicked(widgetData, widgetIndex)
}
}
}
}
}
@@ -600,8 +620,9 @@ Column {
}
case "darkMode":
{
const newMode = !SessionData.isLightMode
Theme.screenTransition()
Theme.setLightMode(!SessionData.isLightMode)
Theme.setLightMode(newMode)
break
}
case "doNotDisturb":
@@ -680,8 +701,9 @@ Column {
}
case "darkMode":
{
const newMode = !SessionData.isLightMode
Theme.screenTransition()
Theme.setLightMode(!SessionData.isLightMode)
Theme.setLightMode(newMode)
break
}
case "doNotDisturb":

View File

@@ -69,6 +69,7 @@ Row {
anchors.right: parent.right
anchors.bottom: parent.bottom
spacing: Theme.spacingS
clip: true
model: root.availableWidgets
delegate: Rectangle {

View File

@@ -11,6 +11,7 @@ Rectangle {
signal powerButtonClicked()
signal lockRequested()
signal editModeToggled()
signal settingsButtonClicked()
implicitHeight: 70
radius: Theme.cornerRadius
@@ -96,6 +97,7 @@ Rectangle {
iconColor: Theme.surfaceText
backgroundColor: "transparent"
onClicked: {
root.settingsButtonClicked()
settingsModal.show()
}
}

View File

@@ -73,21 +73,21 @@ DankPopout {
onShouldBeVisibleChanged: {
if (shouldBeVisible) {
Qt.callLater(() => {
if (NetworkService.activeService) {
NetworkService.activeService.autoRefreshEnabled = NetworkService.wifiEnabled
}
if (UserInfoService)
UserInfoService.getUptime()
})
if (NetworkService.activeService) {
NetworkService.activeService.autoRefreshEnabled = NetworkService.wifiEnabled
}
if (UserInfoService)
UserInfoService.getUptime()
})
} else {
Qt.callLater(() => {
if (NetworkService.activeService) {
NetworkService.activeService.autoRefreshEnabled = false
}
if (BluetoothService.adapter && BluetoothService.adapter.discovering)
BluetoothService.adapter.discovering = false
editMode = false
})
if (NetworkService.activeService) {
NetworkService.activeService.autoRefreshEnabled = false
}
if (BluetoothService.adapter && BluetoothService.adapter.discovering)
BluetoothService.adapter.discovering = false
editMode = false
})
}
}
@@ -108,8 +108,7 @@ DankPopout {
return Qt.rgba(surface.r, surface.g, surface.b, transparency)
}
radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0
antialiasing: true
smooth: true
@@ -140,6 +139,9 @@ DankPopout {
root.close()
root.lockRequested()
}
onSettingsButtonClicked: {
root.close()
}
}
DragDropGrid {
@@ -153,17 +155,20 @@ DankPopout {
bluetoothCodecSelector: bluetoothCodecSelector
colorPickerModal: root.colorPickerModal
onExpandClicked: (widgetData, globalIndex) => {
root.expandedWidgetIndex = globalIndex
root.expandedWidgetData = widgetData
if (widgetData.id === "diskUsage") {
root.toggleSection("diskUsage_" + (widgetData.instanceId || "default"))
} else {
root.toggleSection(widgetData.id)
}
}
onRemoveWidget: (index) => widgetModel.removeWidget(index)
root.expandedWidgetIndex = globalIndex
root.expandedWidgetData = widgetData
if (widgetData.id === "diskUsage") {
root.toggleSection("diskUsage_" + (widgetData.instanceId || "default"))
} else if (widgetData.id === "brightnessSlider") {
root.toggleSection("brightnessSlider_" + (widgetData.instanceId || "default"))
} else {
root.toggleSection(widgetData.id)
}
}
onRemoveWidget: index => widgetModel.removeWidget(index)
onMoveWidget: (fromIndex, toIndex) => widgetModel.moveWidget(fromIndex, toIndex)
onToggleWidgetSize: (index) => widgetModel.toggleWidgetSize(index)
onToggleWidgetSize: index => widgetModel.toggleWidgetSize(index)
onCollapseRequested: root.collapseAll()
}
EditControls {
@@ -171,12 +176,13 @@ DankPopout {
visible: editMode
popoutContent: controlContent
availableWidgets: {
if (!editMode) return []
if (!editMode)
return []
const existingIds = (SettingsData.controlCenterWidgets || []).map(w => w.id)
const allWidgets = widgetModel.baseWidgetDefinitions.concat(widgetModel.getPluginWidgets())
return allWidgets.filter(w => w.allowMultiple || !existingIds.includes(w.id))
}
onAddWidget: (widgetId) => widgetModel.addWidget(widgetId)
onAddWidget: widgetId => widgetModel.addWidget(widgetId)
onResetToDefault: () => widgetModel.resetToDefault()
onClearAll: () => widgetModel.clearAll()
}
@@ -199,10 +205,10 @@ DankPopout {
id: bluetoothDetailComponent
BluetoothDetail {
id: bluetoothDetail
onShowCodecSelector: function(device) {
onShowCodecSelector: function (device) {
if (contentLoader.item && contentLoader.item.bluetoothCodecSelector) {
contentLoader.item.bluetoothCodecSelector.show(device)
contentLoader.item.bluetoothCodecSelector.codecSelected.connect(function(deviceAddress, codecName) {
contentLoader.item.bluetoothCodecSelector.codecSelected.connect(function (deviceAddress, codecName) {
bluetoothDetail.updateDeviceCodecDisplay(deviceAddress, codecName)
})
}
@@ -227,4 +233,4 @@ DankPopout {
property var colorPickerModal: null
property var powerMenuModalLoader: null
}
}

View File

@@ -0,0 +1,174 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
property string currentDeviceName: ""
property string instanceId: ""
signal deviceNameChanged(string newDeviceName)
implicitHeight: brightnessContent.height + Theme.spacingM
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0
DankFlickable {
id: brightnessContent
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: Theme.spacingM
anchors.topMargin: Theme.spacingM
contentHeight: brightnessColumn.height
clip: true
Column {
id: brightnessColumn
width: parent.width
spacing: Theme.spacingS
Item {
width: parent.width
height: 100
visible: !DisplayService.brightnessAvailable || !DisplayService.devices || DisplayService.devices.length === 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingM
DankIcon {
anchors.horizontalCenter: parent.horizontalCenter
name: DisplayService.brightnessAvailable ? "brightness_6" : "error"
size: 32
color: DisplayService.brightnessAvailable ? Theme.primary : Theme.error
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: DisplayService.brightnessAvailable ? "No brightness devices available" : "Brightness control not available"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
horizontalAlignment: Text.AlignHCenter
}
}
}
Repeater {
model: DisplayService.devices || []
delegate: Rectangle {
required property var modelData
required property int index
width: parent.width
height: 80
radius: Theme.cornerRadius
color: Theme.surfaceContainerHighest
border.color: modelData.name === currentDeviceName ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: modelData.name === currentDeviceName ? 2 : 0
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
spacing: Theme.spacingM
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
DankIcon {
name: {
const deviceClass = modelData.class || ""
const deviceName = modelData.name || ""
if (deviceClass === "backlight" || deviceClass === "ddc") {
const brightness = modelData.percentage || 50
if (brightness <= 33) return "brightness_low"
if (brightness <= 66) return "brightness_medium"
return "brightness_high"
} else if (deviceName.includes("kbd")) {
return "keyboard"
} else {
return "lightbulb"
}
}
size: Theme.iconSize
color: modelData.name === currentDeviceName ? Theme.primary : Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: (modelData.percentage || 50) + "%"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.parent.width - parent.parent.anchors.leftMargin - parent.spacing - 50 - Theme.spacingM
StyledText {
text: {
const name = modelData.name || ""
const deviceClass = modelData.class || ""
if (deviceClass === "backlight") {
return name.replace("_", " ").replace(/\b\w/g, c => c.toUpperCase())
}
return name
}
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: modelData.name === currentDeviceName ? Font.Medium : Font.Normal
elide: Text.ElideRight
width: parent.width
}
StyledText {
text: modelData.name
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
width: parent.width
}
StyledText {
text: {
const deviceClass = modelData.class || ""
if (deviceClass === "backlight") return "Backlight device"
if (deviceClass === "ddc") return "DDC/CI monitor"
if (deviceClass === "leds") return "LED device"
return deviceClass
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
width: parent.width
}
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
currentDeviceName = modelData.name
deviceNameChanged(modelData.name)
}
}
}
}
}
}
}

View File

@@ -29,6 +29,26 @@ Rectangle {
NetworkService.removeRef()
}
property int currentPreferenceIndex: {
if (DMSService.apiVersion < 5) {
return 1
}
const pref = NetworkService.userPreference
const status = NetworkService.networkStatus
let index = 1
if (pref === "ethernet") {
index = 0
} else if (pref === "wifi") {
index = 1
} else {
index = status === "ethernet" ? 0 : 1
}
return index
}
Row {
id: headerRow
anchors.left: parent.left
@@ -38,7 +58,7 @@ Rectangle {
anchors.rightMargin: Theme.spacingM
anchors.topMargin: Theme.spacingS
height: 40
StyledText {
id: headerText
text: I18n.tr("Network Settings")
@@ -47,32 +67,16 @@ Rectangle {
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: Math.max(0, parent.width - headerText.implicitWidth - preferenceControls.width - Theme.spacingM)
height: parent.height
}
DankButtonGroup {
id: preferenceControls
anchors.verticalCenter: parent.verticalCenter
visible: NetworkService.ethernetConnected
property int currentPreferenceIndex: {
const pref = NetworkService.userPreference
const status = NetworkService.networkStatus
let index = 1
if (pref === "ethernet") {
index = 0
} else if (pref === "wifi") {
index = 1
} else {
index = status === "ethernet" ? 0 : 1
}
return index
}
visible: DMSService.apiVersion >= 5
model: ["Ethernet", "WiFi"]
currentIndex: currentPreferenceIndex
@@ -92,7 +96,7 @@ Rectangle {
anchors.right: parent.right
anchors.margins: Theme.spacingM
anchors.topMargin: Theme.spacingM
visible: NetworkService.wifiToggling
visible: currentPreferenceIndex === 1 && NetworkService.wifiToggling
height: visible ? 80 : 0
Column {
@@ -131,7 +135,7 @@ Rectangle {
anchors.right: parent.right
anchors.margins: Theme.spacingM
anchors.topMargin: Theme.spacingM
visible: !NetworkService.wifiEnabled && !NetworkService.wifiToggling
visible: currentPreferenceIndex === 1 && !NetworkService.wifiEnabled && !NetworkService.wifiToggling
height: visible ? 120 : 0
Column {
@@ -179,7 +183,179 @@ Rectangle {
cursorShape: Qt.PointingHandCursor
onClicked: NetworkService.toggleWifiRadio()
}
}
}
}
DankFlickable {
id: wiredContent
anchors.top: headerRow.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: Theme.spacingM
anchors.topMargin: Theme.spacingM
visible: currentPreferenceIndex === 0 && DMSService.apiVersion >= 5
contentHeight: wiredColumn.height
clip: true
Column {
id: wiredColumn
width: parent.width
spacing: Theme.spacingS
Repeater {
model: sortedNetworks
property var sortedNetworks: {
const currentUuid = NetworkService.ethernetConnectionUuid
const networks = NetworkService.wiredConnections
let sorted = [...networks]
sorted.sort((a, b) => {
if (a.isActive && !b.isActive) return -1
if (!a.isActive && b.isActive) return 1
return a.id.localeCompare(b.id)
})
return sorted
}
delegate: Rectangle {
required property var modelData
required property int index
width: parent.width
height: 50
radius: Theme.cornerRadius
color: wiredNetworkMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.surfaceContainerHighest
border.color: Theme.primary
border.width: 0
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
spacing: Theme.spacingS
DankIcon {
name: "lan"
size: Theme.iconSize - 4
color: modelData.isActive ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: 200
StyledText {
text: modelData.id || "Unknown Config"
font.pixelSize: Theme.fontSizeMedium
color: modelData.isActive ? Theme.primary : Theme.surfaceText
font.weight: modelData.isActive ? Font.Medium : Font.Normal
elide: Text.ElideRight
width: parent.width
}
}
}
DankActionButton {
id: wiredOptionsButton
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
iconName: "more_horiz"
buttonSize: 28
onClicked: {
if (wiredNetworkContextMenu.visible) {
wiredNetworkContextMenu.close()
} else {
wiredNetworkContextMenu.currentID = modelData.id
wiredNetworkContextMenu.currentUUID = modelData.uuid
wiredNetworkContextMenu.currentConnected = modelData.isActive
wiredNetworkContextMenu.popup(wiredOptionsButton, -wiredNetworkContextMenu.width + wiredOptionsButton.width, wiredOptionsButton.height + Theme.spacingXS)
}
}
}
MouseArea {
id: wiredNetworkMouseArea
anchors.fill: parent
anchors.rightMargin: wiredOptionsButton.width + Theme.spacingS
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: function(event) {
if (modelData.uuid !== NetworkService.ethernetConnectionUuid) {
NetworkService.connectToSpecificWiredConfig(modelData.uuid)
}
event.accepted = true
}
}
}
}
}
}
Menu {
id: wiredNetworkContextMenu
width: 150
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
property string currentID: ""
property string currentUUID: ""
property bool currentConnected: false
background: Rectangle {
color: Theme.popupBackground()
radius: Theme.cornerRadius
border.width: 0
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
}
MenuItem {
text: "Activate"
height: !wiredNetworkContextMenu.currentConnected ? 32 : 0
contentItem: StyledText {
text: parent.text
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
leftPadding: Theme.spacingS
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: Theme.cornerRadius / 2
}
onTriggered: {
if (!networkContextMenu.currentConnected) {
NetworkService.connectToSpecificWiredConfig(wiredNetworkContextMenu.currentUUID)
}
}
}
MenuItem {
text: I18n.tr("Network Info")
height: wiredNetworkContextMenu.currentConnected ? 32 : 0
contentItem: StyledText {
text: parent.text
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
leftPadding: Theme.spacingS
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: Theme.cornerRadius / 2
}
onTriggered: {
let networkData = NetworkService.getWiredNetworkInfo(wiredNetworkContextMenu.currentUUID)
networkWiredInfoModal.showNetworkInfo(wiredNetworkContextMenu.currentID, networkData)
}
}
}
@@ -192,10 +368,10 @@ Rectangle {
anchors.bottom: parent.bottom
anchors.margins: Theme.spacingM
anchors.topMargin: Theme.spacingM
visible: NetworkService.wifiInterface && NetworkService.wifiEnabled && !NetworkService.wifiToggling
visible: currentPreferenceIndex === 1 && NetworkService.wifiEnabled && !NetworkService.wifiToggling
contentHeight: wifiColumn.height
clip: true
Column {
id: wifiColumn
width: parent.width
@@ -282,20 +458,20 @@ Rectangle {
spacing: Theme.spacingXS
StyledText {
text: modelData.ssid === NetworkService.currentWifiSSID ? "Connected" : (modelData.secured ? "Secured" : "Open")
text: modelData.ssid === NetworkService.currentWifiSSID ? "Connected" : (modelData.secured ? "Secured" : "Open")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: modelData.saved ? "Saved" : ""
text: modelData.saved ? "Saved" : ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
visible: text.length > 0
}
StyledText {
text: "• " + modelData.signal + "%"
text: (modelData.saved ? "• " : "") + modelData.signal + "%"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
@@ -449,6 +625,8 @@ Rectangle {
NetworkInfoModal {
id: networkInfoModal
}
NetworkWiredInfoModal {
id: networkWiredInfoModal
}
}

View File

@@ -105,7 +105,8 @@ QtObject {
"icon": "brightness_6",
"type": "slider",
"enabled": DisplayService.brightnessAvailable,
"warning": !DisplayService.brightnessAvailable ? "Brightness control not available" : undefined
"warning": !DisplayService.brightnessAvailable ? "Brightness control not available" : undefined,
"allowMultiple": true
}, {
"id": "inputVolumeSlider",
"text": "Input Volume Slider",

View File

@@ -8,9 +8,49 @@ import qs.Widgets
Row {
id: root
property string deviceName: ""
property string instanceId: ""
signal iconClicked()
height: 40
spacing: 0
property string targetDeviceName: {
if (!DisplayService.brightnessAvailable || !DisplayService.devices || DisplayService.devices.length === 0) {
return ""
}
if (deviceName && deviceName.length > 0) {
const found = DisplayService.devices.find(dev => dev.name === deviceName)
return found ? found.name : ""
}
const currentDeviceName = DisplayService.currentDevice
if (currentDeviceName) {
const found = DisplayService.devices.find(dev => dev.name === currentDeviceName)
return found ? found.name : ""
}
return DisplayService.devices.length > 0 ? DisplayService.devices[0].name : ""
}
property var targetDevice: {
if (!targetDeviceName || !DisplayService.devices) {
return null
}
return DisplayService.devices.find(dev => dev.name === targetDeviceName) || null
}
property real targetBrightness: {
if (!targetDeviceName) {
return 0
}
return DisplayService.getDeviceBrightness(targetDeviceName)
}
Rectangle {
width: Theme.iconSize + Theme.spacingS * 2
height: Theme.iconSize + Theme.spacingS * 2
@@ -24,23 +64,18 @@ Row {
id: iconArea
anchors.fill: parent
hoverEnabled: true
cursorShape: DisplayService.devices.length > 1 ? Qt.PointingHandCursor : Qt.ArrowCursor
cursorShape: DisplayService.devices && DisplayService.devices.length > 1 ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: function(event) {
if (DisplayService.devices.length > 1) {
if (deviceMenu.visible) {
deviceMenu.close()
} else {
deviceMenu.popup(iconArea, 0, iconArea.height + Theme.spacingXS)
}
event.accepted = true
onClicked: {
if (DisplayService.devices && DisplayService.devices.length > 1) {
root.iconClicked()
}
}
onEntered: {
tooltipLoader.active = true
if (tooltipLoader.item) {
const tooltipText = DisplayService.currentDevice ? "bl device: " + DisplayService.currentDevice : "Backlight Control"
const tooltipText = targetDevice ? "bl device: " + targetDevice.name : "Backlight Control"
const p = iconArea.mapToItem(null, iconArea.width / 2, 0)
tooltipLoader.item.show(tooltipText, p.x, p.y - 40, null)
}
@@ -56,15 +91,23 @@ Row {
DankIcon {
anchors.centerIn: parent
name: {
if (!DisplayService.brightnessAvailable) return "brightness_low"
if (!DisplayService.brightnessAvailable || !targetDevice) {
return "brightness_low"
}
let brightness = DisplayService.brightnessLevel
if (brightness <= 33) return "brightness_low"
if (brightness <= 66) return "brightness_medium"
return "brightness_high"
if (targetDevice.class === "backlight" || targetDevice.class === "ddc") {
const brightness = targetBrightness
if (brightness <= 33) return "brightness_low"
if (brightness <= 66) return "brightness_medium"
return "brightness_high"
} else if (targetDevice.name.includes("kbd")) {
return "keyboard"
} else {
return "lightbulb"
}
}
size: Theme.iconSize
color: DisplayService.brightnessAvailable && DisplayService.brightnessLevel > 0 ? Theme.primary : Theme.surfaceText
color: DisplayService.brightnessAvailable && targetDevice && targetBrightness > 0 ? Theme.primary : Theme.surfaceText
}
}
}
@@ -72,88 +115,19 @@ Row {
DankSlider {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
enabled: DisplayService.brightnessAvailable
enabled: DisplayService.brightnessAvailable && targetDeviceName.length > 0
minimum: 1
maximum: 100
value: {
let level = DisplayService.brightnessLevel
if (level > 100) {
let deviceInfo = DisplayService.getCurrentDeviceInfo()
if (deviceInfo && deviceInfo.max > 0) {
return Math.round((level / deviceInfo.max) * 100)
}
return 50
}
return level
}
value: targetBrightness
onSliderValueChanged: function(newValue) {
if (DisplayService.brightnessAvailable) {
DisplayService.setBrightness(newValue)
if (DisplayService.brightnessAvailable && targetDeviceName) {
DisplayService.setBrightness(newValue, targetDeviceName)
}
}
thumbOutlineColor: Theme.surfaceContainer
trackColor: Theme.surfaceContainerHigh
}
Menu {
id: deviceMenu
width: 200
closePolicy: Popup.CloseOnEscape
background: Rectangle {
color: Theme.popupBackground()
radius: Theme.cornerRadius
border.width: 0
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
}
Instantiator {
model: DisplayService.devices
delegate: MenuItem {
required property var modelData
required property int index
property string deviceName: modelData.name || ""
property string deviceClass: modelData.class || ""
text: deviceName
font.pixelSize: Theme.fontSizeMedium
height: 40
indicator: Rectangle {
visible: DisplayService.currentDevice === parent.deviceName
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingS
width: 4
height: parent.height - Theme.spacingS * 2
radius: 2
color: Theme.primary
}
contentItem: StyledText {
text: parent.text
font: parent.font
color: DisplayService.currentDevice === parent.deviceName ? Theme.primary : Theme.surfaceText
leftPadding: Theme.spacingL
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: Theme.cornerRadius / 2
}
onTriggered: {
DisplayService.setCurrentDevice(deviceName, true)
deviceMenu.close()
}
}
onObjectAdded: (index, object) => deviceMenu.insertItem(index, object)
onObjectRemoved: (index, object) => deviceMenu.removeItem(object)
}
}
Loader {
id: tooltipLoader
active: false

View File

@@ -15,6 +15,11 @@ function addWidget(widgetId) {
widget.mountPath = "/"
}
if (widgetId === "brightnessSlider") {
widget.instanceId = generateUniqueId()
widget.deviceName = ""
}
widgets.push(widget)
SettingsData.setControlCenterWidgets(widgets)
}

View File

@@ -1,5 +1,7 @@
import QtQuick
import Quickshell.Hyprland
import qs.Common
import qs.Services
Item {
id: root
@@ -16,6 +18,33 @@ Item {
anchors.topMargin: -(SettingsData.dankBarGothCornersEnabled && !axis.isVertical && axis.edge === "bottom" ? barWindow._wingR : 0)
anchors.bottomMargin: -(SettingsData.dankBarGothCornersEnabled && !axis.isVertical && axis.edge === "top" ? barWindow._wingR : 0)
readonly property real dpr: {
if (CompositorService.isNiri && barWindow.screen) {
const niriScale = NiriService.displayScales[barWindow.screen.name]
if (niriScale !== undefined) return niriScale
}
if (CompositorService.isHyprland && barWindow.screen) {
const hyprlandMonitor = Hyprland.monitors.values.find(m => m.name === barWindow.screen.name)
if (hyprlandMonitor?.scale !== undefined) return hyprlandMonitor.scale
}
return barWindow.screen?.devicePixelRatio || 1
}
function requestRepaint() {
debounceTimer.restart()
}
Timer {
id: debounceTimer
interval: 50
repeat: false
onTriggered: {
barShape.requestPaint()
barTint.requestPaint()
barBorder.requestPaint()
}
}
Canvas {
id: barShape
anchors.fill: parent
@@ -23,46 +52,48 @@ Item {
renderTarget: Canvas.FramebufferObject
renderStrategy: Canvas.Cooperative
readonly property real correctWidth: root.width
readonly property real correctHeight: root.height
canvasSize: Qt.size(barWindow.px(correctWidth), barWindow.px(correctHeight))
readonly property real correctWidth: Theme.px(root.width, dpr)
readonly property real correctHeight: Theme.px(root.height, dpr)
canvasSize: Qt.size(correctWidth, correctHeight)
property real wing: SettingsData.dankBarGothCornersEnabled ? barWindow._wingR : 0
property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.cornerRadius
property real wing: SettingsData.dankBarGothCornersEnabled ? Theme.px(barWindow._wingR, dpr) : 0
property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.px(Theme.cornerRadius, dpr)
onWingChanged: requestPaint()
onRtChanged: requestPaint()
onCorrectWidthChanged: requestPaint()
onCorrectHeightChanged: requestPaint()
onVisibleChanged: if (visible) requestPaint()
Component.onCompleted: requestPaint()
onWingChanged: root.requestRepaint()
onRtChanged: root.requestRepaint()
onCorrectWidthChanged: root.requestRepaint()
onCorrectHeightChanged: root.requestRepaint()
onVisibleChanged: if (visible) root.requestRepaint()
Component.onCompleted: root.requestRepaint()
Connections {
target: root
function onDprChanged() { root.requestRepaint() }
}
Connections {
target: barWindow
function on_BgColorChanged() { barShape.requestPaint() }
function on_DprChanged() { barShape.requestPaint() }
function on_BgColorChanged() { root.requestRepaint() }
}
Connections {
target: Theme
function onIsLightModeChanged() { barShape.requestPaint() }
function onIsLightModeChanged() { root.requestRepaint() }
function onSurfaceContainerChanged() { root.requestRepaint() }
}
onPaint: {
const ctx = getContext("2d")
const scale = barWindow._dpr
const W = barWindow.px(barWindow.isVertical ? correctHeight : correctWidth)
const H_raw = barWindow.px(barWindow.isVertical ? correctWidth : correctHeight)
const R = barWindow.px(wing)
const RT = barWindow.px(rt)
const W = barWindow.isVertical ? correctHeight : correctWidth
const H_raw = barWindow.isVertical ? correctWidth : correctHeight
const R = wing
const RT = rt
const H = H_raw - (R > 0 ? R : 0)
const isTop = SettingsData.dankBarPosition === SettingsData.Position.Top
const isBottom = SettingsData.dankBarPosition === SettingsData.Position.Bottom
const isLeft = SettingsData.dankBarPosition === SettingsData.Position.Left
const isRight = SettingsData.dankBarPosition === SettingsData.Position.Right
ctx.scale(scale, scale)
function drawTopPath() {
ctx.beginPath()
ctx.moveTo(RT, 0)
@@ -118,48 +149,50 @@ Item {
renderTarget: Canvas.FramebufferObject
renderStrategy: Canvas.Cooperative
readonly property real correctWidth: root.width
readonly property real correctHeight: root.height
canvasSize: Qt.size(barWindow.px(correctWidth), barWindow.px(correctHeight))
readonly property real correctWidth: Theme.px(root.width, dpr)
readonly property real correctHeight: Theme.px(root.height, dpr)
canvasSize: Qt.size(correctWidth, correctHeight)
property real wing: SettingsData.dankBarGothCornersEnabled ? barWindow._wingR : 0
property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.cornerRadius
property real wing: SettingsData.dankBarGothCornersEnabled ? Theme.px(barWindow._wingR, dpr) : 0
property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.px(Theme.cornerRadius, dpr)
property real alphaTint: (barWindow._bgColor?.a ?? 1) < 0.99 ? (Theme.stateLayerOpacity ?? 0) : 0
onWingChanged: requestPaint()
onRtChanged: requestPaint()
onAlphaTintChanged: requestPaint()
onCorrectWidthChanged: requestPaint()
onCorrectHeightChanged: requestPaint()
onVisibleChanged: if (visible) requestPaint()
Component.onCompleted: requestPaint()
onWingChanged: root.requestRepaint()
onRtChanged: root.requestRepaint()
onAlphaTintChanged: root.requestRepaint()
onCorrectWidthChanged: root.requestRepaint()
onCorrectHeightChanged: root.requestRepaint()
onVisibleChanged: if (visible) root.requestRepaint()
Component.onCompleted: root.requestRepaint()
Connections {
target: root
function onDprChanged() { root.requestRepaint() }
}
Connections {
target: barWindow
function on_BgColorChanged() { barTint.requestPaint() }
function on_DprChanged() { barTint.requestPaint() }
function on_BgColorChanged() { root.requestRepaint() }
}
Connections {
target: Theme
function onIsLightModeChanged() { barTint.requestPaint() }
function onIsLightModeChanged() { root.requestRepaint() }
function onSurfaceChanged() { root.requestRepaint() }
}
onPaint: {
const ctx = getContext("2d")
const scale = barWindow._dpr
const W = barWindow.px(barWindow.isVertical ? correctHeight : correctWidth)
const H_raw = barWindow.px(barWindow.isVertical ? correctWidth : correctHeight)
const R = barWindow.px(wing)
const RT = barWindow.px(rt)
const W = barWindow.isVertical ? correctHeight : correctWidth
const H_raw = barWindow.isVertical ? correctWidth : correctHeight
const R = wing
const RT = rt
const H = H_raw - (R > 0 ? R : 0)
const isTop = SettingsData.dankBarPosition === SettingsData.Position.Top
const isBottom = SettingsData.dankBarPosition === SettingsData.Position.Bottom
const isLeft = SettingsData.dankBarPosition === SettingsData.Position.Left
const isRight = SettingsData.dankBarPosition === SettingsData.Position.Right
ctx.scale(scale, scale)
function drawTopPath() {
ctx.beginPath()
ctx.moveTo(RT, 0)
@@ -211,53 +244,59 @@ Item {
Canvas {
id: barBorder
anchors.fill: parent
antialiasing: true
antialiasing: false
visible: SettingsData.dankBarBorderEnabled
renderTarget: Canvas.FramebufferObject
renderStrategy: Canvas.Cooperative
readonly property real correctWidth: root.width
readonly property real correctHeight: root.height
canvasSize: Qt.size(barWindow.px(correctWidth), barWindow.px(correctHeight))
readonly property real correctWidth: Theme.px(root.width, dpr)
readonly property real correctHeight: Theme.px(root.height, dpr)
canvasSize: Qt.size(correctWidth, correctHeight)
property real wing: SettingsData.dankBarGothCornersEnabled ? barWindow._wingR : 0
property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.cornerRadius
property real wing: SettingsData.dankBarGothCornersEnabled ? Theme.px(barWindow._wingR, dpr) : 0
property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.px(Theme.cornerRadius, dpr)
property bool borderEnabled: SettingsData.dankBarBorderEnabled
onWingChanged: requestPaint()
onRtChanged: requestPaint()
onBorderEnabledChanged: requestPaint()
onCorrectWidthChanged: requestPaint()
onCorrectHeightChanged: requestPaint()
onVisibleChanged: if (visible) requestPaint()
Component.onCompleted: requestPaint()
onWingChanged: root.requestRepaint()
onRtChanged: root.requestRepaint()
onBorderEnabledChanged: root.requestRepaint()
onCorrectWidthChanged: root.requestRepaint()
onCorrectHeightChanged: root.requestRepaint()
onVisibleChanged: if (visible) root.requestRepaint()
Component.onCompleted: root.requestRepaint()
Connections {
target: barWindow
function on_DprChanged() { barBorder.requestPaint() }
target: root
function onDprChanged() { root.requestRepaint() }
}
Connections {
target: Theme
function onSecondaryChanged() { barBorder.requestPaint() }
function onIsLightModeChanged() { root.requestRepaint() }
function onSurfaceTextChanged() { root.requestRepaint() }
function onPrimaryChanged() { root.requestRepaint() }
function onSecondaryChanged() { root.requestRepaint() }
function onOutlineChanged() { root.requestRepaint() }
}
Connections {
target: SettingsData
function onDankBarSpacingChanged() { barBorder.requestPaint() }
function onDankBarSquareCornersChanged() { barBorder.requestPaint() }
function onCornerRadiusChanged() { barBorder.requestPaint() }
function onDankBarBorderColorChanged() { root.requestRepaint() }
function onDankBarBorderOpacityChanged() { root.requestRepaint() }
function onDankBarBorderThicknessChanged() { root.requestRepaint() }
function onDankBarSpacingChanged() { root.requestRepaint() }
function onDankBarSquareCornersChanged() { root.requestRepaint() }
function onDankBarTransparencyChanged() { root.requestRepaint() }
}
onPaint: {
if (!borderEnabled) return
const ctx = getContext("2d")
const scale = barWindow._dpr
const W = barWindow.px(barWindow.isVertical ? correctHeight : correctWidth)
const H_raw = barWindow.px(barWindow.isVertical ? correctWidth : correctHeight)
const R = barWindow.px(wing)
const RT = barWindow.px(rt)
const W = barWindow.isVertical ? correctHeight : correctWidth
const H_raw = barWindow.isVertical ? correctWidth : correctHeight
const R = wing
const RT = rt
const H = H_raw - (R > 0 ? R : 0)
const isTop = SettingsData.dankBarPosition === SettingsData.Position.Top
const isBottom = SettingsData.dankBarPosition === SettingsData.Position.Bottom
@@ -267,8 +306,6 @@ Item {
const spacing = SettingsData.dankBarSpacing
const hasEdgeGap = spacing > 0 || RT > 0
ctx.scale(scale, scale)
function drawTopBorder() {
ctx.beginPath()
@@ -319,9 +356,17 @@ Item {
drawTopBorder()
ctx.restore()
ctx.lineWidth = 1
ctx.strokeStyle = Theme.secondary
const key = SettingsData.dankBarBorderColor || "surfaceText"
const base = (key === "surfaceText") ? Theme.surfaceText
: (key === "primary") ? Theme.primary
: Theme.secondary
const color = Theme.withAlpha(base, SettingsData.dankBarBorderOpacity ?? 1.0)
const thickness = Math.max(1, SettingsData.dankBarBorderThickness ?? 1)
ctx.globalCompositeOperation = "source-over"
ctx.lineWidth = thickness
ctx.strokeStyle = color
ctx.stroke()
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -364,6 +364,210 @@ DankPopout {
}
}
// Individual battery details for multiple batteries
Column {
width: parent.width
spacing: Theme.spacingS
visible: !BatteryService.usePreferred && BatteryService.batteries.length > 1
StyledText {
text: I18n.tr("Individual Batteries")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
font.weight: Font.Medium
}
Repeater {
model: BatteryService.batteries
delegate: StyledRect {
required property var modelData
required property int index
width: parent.width
height: batteryColumn.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainer
border.width: 0
Column {
id: batteryColumn
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
// Top row: name and percentage
Row {
width: parent.width
spacing: Theme.spacingM
Column {
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
width: parent.width - percentText.width - chargingIcon.width - Theme.spacingM * 2
StyledText {
text: modelData.model || `Battery ${index + 1}`
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
width: parent.width
}
StyledText {
text: modelData.nativePath
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
elide: Text.ElideMiddle
width: parent.width
}
}
Item {
width: 1
height: parent.height
}
StyledText {
id: percentText
text: `${Math.round(100 * modelData.percentage)}%`
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Bold
anchors.verticalCenter: parent.verticalCenter
}
DankIcon {
id: chargingIcon
name: modelData.state === UPowerDeviceState.Charging ? "bolt" : ""
size: Theme.iconSizeSmall
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
visible: modelData.state === UPowerDeviceState.Charging
}
}
// Bottom row: Health, Capacity and Time
Flow {
width: parent.width
spacing: Theme.spacingS
StyledRect {
width: (parent.width - Theme.spacingS * 2) / 3
height: 48
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.width: 0
Column {
anchors.centerIn: parent
spacing: 2
StyledText {
text: I18n.tr("Health")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: {
if (!modelData.healthSupported || modelData.healthPercentage <= 0)
return "N/A"
return `${Math.round(modelData.healthPercentage)}%`
}
font.pixelSize: Theme.fontSizeSmall
color: {
if (!modelData.healthSupported || modelData.healthPercentage <= 0)
return Theme.surfaceText
return modelData.healthPercentage < 80 ? Theme.error : Theme.surfaceText
}
font.weight: Font.Bold
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
StyledRect {
width: (parent.width - Theme.spacingS * 2) / 3
height: 48
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.width: 0
Column {
anchors.centerIn: parent
spacing: 2
StyledText {
text: I18n.tr("Capacity")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: modelData.energyCapacity > 0 ? `${modelData.energyCapacity.toFixed(1)}` : "N/A"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Bold
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
StyledRect {
width: (parent.width - Theme.spacingS * 2) / 3
height: 48
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.width: 0
Column {
anchors.centerIn: parent
spacing: 2
StyledText {
text: modelData.state === UPowerDeviceState.Charging
? I18n.tr("To Full")
: modelData.state === UPowerDeviceState.Discharging
? I18n.tr("Left") : ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: {
const time = modelData.state === UPowerDeviceState.Charging
? modelData.timeToFull
: modelData.state === UPowerDeviceState.Discharging && BatteryService.changeRate > 0
? (3600 * modelData.energy) / BatteryService.changeRate : 0
if (!time || time <= 0 || time > 86400)
return "N/A"
const hours = Math.floor(time / 3600)
const minutes = Math.floor((time % 3600) / 60)
return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Bold
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
}
}
}
}
DankButtonGroup {
property var profileModel: (typeof PowerProfiles !== "undefined") ? [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) : [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance]
property int currentProfileIndex: {

View File

@@ -75,10 +75,7 @@ Loader {
onLoaded: {
if (item) {
contentItemReady(item)
if (widgetId === "spacer") {
item.spacerSize = Qt.binding(() => spacerSize)
}
contentItemReady(item)
if (axis && "isVertical" in item) {
try {
item.isVertical = axis.isVertical
@@ -166,4 +163,4 @@ Loader {
function getWidgetEnabled(enabled) {
return enabled !== false
}
}
}

View File

@@ -123,4 +123,4 @@ Rectangle {
toggleBatteryPopup();
}
}
}
}

View File

@@ -98,6 +98,30 @@ Rectangle {
}
}
Row {
visible: SettingsData.showSeconds
spacing: 0
anchors.horizontalCenter: parent.horizontalCenter
StyledText {
text: String(systemClock?.date?.getSeconds()).padStart(2, '0').charAt(0)
font.pixelSize: Theme.barTextSize(barThickness)
color: Theme.surfaceText
font.weight: Font.Normal
width: 9
horizontalAlignment: Text.AlignHCenter
}
StyledText {
text: String(systemClock?.date?.getSeconds()).padStart(2, '0').charAt(1)
font.pixelSize: Theme.barTextSize(barThickness)
color: Theme.surfaceText
font.weight: Font.Normal
width: 9
horizontalAlignment: Text.AlignHCenter
}
}
Item {
width: 12
height: Theme.spacingM
@@ -191,8 +215,7 @@ Rectangle {
StyledText {
text: {
const format = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP"
return systemClock?.date?.toLocaleTimeString(Qt.locale(), format)
return systemClock?.date?.toLocaleTimeString(Qt.locale(), SettingsData.getEffectiveTimeFormat())
}
font.pixelSize: Theme.barTextSize(barThickness)
color: Theme.surfaceText

View File

@@ -115,7 +115,6 @@ Rectangle {
if (AudioService.sink && AudioService.sink.audio) {
AudioService.sink.audio.muted = false
AudioService.sink.audio.volume = newVolume / 100
AudioService.volumeChanged()
}
wheelEvent.accepted = true
}
@@ -220,7 +219,6 @@ Rectangle {
if (AudioService.sink && AudioService.sink.audio) {
AudioService.sink.audio.muted = false;
AudioService.sink.audio.volume = newVolume / 100;
AudioService.volumeChanged();
}
wheelEvent.accepted = true;
}

View File

@@ -98,29 +98,6 @@ Rectangle {
}
Process {
id: hyprlandLayoutProcess
running: false
command: ["hyprctl", "-j", "devices"]
stdout: StdioCollector {
onStreamFinished: {
try {
const data = JSON.parse(text)
// Find the main keyboard and get its active keymap
const mainKeyboard = data.keyboards.find(kb => kb.main === true)
root.hyprlandKeyboard = mainKeyboard.name
if (mainKeyboard && mainKeyboard.active_keymap) {
root.currentLayout = mainKeyboard.active_keymap
} else {
root.currentLayout = "Unknown"
}
} catch (e) {
root.currentLayout = "Unknown"
}
}
}
}
Timer {
id: updateTimer
interval: 1000
@@ -139,7 +116,24 @@ Rectangle {
if (CompositorService.isNiri) {
root.currentLayout = NiriService.getCurrentKeyboardLayoutName()
} else if (CompositorService.isHyprland) {
hyprlandLayoutProcess.running = true
Proc.runCommand(null, ["hyprctl", "-j", "devices"], (output, exitCode) => {
if (exitCode !== 0) {
root.currentLayout = "Unknown"
return
}
try {
const data = JSON.parse(output)
const mainKeyboard = data.keyboards.find(kb => kb.main === true)
root.hyprlandKeyboard = mainKeyboard.name
if (mainKeyboard && mainKeyboard.active_keymap) {
root.currentLayout = mainKeyboard.active_keymap
} else {
root.currentLayout = "Unknown"
}
} catch (e) {
root.currentLayout = "Unknown"
}
})
}
}
}

View File

@@ -30,8 +30,15 @@ Item {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton
onPressed: {
acceptedButtons: Qt.LeftButton | Qt.RightButton
onPressed: function (mouse){
if (mouse.button === Qt.RightButton) {
if (CompositorService.isNiri) {
NiriService.toggleOverview()
}
return
}
root.clicked();
if (popupTarget && popupTarget.setTriggerPosition) {
const globalPos = mapToGlobal(0, 0);

View File

@@ -128,7 +128,6 @@ Rectangle {
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.popupTarget && root.popupTarget.setTriggerPosition) {
@@ -139,36 +138,6 @@ Rectangle {
}
root.clicked()
}
onEntered: {
tooltipLoader.active = true
if (tooltipLoader.item && activePlayer) {
const globalPos = parent.mapToGlobal(parent.width / 2, parent.height / 2)
const screenX = root.parentScreen ? root.parentScreen.x : 0
const screenY = root.parentScreen ? root.parentScreen.y : 0
const relativeY = globalPos.y - screenY
const tooltipX = root.axis?.edge === "left" ? (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS) : (root.parentScreen.width - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS)
let identity = activePlayer.identity || ""
let isWebMedia = identity.toLowerCase().includes("firefox") || identity.toLowerCase().includes("chrome") || identity.toLowerCase().includes("chromium")
let title = activePlayer.trackTitle || "Unknown Track"
let subtitle = ""
if (isWebMedia && activePlayer.trackTitle) {
subtitle = activePlayer.trackArtist || identity
} else {
subtitle = activePlayer.trackArtist || ""
}
let tooltipText = subtitle.length > 0 ? title + " • " + subtitle : title
const isLeft = root.axis?.edge === "left"
tooltipLoader.item.show(tooltipText, screenX + tooltipX, relativeY, root.parentScreen, isLeft, !isLeft)
}
}
onExited: {
if (tooltipLoader.item) {
tooltipLoader.item.hide()
}
tooltipLoader.active = false
}
}
}
@@ -191,8 +160,7 @@ Rectangle {
MouseArea {
anchors.fill: parent
enabled: root.playerAvailable
hoverEnabled: enabled
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
onClicked: (mouse) => {
if (!activePlayer) return
@@ -208,12 +176,6 @@ Rectangle {
}
}
Loader {
id: tooltipLoader
active: false
sourceComponent: DankTooltip {}
}
Row {
id: mediaRow
@@ -314,9 +276,8 @@ Rectangle {
MouseArea {
anchors.fill: parent
enabled: root.playerAvailable && root.opacity > 0 && root.width > 0 && textContainer.visible
hoverEnabled: enabled
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: root.playerAvailable
cursorShape: Qt.PointingHandCursor
onPressed: {
if (root.popupTarget && root.popupTarget.setTriggerPosition) {
const globalPos = mapToGlobal(0, 0)
@@ -356,9 +317,9 @@ Rectangle {
id: prevArea
anchors.fill: parent
enabled: root.playerAvailable && root.width > 0
hoverEnabled: enabled
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: root.playerAvailable
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (activePlayer) {
activePlayer.previous();
@@ -386,9 +347,8 @@ Rectangle {
MouseArea {
anchors.fill: parent
enabled: root.playerAvailable && root.width > 0
hoverEnabled: enabled
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: root.playerAvailable
cursorShape: Qt.PointingHandCursor
onClicked: {
if (activePlayer) {
activePlayer.togglePlaying();
@@ -418,9 +378,9 @@ Rectangle {
id: nextArea
anchors.fill: parent
enabled: root.playerAvailable && root.width > 0
hoverEnabled: enabled
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: root.playerAvailable
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (activePlayer) {
activePlayer.next();

View File

@@ -22,7 +22,7 @@ Rectangle {
property Item windowRoot: (Window.window ? Window.window.contentItem : null)
readonly property var sortedToplevels: {
if (SettingsData.runningAppsCurrentWorkspace) {
return CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, parentScreen.name);
return CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, parentScreen?.name);
}
return CompositorService.sortedToplevels;
}

View File

@@ -153,7 +153,10 @@ Rectangle {
const name = split[0];
const path = split[1];
const fileName = name.substring(name.lastIndexOf("/") + 1);
let fileName = name.substring(name.lastIndexOf("/") + 1);
if (fileName.startsWith("dropboxstatus")) {
fileName = `hicolor/16x16/status/${fileName}`;
}
return `file://${path}/${fileName}`;
}
if (icon.startsWith("/") && !icon.startsWith("file://")) {

View File

@@ -20,6 +20,10 @@ Rectangle {
signal clicked()
Ref {
service: SystemUpdateService
}
width: isVertical ? widgetThickness : (updaterIcon.width + horizontalPadding * 2)
height: isVertical ? widgetThickness : widgetThickness
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius

View File

@@ -15,6 +15,9 @@ Rectangle {
property string screenName: ""
property real widgetHeight: 30
property real barThickness: 48
readonly property var sortedToplevels: {
return CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, parentScreen?.name);
}
property int currentWorkspace: {
if (CompositorService.isNiri) {
return getNiriActiveWorkspace()
@@ -30,9 +33,9 @@ Rectangle {
}
if (CompositorService.isHyprland) {
const baseList = getHyprlandWorkspaces()
// Filter out special:scratch_term
const filteredList = baseList.filter(ws => ws.name !== "special:scratch_term" && ws.id !== -98)
return SettingsData.showWorkspacePadding ? padWorkspaces(baseList) : baseList
// Filter out special workspaces
const filteredList = baseList.filter(ws => ws.id > -1)
return SettingsData.showWorkspacePadding ? padWorkspaces(filteredList) : filteredList
}
return [1]
}
@@ -241,7 +244,6 @@ Rectangle {
MouseArea {
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
property real scrollAccumulator: 0
@@ -253,14 +255,84 @@ Rectangle {
const direction = deltaY < 0 ? 1 : -1
if (isMouseWheel) {
switchWorkspace(direction)
if (!SettingsData.workspaceScrolling || !CompositorService.isNiri) {
switchWorkspace(direction)
}
else {
const windows = root.sortedToplevels;
if (windows.length < 2) {
return;
}
let currentIndex = -1;
for (let i = 0; i < windows.length; i++) {
if (windows[i].activated) {
currentIndex = i;
break;
}
}
let nextIndex;
if (deltaY < 0) {
if (currentIndex === -1) {
nextIndex = 0;
} else {
nextIndex = currentIndex +1;
}
} else {
if (currentIndex === -1) {
nextIndex = windows.length -1;
} else {
nextIndex = currentIndex - 1
}
}
const nextWindow = windows[nextIndex];
if (nextWindow) {
nextWindow.activate();
}
}
} else {
scrollAccumulator += deltaY
if (Math.abs(scrollAccumulator) >= touchpadThreshold) {
const touchDirection = scrollAccumulator < 0 ? 1 : -1
switchWorkspace(touchDirection)
scrollAccumulator = 0
if (!SettingsData.workspaceScrolling || !CompositorService.isNiri) {
switchWorkspace(touchDirection)
}
else {
const windows = root.sortedToplevels;
if (windows.length < 2) {
return;
}
let currentIndex = -1;
for (let i = 0; i < windows.length; i++) {
if (windows[i].activated) {
currentIndex = i;
break;
}
}
let nextIndex;
if (deltaY < 0) {
if (currentIndex === -1) {
nextIndex = 0;
} else {
nextIndex = currentIndex +1;
}
} else {
if (currentIndex === -1) {
nextIndex = windows.length -1;
} else {
nextIndex = currentIndex - 1
}
}
const nextWindow = windows[nextIndex];
if (nextWindow) {
nextWindow.activate();
}
}
scrollAccumulator = 0
}
}
@@ -377,7 +449,7 @@ Rectangle {
return SettingsData.showWorkspaceApps ? widgetHeight * 0.7 : widgetHeight * 0.5;
}
}
radius: Math.min(width, height) / 2
radius: Theme.cornerRadius
color: isActive ? Theme.primary : isUrgent ? Theme.error : isPlaceholder ? Theme.surfaceTextLight : isHovered ? Theme.outlineButton : Theme.surfaceTextAlpha
border.width: isUrgent && !isActive ? 2 : 0
@@ -416,9 +488,7 @@ Rectangle {
MouseArea {
id: mouseArea
anchors.centerIn: parent
width: root.isVertical ? parent.width + Theme.spacingXL : parent.width
height: root.isVertical ? parent.height : parent.height + Theme.spacingXL
anchors.fill: parent
hoverEnabled: !isPlaceholder
cursorShape: isPlaceholder ? Qt.ArrowCursor : Qt.PointingHandCursor
enabled: !isPlaceholder

View File

@@ -130,7 +130,14 @@ Rectangle {
anchors.rightMargin: Theme.spacingS
height: 40
anchors.verticalCenter: parent.verticalCenter
text: hasEvents ? (Qt.formatDate(selectedDate, "MMM d") + " • " + (selectedDateEvents.length === 1 ? "1 event" : selectedDateEvents.length + " events")) : Qt.formatDate(selectedDate, "MMM d")
text: {
const dateStr = Qt.formatDate(selectedDate, "MMM d")
if (selectedDateEvents && selectedDateEvents.length > 0) {
const eventCount = selectedDateEvents.length === 1 ? I18n.tr("1 event") : selectedDateEvents.length + " " + I18n.tr("events")
return dateStr + " • " + eventCount
}
return dateStr
}
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
@@ -392,7 +399,7 @@ Rectangle {
width: parent.width
text: {
if (!modelData || modelData.allDay) {
return "All day"
return I18n.tr("All day")
} else if (modelData.start && modelData.end) {
const timeFormat = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP"
const startTime = Qt.formatTime(modelData.start, timeFormat)

View File

@@ -76,6 +76,31 @@ Card {
horizontalAlignment: Text.AlignHCenter
}
}
Row {
visible: SettingsData.showSeconds
spacing: 0
anchors.horizontalCenter: parent.horizontalCenter
StyledText {
text: String(systemClock?.date?.getSeconds()).padStart(2, '0').charAt(0)
font.pixelSize: 48
color: Theme.primary
font.weight: Font.Medium
width: 28
horizontalAlignment: Text.AlignHCenter
}
StyledText {
text: String(systemClock?.date?.getSeconds()).padStart(2, '0').charAt(1)
font.pixelSize: 48
color: Theme.primary
font.weight: Font.Medium
width: 28
horizontalAlignment: Text.AlignHCenter
}
}
}
StyledText {

View File

@@ -34,8 +34,8 @@ Variants {
property real backgroundTransparency: SettingsData.dockTransparency
property bool groupByApp: SettingsData.dockGroupByApp
readonly property real widgetHeight: Math.max(20, 26 + SettingsData.dankBarInnerPadding * 0.6)
readonly property real effectiveBarHeight: Math.max(widgetHeight + SettingsData.dankBarInnerPadding + 4, Theme.barHeight - 4 - (8 - SettingsData.dankBarInnerPadding))
readonly property real widgetHeight: SettingsData.dockIconSize
readonly property real effectiveBarHeight: widgetHeight + SettingsData.dockSpacing * 2 + 10
readonly property real barSpacing: {
const barIsHorizontal = (SettingsData.dankBarPosition === SettingsData.Position.Top || SettingsData.dankBarPosition === SettingsData.Position.Bottom)
const barIsVertical = (SettingsData.dankBarPosition === SettingsData.Position.Left || SettingsData.dankBarPosition === SettingsData.Position.Right)
@@ -60,14 +60,6 @@ Variants {
property bool contextMenuOpen: (dockVariants.contextMenu && dockVariants.contextMenu.visible && dockVariants.contextMenu.screen === modelData)
property bool windowIsFullscreen: {
if (!ToplevelManager.activeToplevel) {
return false
}
const activeWindow = ToplevelManager.activeToplevel
const fullscreenApps = ["vlc", "mpv", "kodi", "steam", "lutris", "wine", "dosbox"]
return fullscreenApps.some(app => activeWindow.appId && activeWindow.appId.toLowerCase().includes(app))
}
property bool revealSticky: false
Timer {
@@ -81,7 +73,7 @@ Variants {
if (CompositorService.isNiri && NiriService.inOverview && SettingsData.dockOpenOnOverview) {
return true
}
return (!autoHide || dockMouseArea.containsMouse || dockApps.requestDockShow || contextMenuOpen || revealSticky) && !windowIsFullscreen
return !autoHide || dockMouseArea.containsMouse || dockApps.requestDockShow || contextMenuOpen || revealSticky
}
onContextMenuOpenChanged: {
@@ -99,100 +91,147 @@ Variants {
}
screen: modelData
visible: SettingsData.showDock || (CompositorService.isNiri && SettingsData.dockOpenOnOverview && NiriService.inOverview)
visible: {
if (CompositorService.isNiri && NiriService.inOverview) {
return SettingsData.dockOpenOnOverview
}
return SettingsData.showDock
}
color: "transparent"
exclusiveZone: {
if (!SettingsData.showDock || autoHide) return -1
if (barSpacing > 0) return -1
return px(58 + SettingsData.dockSpacing + SettingsData.dockBottomGap)
return px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap)
}
property real animationHeadroom: Math.ceil(SettingsData.dockIconSize * 0.35)
implicitWidth: isVertical ? (px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
implicitHeight: !isVertical ? (px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
Item {
id: maskItem
parent: dock.contentItem
visible: false
x: {
const baseX = dockCore.x + dockMouseArea.x
if (isVertical && SettingsData.dockPosition === SettingsData.Position.Right) {
return baseX - animationHeadroom
}
return baseX
}
y: {
const baseY = dockCore.y + dockMouseArea.y
if (!isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom) {
return baseY - animationHeadroom
}
return baseY
}
width: dockMouseArea.width + (isVertical ? animationHeadroom : 0)
height: dockMouseArea.height + (!isVertical ? animationHeadroom : 0)
}
mask: Region {
item: dockMouseArea
item: maskItem
}
Rectangle {
id: appTooltip
z: 1000
property var hoveredButton: {
if (!dockApps.children[0]) {
return null
}
const layoutItem = dockApps.children[0]
const flowLayout = layoutItem.children[0]
let repeater = null
for (var i = 0; i < flowLayout.children.length; i++) {
const child = flowLayout.children[i]
if (child && typeof child.count !== "undefined" && typeof child.itemAt === "function") {
repeater = child
break
}
}
if (!repeater || !repeater.itemAt) {
return null
}
for (var i = 0; i < repeater.count; i++) {
const item = repeater.itemAt(i)
if (item && item.dockButton && item.dockButton.showTooltip) {
return item.dockButton
}
}
property var hoveredButton: {
if (!dockApps.children[0]) {
return null
}
const layoutItem = dockApps.children[0]
const flowLayout = layoutItem.children[0]
let repeater = null
for (var i = 0; i < flowLayout.children.length; i++) {
const child = flowLayout.children[i]
if (child && typeof child.count !== "undefined" && typeof child.itemAt === "function") {
repeater = child
break
}
}
if (!repeater || !repeater.itemAt) {
return null
}
for (var i = 0; i < repeater.count; i++) {
const item = repeater.itemAt(i)
if (item && item.dockButton && item.dockButton.showTooltip) {
return item.dockButton
}
}
return null
}
property string tooltipText: hoveredButton ? hoveredButton.tooltipText : ""
DankTooltip {
id: dockTooltip
targetScreen: dock.screen
}
visible: hoveredButton !== null && tooltipText !== ""
width: px(tooltipLabel.implicitWidth + 24)
height: px(tooltipLabel.implicitHeight + 12)
Timer {
id: tooltipRevealDelay
interval: 250
repeat: false
onTriggered: dock.showTooltipForHoveredButton()
}
color: Theme.surfaceContainer
radius: Theme.cornerRadius
border.width: 1
border.color: Theme.outlineMedium
x: {
if (!hoveredButton) return 0
const buttonPos = hoveredButton.mapToItem(dock.contentItem, 0, 0)
if (!dock.isVertical) {
return buttonPos.x + hoveredButton.width / 2 - width / 2
} else {
if (SettingsData.dockPosition === SettingsData.Position.Right) {
return buttonPos.x - width - Theme.spacingS
function showTooltipForHoveredButton() {
dockTooltip.hide()
if (dock.hoveredButton && dock.reveal && !slideXAnimation.running && !slideYAnimation.running) {
const buttonGlobalPos = dock.hoveredButton.mapToGlobal(0, 0)
const tooltipText = dock.hoveredButton.tooltipText || ""
if (tooltipText) {
const screenX = dock.screen ? (dock.screen.x || 0) : 0
const screenY = dock.screen ? (dock.screen.y || 0) : 0
const screenHeight = dock.screen ? dock.screen.height : 0
if (!dock.isVertical) {
const isBottom = SettingsData.dockPosition === SettingsData.Position.Bottom
const globalX = buttonGlobalPos.x + dock.hoveredButton.width / 2
const screenRelativeY = isBottom
? (screenHeight - dock.effectiveBarHeight - SettingsData.dockSpacing - SettingsData.dockBottomGap - 35)
: (buttonGlobalPos.y - screenY + dock.hoveredButton.height + Theme.spacingS)
dockTooltip.show(tooltipText,
globalX,
screenRelativeY,
dock.screen,
false, false)
} else {
return buttonPos.x + hoveredButton.width + Theme.spacingS
const isLeft = SettingsData.dockPosition === SettingsData.Position.Left
const tooltipOffset = dock.effectiveBarHeight + SettingsData.dockSpacing + Theme.spacingXS
const tooltipX = isLeft ? tooltipOffset : (dock.screen.width - tooltipOffset)
const screenRelativeY = buttonGlobalPos.y - screenY + dock.hoveredButton.height / 2
dockTooltip.show(tooltipText,
screenX + tooltipX,
screenRelativeY,
dock.screen,
isLeft,
!isLeft)
}
}
}
y: {
if (!hoveredButton) return 0
const buttonPos = hoveredButton.mapToItem(dock.contentItem, 0, 0)
if (!dock.isVertical) {
if (SettingsData.dockPosition === SettingsData.Position.Bottom) {
return buttonPos.y - height - Theme.spacingS
} else {
return buttonPos.y + hoveredButton.height + Theme.spacingS
}
}
Connections {
target: dock
function onRevealChanged() {
if (!dock.reveal) {
tooltipRevealDelay.stop()
dockTooltip.hide()
} else {
return buttonPos.y + hoveredButton.height / 2 - height / 2
tooltipRevealDelay.restart()
}
}
StyledText {
id: tooltipLabel
anchors.centerIn: parent
text: appTooltip.tooltipText
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
function onHoveredButtonChanged() {
dock.showTooltipForHoveredButton()
}
}
Item {
id: dockCore
anchors.fill: parent
x: isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? animationHeadroom : 0
y: !isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom ? animationHeadroom : 0
Connections {
target: dockMouseArea
@@ -218,16 +257,16 @@ Variants {
height: {
if (dock.isVertical) {
return dock.reveal ? Math.min(dockBackground.implicitHeight + 32, maxDockHeight) : Math.min(Math.max(dockBackground.implicitHeight + 64, 200), screenHeight * 0.5)
return dock.reveal ? Math.min(dockBackground.implicitHeight + 4, maxDockHeight) : Math.min(Math.max(dockBackground.implicitHeight + 64, 200), screenHeight * 0.5)
} else {
return dock.reveal ? px(58 + SettingsData.dockSpacing + SettingsData.dockBottomGap) : 1
return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap) : 1
}
}
width: {
if (dock.isVertical) {
return dock.reveal ? px(58 + SettingsData.dockSpacing + SettingsData.dockBottomGap) : 1
return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap) : 1
} else {
return dock.reveal ? Math.min(dockBackground.implicitWidth + 32, maxDockWidth) : Math.min(Math.max(dockBackground.implicitWidth + 64, 200), screenWidth * 0.5)
return dock.reveal ? Math.min(dockBackground.implicitWidth + 4, maxDockWidth) : Math.min(Math.max(dockBackground.implicitWidth + 64, 200), screenWidth * 0.5)
}
}
anchors {
@@ -243,14 +282,14 @@ Variants {
Behavior on height {
NumberAnimation {
duration: 200
duration: Theme.shortDuration
easing.type: Easing.OutCubic
}
}
Behavior on width {
NumberAnimation {
duration: 200
duration: Theme.shortDuration
easing.type: Easing.OutCubic
}
}
@@ -258,13 +297,14 @@ Variants {
Item {
id: dockContainer
anchors.fill: parent
clip: false
transform: Translate {
id: dockSlide
x: {
if (!dock.isVertical) return 0
if (dock.reveal) return 0
const hideDistance = 58 + SettingsData.dockSpacing + SettingsData.dockBottomGap + 10
const hideDistance = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + 10
if (SettingsData.dockPosition === SettingsData.Position.Right) {
return hideDistance
} else {
@@ -274,7 +314,7 @@ Variants {
y: {
if (dock.isVertical) return 0
if (dock.reveal) return 0
const hideDistance = 58 + SettingsData.dockSpacing + SettingsData.dockBottomGap + 10
const hideDistance = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + 10
if (SettingsData.dockPosition === SettingsData.Position.Bottom) {
return hideDistance
} else {
@@ -284,14 +324,16 @@ Variants {
Behavior on x {
NumberAnimation {
duration: 200
id: slideXAnimation
duration: Theme.shortDuration
easing.type: Easing.OutCubic
}
}
Behavior on y {
NumberAnimation {
duration: 200
id: slideYAnimation
duration: Theme.shortDuration
easing.type: Easing.OutCubic
}
}
@@ -301,17 +343,17 @@ Variants {
id: dockBackground
objectName: "dockBackground"
anchors {
top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? undefined : parent.top) : undefined
top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top ? parent.top : undefined) : undefined
bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? parent.bottom : undefined) : undefined
horizontalCenter: !dock.isVertical ? parent.horizontalCenter : undefined
left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? undefined : parent.left) : undefined
left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Left ? parent.left : undefined) : undefined
right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined) : undefined
verticalCenter: dock.isVertical ? parent.verticalCenter : undefined
}
anchors.topMargin: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? 0 : barSpacing + 4) : 0
anchors.bottomMargin: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? barSpacing + 1 : 0) : 0
anchors.leftMargin: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? 0 : barSpacing + 4) : 0
anchors.rightMargin: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? barSpacing + 1 : 0) : 0
anchors.topMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Top ? barSpacing + 1 : 0
anchors.bottomMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom ? barSpacing + 1 : 0
anchors.leftMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? barSpacing + 1 : 0
anchors.rightMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? barSpacing + 1 : 0
implicitWidth: dock.isVertical ? (dockApps.implicitHeight + SettingsData.dockSpacing * 2) : (dockApps.implicitWidth + SettingsData.dockSpacing * 2)
implicitHeight: dock.isVertical ? (dockApps.implicitWidth + SettingsData.dockSpacing * 2) : (dockApps.implicitHeight + SettingsData.dockSpacing * 2)
@@ -322,32 +364,34 @@ Variants {
radius: Theme.cornerRadius
border.width: 1
border.color: Theme.outlineMedium
layer.enabled: true
clip: false
Rectangle {
anchors.fill: parent
color: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g, Theme.surfaceTint.b, 0.04)
radius: parent.radius
}
}
DockApps {
id: dockApps
DockApps {
id: dockApps
anchors.top: !dock.isVertical ? parent.top : undefined
anchors.bottom: !dock.isVertical ? parent.bottom : undefined
anchors.horizontalCenter: !dock.isVertical ? parent.horizontalCenter : undefined
anchors.left: dock.isVertical ? parent.left : undefined
anchors.right: dock.isVertical ? parent.right : undefined
anchors.verticalCenter: dock.isVertical ? parent.verticalCenter : undefined
anchors.topMargin: !dock.isVertical ? SettingsData.dockSpacing : 0
anchors.bottomMargin: !dock.isVertical ? SettingsData.dockSpacing : 0
anchors.leftMargin: dock.isVertical ? SettingsData.dockSpacing : 0
anchors.rightMargin: dock.isVertical ? SettingsData.dockSpacing : 0
anchors.top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top ? dockBackground.top : undefined) : undefined
anchors.bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? dockBackground.bottom : undefined) : undefined
anchors.horizontalCenter: !dock.isVertical ? dockBackground.horizontalCenter : undefined
anchors.left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Left ? dockBackground.left : undefined) : undefined
anchors.right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? dockBackground.right : undefined) : undefined
anchors.verticalCenter: dock.isVertical ? dockBackground.verticalCenter : undefined
anchors.topMargin: !dock.isVertical ? SettingsData.dockSpacing : 0
anchors.bottomMargin: !dock.isVertical ? SettingsData.dockSpacing : 0
anchors.leftMargin: dock.isVertical ? SettingsData.dockSpacing : 0
anchors.rightMargin: dock.isVertical ? SettingsData.dockSpacing : 0
contextMenu: dockVariants.contextMenu
groupByApp: dock.groupByApp
isVertical: dock.isVertical
}
contextMenu: dockVariants.contextMenu
groupByApp: dock.groupByApp
isVertical: dock.isVertical
dockScreen: dock.screen
iconSize: dock.widgetHeight
}
}
}

View File

@@ -15,6 +15,7 @@ Item {
property var contextMenu: null
property var dockApps: null
property int index: -1
property var parentDockScreen: null
property bool longPressing: false
property bool dragging: false
property point dragStartPos: Qt.point(0, 0)
@@ -26,6 +27,7 @@ Item {
property bool isHovered: mouseArea.containsMouse && !dragging
property bool showTooltip: mouseArea.containsMouse && !dragging
property var cachedDesktopEntry: null
property real actualIconSize: 40
function updateDesktopEntry() {
if (!appData || appData.appId === "__SEPARATOR__") {
@@ -88,9 +90,6 @@ Item {
return cachedDesktopEntry && cachedDesktopEntry.name ? cachedDesktopEntry.name : appData.appId
}
width: 40
height: 40
function getToplevelObject() {
if (!appData) {
return null
@@ -144,34 +143,47 @@ Item {
return toplevels
}
onIsHoveredChanged: {
if (mouseArea.pressed) return
if (isHovered) {
exitAnimation.stop()
if (!bounceAnimation.running)
if (!bounceAnimation.running) {
bounceAnimation.restart()
}
} else {
bounceAnimation.stop()
exitAnimation.restart()
}
}
readonly property bool animateX: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right
readonly property real animationDistance: actualIconSize
readonly property real animationDirection: {
if (SettingsData.dockPosition === SettingsData.Position.Bottom) return -1
if (SettingsData.dockPosition === SettingsData.Position.Top) return 1
if (SettingsData.dockPosition === SettingsData.Position.Right) return -1
if (SettingsData.dockPosition === SettingsData.Position.Left) return 1
return -1
}
SequentialAnimation {
id: bounceAnimation
running: false
NumberAnimation {
target: translateY
property: "y"
to: -10
target: iconTransform
property: animateX ? "x" : "y"
to: animationDirection * animationDistance * 0.25
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedAccel
}
NumberAnimation {
target: translateY
property: "y"
to: -8
target: iconTransform
property: animateX ? "x" : "y"
to: animationDirection * animationDistance * 0.2
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedDecel
@@ -182,24 +194,14 @@ Item {
id: exitAnimation
running: false
target: translateY
property: "y"
target: iconTransform
property: animateX ? "x" : "y"
to: 0
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedDecel
}
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
border.width: 2
border.color: Theme.primary
visible: dragging
z: -1
}
Timer {
id: longPressTimer
@@ -216,7 +218,6 @@ Item {
id: mouseArea
anchors.fill: parent
anchors.bottomMargin: -20
hoverEnabled: true
cursorShape: longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
@@ -252,7 +253,7 @@ Item {
if (dragging) {
dragOffset = Qt.point(mouse.x - dragStartPos.x, mouse.y - dragStartPos.y)
if (dockApps) {
const threshold = 40
const threshold = actualIconSize
let newTargetIndex = targetIndex
if (dragOffset.x > threshold && targetIndex < dockApps.pinnedAppCount - 1) {
newTargetIndex = targetIndex + 1
@@ -317,7 +318,7 @@ Item {
}
} else {
if (contextMenu) {
contextMenu.showForButton(root, appData, 65, true, cachedDesktopEntry)
contextMenu.showForButton(root, appData, root.height + 25, true, cachedDesktopEntry, parentDockScreen)
}
}
}
@@ -334,7 +335,7 @@ Item {
}
} else if (appData && appData.type === "grouped") {
if (contextMenu) {
contextMenu.showForButton(root, appData, 40, false, cachedDesktopEntry)
contextMenu.showForButton(root, appData, root.height, false, cachedDesktopEntry, parentDockScreen)
}
} else if (appData && appData.appId) {
const desktopEntry = cachedDesktopEntry
@@ -351,7 +352,7 @@ Item {
}
} else if (mouse.button === Qt.RightButton) {
if (contextMenu && appData) {
contextMenu.showForButton(root, appData, 40, false, cachedDesktopEntry)
contextMenu.showForButton(root, appData, root.height, false, cachedDesktopEntry, parentDockScreen)
} else {
console.warn("No context menu or appData available")
}
@@ -359,123 +360,205 @@ Item {
}
}
IconImage {
id: iconImg
Item {
id: visualContent
anchors.fill: parent
anchors.centerIn: parent
implicitSize: 40
source: {
if (appData.appId === "__SEPARATOR__") {
return ""
}
const moddedId = Paths.moddedAppId(appData.appId)
if (moddedId.toLowerCase().includes("steam_app")) {
return ""
}
return cachedDesktopEntry && cachedDesktopEntry.icon ? Quickshell.iconPath(cachedDesktopEntry.icon, true) : ""
transform: Translate {
id: iconTransform
x: 0
y: 0
}
mipmap: true
smooth: true
asynchronous: true
visible: status === Image.Ready
}
DankIcon {
anchors.centerIn: parent
size: 40
name: "sports_esports"
color: Theme.surfaceText
visible: {
if (!appData || !appData.appId || appData.appId === "__SEPARATOR__") {
return false
}
const moddedId = Paths.moddedAppId(appData.appId)
return moddedId.toLowerCase().includes("steam_app")
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
border.width: 2
border.color: Theme.primary
visible: dragging
z: -1
}
}
Rectangle {
width: 40
height: 40
anchors.centerIn: parent
visible: iconImg.status !== Image.Ready
color: Theme.surfaceLight
radius: Theme.cornerRadius
border.width: 1
border.color: Theme.primarySelected
IconImage {
id: iconImg
Text {
anchors.centerIn: parent
text: {
if (!appData || !appData.appId) {
return "?"
implicitSize: actualIconSize
source: {
if (appData.appId === "__SEPARATOR__") {
return ""
}
const desktopEntry = cachedDesktopEntry
if (desktopEntry && desktopEntry.name) {
return desktopEntry.name.charAt(0).toUpperCase()
const moddedId = Paths.moddedAppId(appData.appId)
if (moddedId.toLowerCase().includes("steam_app")) {
return ""
}
return appData.appId.charAt(0).toUpperCase()
return cachedDesktopEntry && cachedDesktopEntry.icon ? Quickshell.iconPath(cachedDesktopEntry.icon, true) : ""
}
font.pixelSize: 14
color: Theme.primary
font.weight: Font.Bold
mipmap: true
smooth: true
asynchronous: true
visible: status === Image.Ready
}
}
// Indicator for running/focused state
Row {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: -2
spacing: 2
visible: appData && (appData.isRunning || appData.type === "window" || (appData.type === "grouped" && appData.windowCount > 0))
Repeater {
model: {
if (!appData) return 0
if (appData.type === "grouped") {
return Math.min(appData.windowCount, 4)
} else if (appData.type === "window" || appData.isRunning) {
return 1
DankIcon {
anchors.centerIn: parent
size: actualIconSize
name: "sports_esports"
color: Theme.surfaceText
visible: {
if (!appData || !appData.appId || appData.appId === "__SEPARATOR__") {
return false
}
return 0
const moddedId = Paths.moddedAppId(appData.appId)
return moddedId.toLowerCase().includes("steam_app")
}
}
Rectangle {
width: appData && appData.type === "grouped" && appData.windowCount > 1 ? 4 : 8
height: 2
radius: 1
color: {
if (!appData) {
return "transparent"
Rectangle {
width: actualIconSize
height: actualIconSize
anchors.centerIn: parent
visible: iconImg.status !== Image.Ready
color: Theme.surfaceLight
radius: Theme.cornerRadius
border.width: 1
border.color: Theme.primarySelected
Text {
anchors.centerIn: parent
text: {
if (!appData || !appData.appId) {
return "?"
}
if (appData.type !== "grouped" || appData.windowCount === 1) {
if (isWindowFocused) {
return Theme.primary
const desktopEntry = cachedDesktopEntry
if (desktopEntry && desktopEntry.name) {
return desktopEntry.name.charAt(0).toUpperCase()
}
return appData.appId.charAt(0).toUpperCase()
}
font.pixelSize: Math.max(8, parent.width * 0.35)
color: Theme.primary
font.weight: Font.Bold
}
}
Loader {
anchors.horizontalCenter: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right ? undefined : parent.horizontalCenter
anchors.verticalCenter: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right ? parent.verticalCenter : undefined
anchors.bottom: SettingsData.dockPosition === SettingsData.Position.Bottom ? parent.bottom : undefined
anchors.top: SettingsData.dockPosition === SettingsData.Position.Top ? parent.top : undefined
anchors.left: SettingsData.dockPosition === SettingsData.Position.Left ? parent.left : undefined
anchors.right: SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined
anchors.bottomMargin: SettingsData.dockPosition === SettingsData.Position.Bottom ? -2 : 0
anchors.topMargin: SettingsData.dockPosition === SettingsData.Position.Top ? -2 : 0
anchors.leftMargin: SettingsData.dockPosition === SettingsData.Position.Left ? -2 : 0
anchors.rightMargin: SettingsData.dockPosition === SettingsData.Position.Right ? -2 : 0
sourceComponent: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right ? columnIndicator : rowIndicator
visible: {
if (!appData) return false
if (appData.type === "window") return true
if (appData.type === "grouped") return appData.windowCount > 0
return appData.isRunning
}
}
}
Component {
id: rowIndicator
Row {
spacing: 2
Repeater {
model: {
if (!appData) return 0
if (appData.type === "grouped") {
return Math.min(appData.windowCount, 4)
} else if (appData.type === "window" || appData.isRunning) {
return 1
}
return 0
}
Rectangle {
width: appData && appData.type === "grouped" && appData.windowCount > 1 ? Math.max(3, actualIconSize * 0.1) : Math.max(6, actualIconSize * 0.2)
height: Math.max(2, actualIconSize * 0.05)
radius: Theme.cornerRadius
color: {
if (!appData) {
return "transparent"
}
if (appData.type !== "grouped" || appData.windowCount === 1) {
if (isWindowFocused) {
return Theme.primary
}
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
}
if (appData.type === "grouped" && appData.windowCount > 1) {
const groupToplevels = getGroupedToplevels()
if (index < groupToplevels.length && groupToplevels[index].activated) {
return Theme.primary
}
}
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
}
if (appData.type === "grouped" && appData.windowCount > 1) {
const groupToplevels = getGroupedToplevels()
if (index < groupToplevels.length && groupToplevels[index].activated) {
return Theme.primary
}
}
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
}
}
}
}
Component {
id: columnIndicator
transform: Translate {
id: translateY
Column {
spacing: 2
y: 0
Repeater {
model: {
if (!appData) return 0
if (appData.type === "grouped") {
return Math.min(appData.windowCount, 4)
} else if (appData.type === "window" || appData.isRunning) {
return 1
}
return 0
}
Rectangle {
width: Math.max(2, actualIconSize * 0.05)
height: appData && appData.type === "grouped" && appData.windowCount > 1 ? Math.max(3, actualIconSize * 0.1) : Math.max(6, actualIconSize * 0.2)
radius: Theme.cornerRadius
color: {
if (!appData) {
return "transparent"
}
if (appData.type !== "grouped" || appData.windowCount === 1) {
if (isWindowFocused) {
return Theme.primary
}
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
}
if (appData.type === "grouped" && appData.windowCount > 1) {
const groupToplevels = getGroupedToplevels()
if (index < groupToplevels.length && groupToplevels[index].activated) {
return Theme.primary
}
}
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
}
}
}
}
}
}

View File

@@ -14,7 +14,10 @@ Item {
property int pinnedAppCount: 0
property bool groupByApp: false
property bool isVertical: false
property var dockScreen: null
property real iconSize: 40
clip: false
implicitWidth: isVertical ? appLayout.height : appLayout.width
implicitHeight: isVertical ? appLayout.width : appLayout.height
@@ -36,14 +39,18 @@ Item {
Item {
id: appLayout
anchors.centerIn: parent
width: layoutFlow.width
height: layoutFlow.height
anchors.horizontalCenter: root.isVertical ? undefined : parent.horizontalCenter
anchors.verticalCenter: root.isVertical ? parent.verticalCenter : undefined
anchors.left: root.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? parent.left : undefined
anchors.right: root.isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined
anchors.top: root.isVertical ? undefined : parent.top
Flow {
id: layoutFlow
flow: root.isVertical ? Flow.TopToBottom : Flow.LeftToRight
spacing: 8
spacing: Math.min(8, Math.max(4, root.iconSize * 0.08))
Repeater {
id: repeater
@@ -192,33 +199,42 @@ Item {
delegate: Item {
id: delegateItem
property alias dockButton: button
clip: false
width: model.type === "separator" ? 16 : 40
height: 40
width: model.type === "separator" ? (root.isVertical ? root.iconSize : 8) : (root.isVertical ? root.iconSize : root.iconSize * 1.2)
height: model.type === "separator" ? (root.isVertical ? 8 : root.iconSize) : (root.isVertical ? root.iconSize * 1.2 : root.iconSize)
Rectangle {
visible: model.type === "separator"
width: 2
height: 20
width: root.isVertical ? root.iconSize * 0.5 : 2
height: root.isVertical ? 2 : root.iconSize * 0.5
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
radius: 1
anchors.centerIn: parent
}
MouseArea {
visible: model.type === "separator"
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
}
DockAppButton {
id: button
visible: model.type !== "separator"
anchors.centerIn: parent
width: 40
height: 40
width: delegateItem.width
height: delegateItem.height
actualIconSize: root.iconSize
appData: model
contextMenu: root.contextMenu
dockApps: root
index: model.index
parentDockScreen: root.dockScreen
// Override tooltip for windows to show window title
showWindowTitle: model.type === "window" || model.type === "grouped"
windowTitle: model.windowTitle || ""
}

View File

@@ -9,7 +9,6 @@ import qs.Widgets
PanelWindow {
id: root
property bool showContextMenu: false
property var appData: null
property var anchorItem: null
property real dockVisibleHeight: 40
@@ -17,33 +16,25 @@ PanelWindow {
property bool hidePin: false
property var desktopEntry: null
function showForButton(button, data, dockHeight, hidePinOption, entry) {
function showForButton(button, data, dockHeight, hidePinOption, entry, dockScreen) {
if (dockScreen) {
root.screen = dockScreen
}
anchorItem = button
appData = data
dockVisibleHeight = dockHeight || 40
hidePin = hidePinOption || false
desktopEntry = entry || null
const dockWindow = button.Window.window
if (dockWindow) {
for (var i = 0; i < Quickshell.screens.length; i++) {
const s = Quickshell.screens[i]
if (dockWindow.x >= s.x && dockWindow.x < s.x + s.width) {
root.screen = s
break
}
}
}
showContextMenu = true
visible = true
}
function close() {
showContextMenu = false
visible = false
}
screen: Quickshell.screens[0]
visible: showContextMenu
screen: null
visible: false
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
@@ -175,7 +166,7 @@ PanelWindow {
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
opacity: root.showContextMenu ? 1 : 0
opacity: root.visible ? 1 : 0
visible: opacity > 0
Behavior on opacity {

View File

@@ -34,30 +34,10 @@ Item {
signal launchRequested
property bool powerDialogVisible: false
property string powerDialogTitle: ""
property string powerDialogMessage: ""
property string powerDialogConfirmText: ""
property color powerDialogConfirmColor: Theme.primary
property var powerDialogOnConfirm: function () {}
function pickRandomFact() {
randomFact = Facts.getRandomFact()
}
function showPowerDialog(title, message, confirmText, confirmColor, onConfirm) {
powerDialogTitle = title
powerDialogMessage = message
powerDialogConfirmText = confirmText
powerDialogConfirmColor = confirmColor
powerDialogOnConfirm = onConfirm
powerDialogVisible = true
}
function hidePowerDialog() {
powerDialogVisible = false
}
Component.onCompleted: {
pickRandomFact()
WeatherService.addRef()
@@ -186,9 +166,9 @@ Item {
source: {
var currentWallpaper = SessionData.getMonitorWallpaper(screenName)
if (screenName && currentWallpaper && currentWallpaper.startsWith("we:")) {
const cacheHome = StandardPaths.writableLocation(StandardPaths.CacheLocation).toString()
const cacheHome = StandardPaths.writableLocation(StandardPaths.GenericCacheLocation).toString()
const baseDir = Paths.strip(cacheHome)
const screenshotPath = baseDir + "/dankshell/we_screenshots" + "/" + currentWallpaper.substring(3) + ".jpg"
const screenshotPath = baseDir + "/DankMaterialShell/we_screenshots" + "/" + currentWallpaper.substring(3) + ".jpg"
return screenshotPath
}
return (currentWallpaper && !currentWallpaper.startsWith("#")) ? currentWallpaper : ""
@@ -373,11 +353,11 @@ Item {
syncingFromState = true
text = GreeterState.showPasswordInput ? GreeterState.passwordBuffer : GreeterState.usernameInput
syncingFromState = false
if (isPrimaryScreen)
if (isPrimaryScreen && !powerMenu.isVisible)
forceActiveFocus()
}
onVisibleChanged: {
if (visible && isPrimaryScreen)
if (visible && isPrimaryScreen && !powerMenu.isVisible)
forceActiveFocus()
}
}
@@ -1073,33 +1053,15 @@ Item {
visible: root.randomFact !== ""
}
Row {
DankActionButton {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.margins: Theme.spacingXL
spacing: Theme.spacingL
visible: GreetdSettings.lockScreenShowPowerActions
DankActionButton {
iconName: "power_settings_new"
iconColor: Theme.error
buttonSize: 40
onClicked: {
showPowerDialog("Power Off", "Power off this computer?", "Power Off", Theme.error, function () {
SessionService.poweroff()
})
}
}
DankActionButton {
iconName: "refresh"
buttonSize: 40
onClicked: {
showPowerDialog("Restart", "Restart this computer?", "Restart", Theme.primary, function () {
SessionService.reboot()
})
}
}
iconName: "power_settings_new"
iconColor: Theme.error
buttonSize: 40
onClicked: powerMenu.show()
}
Item {
@@ -1314,90 +1276,12 @@ Item {
onTriggered: GreeterState.pamState = ""
}
Rectangle {
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.8)
visible: powerDialogVisible
z: 1000
Rectangle {
anchors.centerIn: parent
width: 320
height: 180
radius: Theme.cornerRadius
color: Theme.surfaceContainer
border.color: Theme.outline
border.width: 1
Column {
anchors.centerIn: parent
spacing: Theme.spacingXL
DankIcon {
anchors.horizontalCenter: parent.horizontalCenter
name: "power_settings_new"
size: 32
color: powerDialogConfirmColor
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: powerDialogMessage
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
Rectangle {
width: 100
height: 40
radius: Theme.cornerRadius
color: Theme.surfaceVariant
StyledText {
anchors.centerIn: parent
text: I18n.tr("Cancel")
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: hidePowerDialog()
}
}
Rectangle {
width: 100
height: 40
radius: Theme.cornerRadius
color: powerDialogConfirmColor
StyledText {
anchors.centerIn: parent
text: powerDialogConfirmText
color: Theme.primaryText
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
hidePowerDialog()
powerDialogOnConfirm()
}
}
}
}
LockPowerMenu {
id: powerMenu
showLogout: false
onClosed: {
if (isPrimaryScreen && inputField && inputField.forceActiveFocus) {
Qt.callLater(() => inputField.forceActiveFocus())
}
}
}

View File

@@ -12,6 +12,110 @@ A greeter for [greetd](https://github.com/kennylevinsen/greetd) that follows the
## Installation
### Arch Linux
Arch linux users can install [greetd-dms-greeter-git](https://aur.archlinux.org/packages/greetd-dms-greeter-git) from the AUR.
```bash
paru -S greetd-dms-greeter-git
# Or with yay
yay -S greetd-dms-greeter-git
```
Once installed, disable any existing display manager and enable greetd:
```bash
sudo systemctl disable gdm sddm lightdm
sudo systemctl enable greetd
```
#### Syncing themes (Optional)
To sync your wallpaper and theme with the greeter login screen:
```bash
dms-greeter-sync
```
Then logout/login for changes to take effect. Your wallpaper and theme will appear on the greeter!
<details>
<summary>What does dms-greeter-sync do?</summary>
The `dms-greeter-sync` helper automatically:
- Adds you to the greeter group
- Sets minimal ACL permissions on parent directories (traverse only)
- Sets group ownership on your DMS config directories
- Creates symlinks to share your theme files with the greeter
This uses standard Linux ACLs (Access Control Lists) - the same security model used by GNOME, KDE, and systemd. The greeter user only gets traverse permission through your directories and can only read the specific theme files you share.
</details>
<details>
<summary>Manual theme syncing (advanced)</summary>
If you prefer to set up theme syncing manually:
```bash
# Add yourself to greeter group
sudo usermod -aG greeter <username>
# Set ACLs to allow greeter to traverse your directories
setfacl -m u:greeter:x ~ ~/.config ~/.local ~/.cache ~/.local/state
# Set group ownership on config directories
sudo chgrp -R greeter ~/.config/DankMaterialShell
sudo chgrp -R greeter ~/.local/state/DankMaterialShell
sudo chgrp -R greeter ~/.cache/quickshell
sudo chmod -R g+rX ~/.config/DankMaterialShell ~/.local/state/DankMaterialShell ~/.cache/quickshell
# Create symlinks
sudo ln -sf ~/.config/DankMaterialShell/settings.json /var/cache/dms-greeter/settings.json
sudo ln -sf ~/.local/state/DankMaterialShell/session.json /var/cache/dms-greeter/session.json
sudo ln -sf ~/.cache/quickshell/dankshell/dms-colors.json /var/cache/dms-greeter/colors.json
# Logout and login for group membership to take effect
```
</details>
### Fedora / RHEL / Rocky / Alma
Install from COPR or build the RPM:
```bash
# From COPR (when available)
sudo dnf copr enable avenge/dms
sudo dnf install dms-greeter
# Or build locally
cd /path/to/DankMaterialShell
rpkg local
sudo rpm -ivh x86_64/dms-greeter-*.rpm
```
The package automatically:
- Creates the greeter user
- Sets up directories and permissions
- Configures greetd with auto-detected compositor
- Applies SELinux contexts
Then disable existing display manager and enable greetd:
```bash
sudo systemctl disable gdm sddm lightdm
sudo systemctl enable greetd
```
**Optional:** Sync your theme with the greeter:
```bash
dms-greeter-sync
```
Then logout/login to see your wallpaper on the greeter!
### Automatic
The easiest thing is to run `dms greeter install` or `dms` for interactive installation.
@@ -19,21 +123,33 @@ The easiest thing is to run `dms greeter install` or `dms` for interactive insta
### Manual
1. Install `greetd` (in most distro's standard repositories) and `quickshell`
2. Clone the dms project to `/etc/xdg/quickshell/dms-greeter`
2. Create the greeter user (if not already created by greetd):
```bash
sudo groupadd -r greeter
sudo useradd -r -g greeter -d /var/lib/greeter -s /bin/bash -c "System Greeter" greeter
sudo mkdir -p /var/lib/greeter
sudo chown greeter:greeter /var/lib/greeter
```
3. Clone the dms project to `/etc/xdg/quickshell/dms-greeter`:
```bash
sudo git clone https://github.com/AvengeMedia/DankMaterialShell.git /etc/xdg/quickshell/dms-greeter
```
3. Copy `assets/dms-greeter` to `/usr/local/bin/dms-greeter`:
4. Copy `Modules/Greetd/assets/dms-greeter` to `/usr/local/bin/dms-greeter`:
```bash
sudo cp assets/dms-greeter /usr/local/bin/dms-greeter
sudo cp /etc/xdg/quickshell/dms-greeter/Modules/Greetd/assets/dms-greeter /usr/local/bin/dms-greeter
sudo chmod +x /usr/local/bin/dms-greeter
```
4. Create greeter cache directory with proper permissions:
5. Create greeter cache directory with proper permissions:
```bash
sudo mkdir -p /var/cache/dms-greeter
sudo chown greeter:greeter /var/cache/dms-greeter
sudo chmod 770 /var/cache/dms-greeter
sudo chmod 750 /var/cache/dms-greeter
```
6. Edit or create `/etc/greetd/config.toml`:
```toml
[terminal]
@@ -45,7 +161,18 @@ user = "greeter"
command = "/usr/local/bin/dms-greeter --command niri"
```
Enable the greeter with `sudo systemctl enable greetd`
7. Disable existing display manager and enable greetd:
```bash
sudo systemctl disable gdm sddm lightdm
sudo systemctl enable greetd
```
8. (Optional) Install the `dms-greeter-sync` helper for easy theme syncing:
```bash
# Download or copy the dms-greeter-sync script from the spec file
sudo cp /path/to/dms-greeter-sync /usr/local/bin/dms-greeter-sync
sudo chmod +x /usr/local/bin/dms-greeter-sync
```
#### Legacy installation (deprecated)
@@ -114,21 +241,31 @@ Simply edit `/etc/greetd/dms-niri.kdl` or `/etc/greetd/dms-hypr.conf` to change
#### Personalization
Wallpapers and themes and weather and clock formats and things are a TODO on the documentation, but it's configured exactly the same as dms.
The greeter can be personalized with wallpapers, themes, weather, clock formats, and more - configured exactly the same as dms.
You can synchronize those configurations with a specific user if you want greeter settings to always mirror the shell.
**Easiest method:** Run `dms-greeter-sync` to automatically sync your DMS theme with the greeter.
The greeter uses the `dms-greeter` group for file access permissions, so ensure your user and the greeter user are both members of this group.
**Manual method:** You can manually synchronize configurations if you want greeter settings to always mirror your shell:
```bash
# For core settings (theme, clock formats, etc)
# Add yourself to the greeter group
sudo usermod -aG greeter $USER
# Set ACLs to allow greeter user to traverse your home directory
setfacl -m u:greeter:x ~ ~/.config ~/.local ~/.cache ~/.local/state
# Set group permissions on DMS directories
sudo chgrp -R greeter ~/.config/DankMaterialShell ~/.local/state/DankMaterialShell ~/.cache/quickshell
sudo chmod -R g+rX ~/.config/DankMaterialShell ~/.local/state/DankMaterialShell ~/.cache/quickshell
# Create symlinks for theme files
sudo ln -sf ~/.config/DankMaterialShell/settings.json /var/cache/dms-greeter/settings.json
# For state (mainly you would configure wallpaper in this file)
sudo ln -sf ~/.local/state/DankMaterialShell/session.json /var/cache/dms-greeter/session.json
# For wallpaper based theming
sudo ln -sf ~/.cache/quickshell/dankshell/dms-colors.json /var/cache/dms-greeter/dms-colors.json
sudo ln -sf ~/.cache/quickshell/dankshell/dms-colors.json /var/cache/dms-greeter/colors.json
# Logout and login for group membership to take effect
```
You can override the configuration path with the `DMS_GREET_CFG_DIR` environment variable or the `--cache-dir` flag when using `dms-greeter`. The default is `/var/cache/dms-greeter`.
**Advanced:** You can override the configuration path with the `DMS_GREET_CFG_DIR` environment variable or the `--cache-dir` flag when using `dms-greeter`. The default is `/var/cache/dms-greeter`.
The cache directory should be owned by `greeter:greeter` with `770` permissions.

View File

@@ -1,5 +1,6 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets

View File

@@ -1,3 +1,5 @@
pragma ComponentBehavior: Bound
import QtQuick
Item {

View File

@@ -1,3 +1,5 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
@@ -5,29 +7,64 @@ import Quickshell.Wayland
import qs.Common
import qs.Services
Item {
Scope {
id: root
property string sharedPasswordBuffer: ""
property bool shouldLock: false
property bool processingExternalEvent: false
Component.onCompleted: {
IdleService.lockComponent = root
IdleService.lockComponent = this
}
function lock() {
if (SettingsData.customPowerActionLock && SettingsData.customPowerActionLock.length > 0) {
Quickshell.execDetached(SettingsData.customPowerActionLock.split(" "))
return
}
if (!processingExternalEvent && SettingsData.loginctlLockIntegration && DMSService.isConnected) {
DMSService.lockSession(response => {
if (response.error) {
console.warn("Lock: Failed to call loginctl.lock:", response.error)
shouldLock = true
}
})
} else {
shouldLock = true
}
}
function unlock() {
if (!processingExternalEvent && SettingsData.loginctlLockIntegration && DMSService.isConnected) {
DMSService.unlockSession(response => {
if (response.error) {
console.warn("Lock: Failed to call loginctl.unlock:", response.error)
shouldLock = false
}
})
} else {
shouldLock = false
}
}
function activate() {
shouldLock = true
lock()
}
Connections {
target: SessionService
function onSessionLocked() {
processingExternalEvent = true
shouldLock = true
processingExternalEvent = false
}
function onSessionUnlocked() {
processingExternalEvent = true
shouldLock = false
processingExternalEvent = false
}
}
@@ -35,14 +72,14 @@ Item {
target: IdleService
function onLockRequested() {
shouldLock = true
lock()
}
}
WlSessionLock {
id: sessionLock
locked: root.shouldLock
locked: shouldLock
WlSessionLockSurface {
color: "transparent"
@@ -52,7 +89,7 @@ Item {
lock: sessionLock
sharedPasswordBuffer: root.sharedPasswordBuffer
onUnlockRequested: {
root.shouldLock = false
root.unlock()
}
onPasswordChanged: newPassword => {
root.sharedPasswordBuffer = newPassword
@@ -69,7 +106,7 @@ Item {
target: "lock"
function lock() {
shouldLock = true
root.lock()
}
function demo() {

View File

@@ -0,0 +1,459 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
property bool isVisible: false
property bool showLogout: true
property int selectedIndex: 0
property int optionCount: {
let count = 0
if (showLogout) count++
count++
if (SessionService.hibernateSupported) count++
count += 2
return count
}
signal closed()
function show() {
isVisible = true
selectedIndex = 0
Qt.callLater(() => {
if (powerMenuFocusScope && powerMenuFocusScope.forceActiveFocus) {
powerMenuFocusScope.forceActiveFocus()
}
})
}
function hide() {
isVisible = false
closed()
}
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.5)
visible: isVisible
z: 1000
MouseArea {
anchors.fill: parent
onClicked: root.hide()
}
FocusScope {
id: powerMenuFocusScope
anchors.fill: parent
focus: root.isVisible
onVisibleChanged: {
if (visible) {
Qt.callLater(() => forceActiveFocus())
}
}
Keys.onEscapePressed: {
root.hide()
}
Keys.onPressed: event => {
switch (event.key) {
case Qt.Key_Up:
case Qt.Key_Backtab:
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount
event.accepted = true
break
case Qt.Key_Down:
case Qt.Key_Tab:
selectedIndex = (selectedIndex + 1) % optionCount
event.accepted = true
break
case Qt.Key_Return:
case Qt.Key_Enter:
const actions = []
if (showLogout) actions.push("logout")
actions.push("suspend")
if (SessionService.hibernateSupported) actions.push("hibernate")
actions.push("reboot", "poweroff")
if (selectedIndex < actions.length) {
const action = actions[selectedIndex]
hide()
switch (action) {
case "logout":
SessionService.logout()
break
case "suspend":
SessionService.suspend()
break
case "hibernate":
SessionService.hibernate()
break
case "reboot":
SessionService.reboot()
break
case "poweroff":
SessionService.poweroff()
break
}
}
event.accepted = true
break
case Qt.Key_N:
if (event.modifiers & Qt.ControlModifier) {
selectedIndex = (selectedIndex + 1) % optionCount
event.accepted = true
}
break
case Qt.Key_P:
if (event.modifiers & Qt.ControlModifier) {
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount
event.accepted = true
}
break
case Qt.Key_J:
if (event.modifiers & Qt.ControlModifier) {
selectedIndex = (selectedIndex + 1) % optionCount
event.accepted = true
}
break
case Qt.Key_K:
if (event.modifiers & Qt.ControlModifier) {
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount
event.accepted = true
}
break
}
}
Rectangle {
anchors.centerIn: parent
width: 320
implicitHeight: mainColumn.implicitHeight + Theme.spacingL * 2
height: implicitHeight
radius: Theme.cornerRadius
color: Theme.surfaceContainer
border.color: Theme.outlineMedium
border.width: 1
Column {
id: mainColumn
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
StyledText {
text: I18n.tr("Power Options")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - 150
height: 1
}
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: root.hide()
}
}
Column {
width: parent.width
spacing: Theme.spacingS
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
visible: showLogout
color: {
if (selectedIndex === 0) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
} else if (logoutArea.containsMouse) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
} else {
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
}
}
border.color: selectedIndex === 0 ? Theme.primary : "transparent"
border.width: selectedIndex === 0 ? 1 : 0
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "logout"
size: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Log Out")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: logoutArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.hide()
SessionService.logout()
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: {
const suspendIdx = showLogout ? 1 : 0
if (selectedIndex === suspendIdx) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
} else if (suspendArea.containsMouse) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
} else {
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
}
}
border.color: selectedIndex === (showLogout ? 1 : 0) ? Theme.primary : "transparent"
border.width: selectedIndex === (showLogout ? 1 : 0) ? 1 : 0
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "bedtime"
size: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Suspend")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: suspendArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.hide()
SessionService.suspend()
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: {
const hibernateIdx = showLogout ? 2 : 1
if (selectedIndex === hibernateIdx) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
} else if (hibernateArea.containsMouse) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
} else {
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
}
}
border.color: selectedIndex === (showLogout ? 2 : 1) ? Theme.primary : "transparent"
border.width: selectedIndex === (showLogout ? 2 : 1) ? 1 : 0
visible: SessionService.hibernateSupported
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "ac_unit"
size: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Hibernate")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: hibernateArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.hide()
SessionService.hibernate()
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: {
let rebootIdx = showLogout ? 3 : 2
if (!SessionService.hibernateSupported) rebootIdx--
if (selectedIndex === rebootIdx) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
} else if (rebootArea.containsMouse) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
} else {
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
}
}
border.color: {
let rebootIdx = showLogout ? 3 : 2
if (!SessionService.hibernateSupported) rebootIdx--
return selectedIndex === rebootIdx ? Theme.primary : "transparent"
}
border.width: {
let rebootIdx = showLogout ? 3 : 2
if (!SessionService.hibernateSupported) rebootIdx--
return selectedIndex === rebootIdx ? 1 : 0
}
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "restart_alt"
size: Theme.iconSize
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Reboot")
font.pixelSize: Theme.fontSizeMedium
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: rebootArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.hide()
SessionService.reboot()
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: {
let powerOffIdx = showLogout ? 4 : 3
if (!SessionService.hibernateSupported) powerOffIdx--
if (selectedIndex === powerOffIdx) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
} else if (powerOffArea.containsMouse) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
} else {
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
}
}
border.color: {
let powerOffIdx = showLogout ? 4 : 3
if (!SessionService.hibernateSupported) powerOffIdx--
return selectedIndex === powerOffIdx ? Theme.primary : "transparent"
}
border.width: {
let powerOffIdx = showLogout ? 4 : 3
if (!SessionService.hibernateSupported) powerOffIdx--
return selectedIndex === powerOffIdx ? 1 : 0
}
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "power_settings_new"
size: Theme.iconSize
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Power Off")
font.pixelSize: Theme.fontSizeMedium
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: powerOffArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.hide()
SessionService.poweroff()
}
}
}
}
Item {
height: Theme.spacingS
}
}
}
}
}

View File

@@ -1,11 +1,11 @@
pragma ComponentBehavior: Bound
import QtCore
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Services.Pam
import Quickshell.Services.Mpris
import qs.Common
import qs.Services
@@ -26,27 +26,6 @@ Item {
signal unlockRequested
// Internal power dialog state
property bool powerDialogVisible: false
property string powerDialogTitle: ""
property string powerDialogMessage: ""
property string powerDialogConfirmText: ""
property color powerDialogConfirmColor: Theme.primary
property var powerDialogOnConfirm: function () {}
function showPowerDialog(title, message, confirmText, confirmColor, onConfirm) {
powerDialogTitle = title
powerDialogMessage = message
powerDialogConfirmText = confirmText
powerDialogConfirmColor = confirmColor
powerDialogOnConfirm = onConfirm
powerDialogVisible = true
}
function hidePowerDialog() {
powerDialogVisible = false
}
function pickRandomFact() {
randomFact = Facts.getRandomFact()
}
@@ -63,6 +42,16 @@ Item {
updateHyprlandLayout()
hyprlandLayoutUpdateTimer.start()
}
if (SessionService.loginctlAvailable && DMSService.apiVersion >= 2) {
DMSService.sendRequest("loginctl.lockerReady", null, response => {
if (response.error) {
console.warn("LockScreenContent: Failed to signal locker ready:", response.error)
} else {
console.log("LockScreenContent: Locker ready signaled, inhibitor released")
}
})
}
}
onDemoModeChanged: {
if (demoMode) {
@@ -143,9 +132,9 @@ Item {
source: {
var currentWallpaper = SessionData.getMonitorWallpaper(screenName)
if (screenName && currentWallpaper && currentWallpaper.startsWith("we:")) {
const cacheHome = StandardPaths.writableLocation(StandardPaths.CacheLocation).toString()
const cacheHome = StandardPaths.writableLocation(StandardPaths.GenericCacheLocation).toString()
const baseDir = Paths.strip(cacheHome)
const screenshotPath = baseDir + "/dankshell/we_screenshots" + "/" + currentWallpaper.substring(3) + ".jpg"
const screenshotPath = baseDir + "/DankMaterialShell/we_screenshots" + "/" + currentWallpaper.substring(3) + ".jpg"
return screenshotPath
}
return (currentWallpaper && !currentWallpaper.startsWith("#")) ? currentWallpaper : ""
@@ -201,8 +190,7 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
text: {
const format = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP"
return systemClock.date.toLocaleTimeString(Qt.locale(), format)
return systemClock.date.toLocaleTimeString(Qt.locale(), SettingsData.getEffectiveTimeFormat())
}
font.pixelSize: 120
font.weight: Font.Light
@@ -263,22 +251,50 @@ Item {
border.color: passwordField.activeFocus ? Theme.primary : Qt.rgba(1, 1, 1, 0.3)
border.width: passwordField.activeFocus ? 2 : 1
DankIcon {
id: lockIcon
Item {
id: lockIconContainer
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
name: "lock"
size: 20
color: passwordField.activeFocus ? Theme.primary : Theme.surfaceVariantText
width: 20
height: 20
DankIcon {
id: lockIcon
anchors.centerIn: parent
name: {
if (pam.fprint.tries >= SettingsData.maxFprintTries)
return "fingerprint_off";
if (pam.fprint.active)
return "fingerprint";
return "lock";
}
size: 20
color: pam.fprint.tries >= SettingsData.maxFprintTries ? Theme.error : (passwordField.activeFocus ? Theme.primary : Theme.surfaceVariantText)
opacity: pam.passwd.active ? 0 : 1
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
TextInput {
id: passwordField
anchors.fill: parent
anchors.leftMargin: lockIcon.width + Theme.spacingM * 2
anchors.leftMargin: lockIconContainer.width + Theme.spacingM * 2
anchors.rightMargin: {
let margin = Theme.spacingM
if (loadingSpinner.visible) {
@@ -306,9 +322,12 @@ Item {
}
}
onAccepted: {
if (!demoMode && !pam.active) {
if (!demoMode && !pam.passwd.active) {
console.log("Enter pressed, starting PAM authentication")
pam.start()
if (pam.fprint.active) {
pam.fprint.abort()
}
pam.passwd.start()
}
}
Keys.onPressed: event => {
@@ -316,7 +335,7 @@ Item {
return
}
if (pam.active) {
if (pam.passwd.active) {
console.log("PAM is active, ignoring input")
event.accepted = true
return
@@ -336,7 +355,7 @@ Item {
}
onActiveFocusChanged: {
if (!activeFocus && !demoMode && visible && passwordField) {
if (!activeFocus && !demoMode && visible && passwordField && !powerMenu.isVisible) {
Qt.callLater(() => {
if (passwordField && passwordField.forceActiveFocus) {
passwordField.forceActiveFocus()
@@ -346,7 +365,7 @@ Item {
}
onEnabledChanged: {
if (enabled && !demoMode && visible && passwordField) {
if (enabled && !demoMode && visible && passwordField && !powerMenu.isVisible) {
Qt.callLater(() => {
if (passwordField && passwordField.forceActiveFocus) {
passwordField.forceActiveFocus()
@@ -365,7 +384,7 @@ Item {
StyledText {
id: placeholder
anchors.left: lockIcon.right
anchors.left: lockIconContainer.right
anchors.leftMargin: Theme.spacingM
anchors.right: (revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right))))
anchors.rightMargin: 2
@@ -377,12 +396,12 @@ Item {
if (root.unlocking) {
return "Unlocking..."
}
if (pam.active) {
if (pam.passwd.active) {
return "Authenticating..."
}
return "Password..."
}
color: root.unlocking ? Theme.primary : (pam.active ? Theme.primary : Theme.outline)
color: root.unlocking ? Theme.primary : (pam.passwd.active ? Theme.primary : Theme.outline)
font.pixelSize: Theme.fontSizeMedium
opacity: (demoMode || root.passwordBuffer.length === 0) ? 1 : 0
@@ -402,7 +421,7 @@ Item {
}
StyledText {
anchors.left: lockIcon.right
anchors.left: lockIconContainer.right
anchors.leftMargin: Theme.spacingM
anchors.right: (revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right))))
anchors.rightMargin: 2
@@ -437,7 +456,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter
iconName: parent.showPassword ? "visibility_off" : "visibility"
buttonSize: 32
visible: !demoMode && root.passwordBuffer.length > 0 && !pam.active && !root.unlocking
visible: !demoMode && root.passwordBuffer.length > 0 && !pam.passwd.active && !root.unlocking
enabled: visible
onClicked: parent.showPassword = !parent.showPassword
}
@@ -449,7 +468,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter
iconName: "keyboard"
buttonSize: 32
visible: !demoMode && !pam.active && !root.unlocking
visible: !demoMode && !pam.passwd.active && !root.unlocking
enabled: visible
onClicked: {
if (keyboardController.isKeyboardActive) {
@@ -470,7 +489,7 @@ Item {
height: 24
radius: 12
color: "transparent"
visible: !demoMode && (pam.active || root.unlocking)
visible: !demoMode && (pam.passwd.active || root.unlocking)
DankIcon {
anchors.centerIn: parent
@@ -502,7 +521,7 @@ Item {
Item {
anchors.fill: parent
visible: pam.active && !root.unlocking
visible: pam.passwd.active && !root.unlocking
Rectangle {
width: 20
@@ -532,7 +551,7 @@ Item {
}
RotationAnimation on rotation {
running: pam.active && !root.unlocking
running: pam.passwd.active && !root.unlocking
loops: Animation.Infinite
duration: Anims.durLong
from: 0
@@ -550,12 +569,15 @@ Item {
anchors.verticalCenter: parent.verticalCenter
iconName: "keyboard_return"
buttonSize: 36
visible: (demoMode || (!pam.active && !root.unlocking))
visible: (demoMode || (!pam.passwd.active && !root.unlocking))
enabled: !demoMode
onClicked: {
if (!demoMode) {
console.log("Enter button clicked, starting PAM authentication")
pam.start()
if (pam.fprint.active) {
pam.fprint.abort()
}
pam.passwd.start()
}
}
@@ -743,8 +765,9 @@ Item {
Repeater {
model: 6
delegate: Rectangle {
required property int index
Rectangle {
width: 2
height: {
if (MprisController.activePlayer?.playbackState === MprisPlaybackState.Playing && CavaService.values.length > index) {
@@ -1070,53 +1093,19 @@ Item {
}
}
Row {
DankActionButton {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.margins: Theme.spacingXL
spacing: Theme.spacingL
visible: SettingsData.lockScreenShowPowerActions
DankActionButton {
iconName: "power_settings_new"
iconColor: Theme.error
buttonSize: 40
onClicked: {
if (demoMode) {
console.log("Demo: Power")
} else {
showPowerDialog("Power Off", "Power off this computer?", "Power Off", Theme.error, function () {
SessionService.poweroff()
})
}
}
}
DankActionButton {
iconName: "refresh"
buttonSize: 40
onClicked: {
if (demoMode) {
console.log("Demo: Reboot")
} else {
showPowerDialog("Restart", "Restart this computer?", "Restart", Theme.primary, function () {
SessionService.reboot()
})
}
}
}
DankActionButton {
iconName: "logout"
buttonSize: 40
onClicked: {
if (demoMode) {
console.log("Demo: Logout")
} else {
showPowerDialog("Log Out", "End this session?", "Log Out", Theme.primary, function () {
SessionService.logout()
})
}
iconName: "power_settings_new"
iconColor: Theme.error
buttonSize: 40
onClicked: {
if (demoMode) {
console.log("Demo: Power Menu")
} else {
powerMenu.show()
}
}
}
@@ -1136,52 +1125,29 @@ Item {
}
}
FileView {
id: pamConfigWatcher
path: "/etc/pam.d/dankshell"
printErrors: false
Pam {
id: pam
lockSecured: !demoMode
onUnlockRequested: {
root.unlocking = true
passwordField.text = ""
root.passwordBuffer = ""
root.unlockRequested()
}
onStateChanged: {
root.pamState = state
if (state !== "") {
placeholderDelay.restart()
passwordField.text = ""
root.passwordBuffer = ""
}
}
}
PamContext {
id: pam
config: pamConfigWatcher.loaded ? "dankshell" : "login"
onResponseRequiredChanged: {
if (demoMode)
return
console.log("PAM response required:", responseRequired)
if (!responseRequired)
return
console.log("Responding to PAM with password buffer length:", root.passwordBuffer.length)
respond(root.passwordBuffer)
}
onCompleted: res => {
if (demoMode)
return
console.log("PAM authentication completed with result:", res)
if (res === PamResult.Success) {
console.log("Authentication successful, unlocking")
root.unlocking = true
passwordField.text = ""
root.passwordBuffer = ""
root.unlockRequested()
return
}
console.log("Authentication failed:", res)
passwordField.text = ""
root.passwordBuffer = ""
if (res === PamResult.Error)
root.pamState = "error"
else if (res === PamResult.MaxTries)
root.pamState = "max"
else if (res === PamResult.Failed)
root.pamState = "fail"
placeholderDelay.restart()
}
Binding {
target: pam
property: "buffer"
value: root.passwordBuffer
}
Timer {
@@ -1197,91 +1163,12 @@ Item {
onClicked: root.unlockRequested()
}
// Internal power dialog
Rectangle {
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.8)
visible: powerDialogVisible
z: 1000
Rectangle {
anchors.centerIn: parent
width: 320
height: 180
radius: Theme.cornerRadius
color: Theme.surfaceContainer
border.color: Theme.outline
border.width: 1
Column {
anchors.centerIn: parent
spacing: Theme.spacingXL
DankIcon {
anchors.horizontalCenter: parent.horizontalCenter
name: "power_settings_new"
size: 32
color: powerDialogConfirmColor
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: powerDialogMessage
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
Rectangle {
width: 100
height: 40
radius: Theme.cornerRadius
color: Theme.surfaceVariant
StyledText {
anchors.centerIn: parent
text: I18n.tr("Cancel")
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: hidePowerDialog()
}
}
Rectangle {
width: 100
height: 40
radius: Theme.cornerRadius
color: powerDialogConfirmColor
StyledText {
anchors.centerIn: parent
text: powerDialogConfirmText
color: Theme.primaryText
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
hidePowerDialog()
powerDialogOnConfirm()
}
}
}
}
LockPowerMenu {
id: powerMenu
showLogout: true
onClosed: {
if (!demoMode && passwordField && passwordField.forceActiveFocus) {
Qt.callLater(() => passwordField.forceActiveFocus())
}
}
}

View File

@@ -1,3 +1,5 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Wayland

View File

@@ -1,5 +1,6 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import qs.Common

177
Modules/Lock/Pam.qml Normal file
View File

@@ -0,0 +1,177 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Services.Pam
import qs.Common
Scope {
id: root
property bool lockSecured: false
readonly property alias passwd: passwd
readonly property alias fprint: fprint
property string lockMessage
property string state
property string fprintState
property string buffer
signal flashMsg
signal unlockRequested
FileView {
id: pamConfigWatcher
path: "/etc/pam.d/dankshell"
printErrors: false
}
PamContext {
id: passwd
config: pamConfigWatcher.loaded ? "dankshell" : "login"
onMessageChanged: {
if (message.startsWith("The account is locked"))
root.lockMessage = message;
else if (root.lockMessage && message.endsWith(" left to unlock)"))
root.lockMessage += "\n" + message;
}
onResponseRequiredChanged: {
if (!responseRequired)
return;
respond(root.buffer);
}
onCompleted: res => {
if (res === PamResult.Success) {
root.unlockRequested();
return;
}
if (res === PamResult.Error)
root.state = "error";
else if (res === PamResult.MaxTries)
root.state = "max";
else if (res === PamResult.Failed)
root.state = "fail";
root.flashMsg();
stateReset.restart();
}
}
PamContext {
id: fprint
property bool available
property int tries
property int errorTries
function checkAvail(): void {
if (!available || !SettingsData.enableFprint || !root.lockSecured) {
abort();
return;
}
tries = 0;
errorTries = 0;
start();
}
config: "fprint"
configDirectory: Quickshell.shellDir + "/assets/pam"
onCompleted: res => {
if (!available)
return;
if (res === PamResult.Success) {
root.unlockRequested();
return;
}
if (res === PamResult.Error) {
root.fprintState = "error";
errorTries++;
if (errorTries < 5) {
abort();
errorRetry.restart();
}
} else if (res === PamResult.MaxTries) {
tries++;
if (tries < SettingsData.maxFprintTries) {
root.fprintState = "fail";
start();
} else {
root.fprintState = "max";
abort();
}
}
root.flashMsg();
fprintStateReset.start();
}
}
Process {
id: availProc
command: ["sh", "-c", "fprintd-list $USER"]
onExited: code => {
fprint.available = code === 0;
fprint.checkAvail();
}
}
Timer {
id: errorRetry
interval: 800
onTriggered: fprint.start()
}
Timer {
id: stateReset
interval: 4000
onTriggered: {
if (root.state !== "max")
root.state = "";
}
}
Timer {
id: fprintStateReset
interval: 4000
onTriggered: {
root.fprintState = "";
fprint.errorTries = 0;
}
}
onLockSecuredChanged: {
if (lockSecured) {
availProc.running = true;
root.state = "";
root.fprintState = "";
root.lockMessage = "";
} else {
fprint.abort();
}
}
Connections {
target: SettingsData
function onEnableFprintChanged(): void {
fprint.checkAvail();
}
}
}

View File

@@ -28,6 +28,10 @@ Item {
signal hideRequested()
Ref {
service: NotepadStorageService
}
function hasUnsavedChanges() {
return textEditor.hasUnsavedChanges()
}

View File

@@ -275,7 +275,7 @@ Column {
// Match count display
StyledText {
Layout.alignment: Qt.AlignVCenter
text: matchCount > 0 ? I18n.tr("%1/%2").arg(currentMatchIndex + 1).arg(matchCount) : searchQuery.length > 0 ? I18n.tr("No matches") : ""
text: matchCount > 0 ? "%1/%2".arg(currentMatchIndex + 1).arg(matchCount) : searchQuery.length > 0 ? I18n.tr("No matches") : ""
font.pixelSize: Theme.fontSizeSmall
color: matchCount > 0 ? Theme.primary : Theme.surfaceTextMedium
visible: searchQuery.length > 0

View File

@@ -34,39 +34,18 @@ Item {
buttonSize: 28
anchors.verticalCenter: parent.verticalCenter
onClicked: SessionData.setDoNotDisturb(!SessionData.doNotDisturb)
Rectangle {
id: doNotDisturbTooltip
width: tooltipText.contentWidth + Theme.spacingS * 2
height: tooltipText.contentHeight + Theme.spacingXS * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainer
border.color: Theme.outline
border.width: 1
anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
visible: doNotDisturbButton.children[1].containsMouse
opacity: visible ? 1 : 0
StyledText {
id: tooltipText
text: I18n.tr("Do Not Disturb")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.centerIn: parent
font.hintingPreference: Font.PreferFullHinting
onEntered: {
tooltipLoader.active = true
if (tooltipLoader.item) {
const p = mapToItem(null, width / 2, 0)
tooltipLoader.item.show(I18n.tr("Do Not Disturb"), p.x, p.y - 40, null)
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
onExited: {
if (tooltipLoader.item) {
tooltipLoader.item.hide()
}
tooltipLoader.active = false
}
}
}
@@ -139,4 +118,11 @@ Item {
}
}
Loader {
id: tooltipLoader
active: false
sourceComponent: DankTooltip {}
}
}

View File

@@ -107,6 +107,7 @@ DankOSD {
AudioService.suppressOSD = true
AudioService.sink.audio.volume = newValue / 100
AudioService.suppressOSD = false
resetHideTimer()
}
}

View File

@@ -8,11 +8,12 @@ Item {
required property string pluginId
property var pluginService: null
default property alias content: settingsColumn.children
default property list<QtObject> content
signal settingChanged()
property var variants: []
property alias variantsModel: variantsListModel
implicitHeight: hasPermission ? settingsColumn.implicitHeight : errorText.implicitHeight
height: implicitHeight
@@ -33,8 +34,8 @@ Item {
onPluginServiceChanged: {
if (pluginService) {
loadVariants()
for (let i = 0; i < settingsColumn.children.length; i++) {
const child = settingsColumn.children[i]
for (let i = 0; i < content.length; i++) {
const child = content[i]
if (child.loadValue) {
child.loadValue()
}
@@ -42,6 +43,15 @@ Item {
}
}
onContentChanged: {
for (let i = 0; i < content.length; i++) {
const item = content[i]
if (item instanceof Item) {
item.parent = settingsColumn
}
}
}
Connections {
target: pluginService
function onPluginDataChanged(changedPluginId) {
@@ -57,6 +67,22 @@ Item {
return
}
variants = pluginService.getPluginVariants(pluginId)
syncVariantsToModel()
}
function syncVariantsToModel() {
variantsListModel.clear()
for (let i = 0; i < variants.length; i++) {
variantsListModel.append(variants[i])
}
}
onVariantsChanged: {
syncVariantsToModel()
}
ListModel {
id: variantsListModel
}
function createVariant(variantName, variantConfig) {
@@ -101,11 +127,47 @@ Item {
return defaultValue
}
function findFlickable(item) {
var current = item?.parent
while (current) {
if (current.contentY !== undefined && current.contentHeight !== undefined) {
return current
}
current = current.parent
}
return null
}
function ensureItemVisible(item) {
if (!item) return
var flickable = findFlickable(root)
if (!flickable) return
var itemGlobalY = item.mapToItem(null, 0, 0).y
var itemHeight = item.height
var flickableGlobalY = flickable.mapToItem(null, 0, 0).y
var viewportHeight = flickable.height
var itemRelativeY = itemGlobalY - flickableGlobalY
var viewportTop = 0
var viewportBottom = viewportHeight
if (itemRelativeY < viewportTop) {
flickable.contentY = Math.max(0, flickable.contentY - (viewportTop - itemRelativeY) - Theme.spacingL)
} else if (itemRelativeY + itemHeight > viewportBottom) {
flickable.contentY = Math.min(
flickable.contentHeight - viewportHeight,
flickable.contentY + (itemRelativeY + itemHeight - viewportBottom) + Theme.spacingL
)
}
}
StyledText {
id: errorText
visible: pluginService && !root.hasPermission
anchors.fill: parent
text: qsTr("This plugin does not have 'settings_write' permission.\n\nAdd \"permissions\": [\"settings_read\", \"settings_write\"] to plugin.json")
text: I18n.tr("This plugin does not have 'settings_write' permission.\n\nAdd \"permissions\": [\"settings_read\", \"settings_write\"] to plugin.json")
color: Theme.error
font.pixelSize: Theme.fontSizeMedium
wrapMode: Text.WordWrap

View File

@@ -258,7 +258,7 @@ Item {
StyledText {
text: `dms is a highly customizable, modern desktop shell with a <a href="https://m3.material.io/" style="text-decoration:none; color:${Theme.primary};">material 3 inspired</a> design.
<br /><br/>It is built on top of <a href="https://quickshell.org" style="text-decoration:none; color:${Theme.primary};">Quickshell</a>, a QT6 framework for building desktop shells.
<br /><br/>It is built with <a href="https://quickshell.org" style="text-decoration:none; color:${Theme.primary};">Quickshell</a>, a QT6 framework for building desktop shells, and <a href="https://go.dev" style="text-decoration:none; color:${Theme.primary};">Go</a>, a statically typed, compiled programming language.
`
textFormat: Text.RichText
font.pixelSize: Theme.fontSizeMedium
@@ -352,7 +352,7 @@ Item {
}
StyledText {
text: I18n.tr("QML (Qt Modeling Language)")
text: I18n.tr("QML, JavaScript, Go")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
}

View File

@@ -942,14 +942,51 @@ Item {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: I18n.tr("Edge Spacing (0 = edge-to-edge)")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
Row {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: I18n.tr("Edge Spacing (0 = edge-to-edge)")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - edgeSpacingText.implicitWidth - resetEdgeSpacingBtn.width - Theme.spacingS - Theme.spacingM
height: 1
StyledText {
id: edgeSpacingText
visible: false
text: I18n.tr("Edge Spacing (0 = edge-to-edge)")
font.pixelSize: Theme.fontSizeSmall
}
}
DankActionButton {
id: resetEdgeSpacingBtn
buttonSize: 20
iconName: "refresh"
iconSize: 12
backgroundColor: Theme.surfaceContainerHigh
iconColor: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
onClicked: {
SettingsData.setDankBarSpacing(4)
}
}
Item {
width: Theme.spacingS
height: 1
}
}
DankSlider {
id: edgeSpacingSlider
width: parent.width
height: 24
value: SettingsData.dankBarSpacing
@@ -963,6 +1000,13 @@ Item {
SettingsData.setDankBarSpacing(
newValue)
}
Binding {
target: edgeSpacingSlider
property: "value"
value: SettingsData.dankBarSpacing
restoreMode: Binding.RestoreBinding
}
}
}
@@ -970,19 +1014,56 @@ Item {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: I18n.tr("Exclusive Zone Offset")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
Row {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: I18n.tr("Exclusive Zone Offset")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - exclusiveZoneText.implicitWidth - resetExclusiveZoneBtn.width - Theme.spacingS - Theme.spacingM
height: 1
StyledText {
id: exclusiveZoneText
visible: false
text: I18n.tr("Exclusive Zone Offset")
font.pixelSize: Theme.fontSizeSmall
}
}
DankActionButton {
id: resetExclusiveZoneBtn
buttonSize: 20
iconName: "refresh"
iconSize: 12
backgroundColor: Theme.surfaceContainerHigh
iconColor: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
onClicked: {
SettingsData.setDankBarBottomGap(0)
}
}
Item {
width: Theme.spacingS
height: 1
}
}
DankSlider {
id: exclusiveZoneSlider
width: parent.width
height: 24
value: SettingsData.dankBarBottomGap
minimum: -100
maximum: 100
minimum: -50
maximum: 50
unit: ""
showValue: true
wheelEnabled: false
@@ -991,6 +1072,13 @@ Item {
SettingsData.setDankBarBottomGap(
newValue)
}
Binding {
target: exclusiveZoneSlider
property: "value"
value: SettingsData.dankBarBottomGap
restoreMode: Binding.RestoreBinding
}
}
}
@@ -998,14 +1086,51 @@ Item {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: I18n.tr("Size")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
Row {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: I18n.tr("Size")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - sizeText.implicitWidth - resetSizeBtn.width - Theme.spacingS - Theme.spacingM
height: 1
StyledText {
id: sizeText
visible: false
text: I18n.tr("Size")
font.pixelSize: Theme.fontSizeSmall
}
}
DankActionButton {
id: resetSizeBtn
buttonSize: 20
iconName: "refresh"
iconSize: 12
backgroundColor: Theme.surfaceContainerHigh
iconColor: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
onClicked: {
SettingsData.setDankBarInnerPadding(4)
}
}
Item {
width: Theme.spacingS
height: 1
}
}
DankSlider {
id: sizeSlider
width: parent.width
height: 24
value: SettingsData.dankBarInnerPadding
@@ -1019,9 +1144,115 @@ Item {
SettingsData.setDankBarInnerPadding(
newValue)
}
Binding {
target: sizeSlider
property: "value"
value: SettingsData.dankBarInnerPadding
restoreMode: Binding.RestoreBinding
}
}
}
Column {
width: parent.width
spacing: Theme.spacingM
DankToggle {
width: parent.width
text: I18n.tr("Auto Popup Gaps")
description: I18n.tr("Automatically calculate popup distance from bar edge.")
checked: SettingsData.popupGapsAuto
onToggled: checked => {
SettingsData.setPopupGapsAuto(checked)
}
}
Column {
width: parent.width
leftPadding: Theme.spacingM
spacing: Theme.spacingM
visible: !SettingsData.popupGapsAuto
Rectangle {
width: parent.width - parent.leftPadding
height: 1
color: Theme.outline
opacity: 0.2
}
Column {
width: parent.width - parent.leftPadding
spacing: Theme.spacingS
Row {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: I18n.tr("Manual Gap Size")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - manualGapSizeText.implicitWidth - resetManualGapSizeBtn.width - Theme.spacingS - Theme.spacingM
height: 1
StyledText {
id: manualGapSizeText
visible: false
text: I18n.tr("Manual Gap Size")
font.pixelSize: Theme.fontSizeSmall
}
}
DankActionButton {
id: resetManualGapSizeBtn
buttonSize: 20
iconName: "refresh"
iconSize: 12
backgroundColor: Theme.surfaceContainerHigh
iconColor: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
onClicked: {
SettingsData.setPopupGapsManual(4)
}
}
Item {
width: Theme.spacingS
height: 1
}
}
DankSlider {
id: popupGapsManualSlider
width: parent.width
height: 24
value: SettingsData.popupGapsManual
minimum: 0
maximum: 50
unit: ""
showValue: true
wheelEnabled: false
thumbOutlineColor: Theme.surfaceContainerHigh
onSliderValueChanged: newValue => {
SettingsData.setPopupGapsManual(newValue)
}
Binding {
target: popupGapsManualSlider
property: "value"
value: SettingsData.popupGapsManual
restoreMode: Binding.RestoreBinding
}
}
}
}
}
DankToggle {
width: parent.width
@@ -1056,14 +1287,227 @@ Item {
}
}
DankToggle {
Column {
width: parent.width
text: I18n.tr("Border")
description: "Add a 1px border to the bar. Smart edge detection only shows border on exposed sides."
checked: SettingsData.dankBarBorderEnabled
onToggled: checked => {
SettingsData.setDankBarBorderEnabled(checked)
}
spacing: Theme.spacingM
DankToggle {
width: parent.width
text: I18n.tr("Border")
description: "Add a 1px border to the bar. Smart edge detection only shows border on exposed sides."
checked: SettingsData.dankBarBorderEnabled
onToggled: checked => {
SettingsData.setDankBarBorderEnabled(checked)
}
}
Column {
width: parent.width
leftPadding: Theme.spacingM
spacing: Theme.spacingM
visible: SettingsData.dankBarBorderEnabled
Rectangle {
width: parent.width - parent.leftPadding
height: 1
color: Theme.outline
opacity: 0.2
}
Row {
width: parent.width - parent.leftPadding
spacing: Theme.spacingM
Column {
width: parent.width - borderColorGroup.width - Theme.spacingM
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Border Color")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: I18n.tr("Choose the border accent color")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
}
}
DankButtonGroup {
id: borderColorGroup
anchors.verticalCenter: parent.verticalCenter
model: ["Surface", "Secondary", "Primary"]
currentIndex: {
const colorOption = SettingsData.dankBarBorderColor || "surfaceText"
switch (colorOption) {
case "surfaceText": return 0
case "secondary": return 1
case "primary": return 2
default: return 0
}
}
onSelectionChanged: (index, selected) => {
if (selected) {
let newColor = "surfaceText"
switch (index) {
case 0: newColor = "surfaceText"; break
case 1: newColor = "secondary"; break
case 2: newColor = "primary"; break
}
if (SettingsData.dankBarBorderColor !== newColor) {
SettingsData.dankBarBorderColor = newColor
}
}
}
}
}
Column {
width: parent.width - parent.leftPadding
spacing: Theme.spacingS
Row {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: I18n.tr("Border Opacity")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - borderOpacityText.implicitWidth - resetBorderOpacityBtn.width - Theme.spacingS - Theme.spacingM
height: 1
StyledText {
id: borderOpacityText
visible: false
text: I18n.tr("Border Opacity")
font.pixelSize: Theme.fontSizeSmall
}
}
DankActionButton {
id: resetBorderOpacityBtn
buttonSize: 20
iconName: "refresh"
iconSize: 12
backgroundColor: Theme.surfaceContainerHigh
iconColor: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
onClicked: {
SettingsData.dankBarBorderOpacity = 1.0
}
}
Item {
width: Theme.spacingS
height: 1
}
}
DankSlider {
id: borderOpacitySlider
width: parent.width
height: 24
value: (SettingsData.dankBarBorderOpacity ?? 1.0) * 100
minimum: 0
maximum: 100
unit: "%"
showValue: true
wheelEnabled: false
thumbOutlineColor: Theme.surfaceContainerHigh
onSliderValueChanged: newValue => {
SettingsData.dankBarBorderOpacity = newValue / 100
}
Binding {
target: borderOpacitySlider
property: "value"
value: (SettingsData.dankBarBorderOpacity ?? 1.0) * 100
restoreMode: Binding.RestoreBinding
}
}
}
Column {
width: parent.width - parent.leftPadding
spacing: Theme.spacingS
Row {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: I18n.tr("Border Thickness")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - borderThicknessText.implicitWidth - resetBorderThicknessBtn.width - Theme.spacingS - Theme.spacingM
height: 1
StyledText {
id: borderThicknessText
visible: false
text: I18n.tr("Border Thickness")
font.pixelSize: Theme.fontSizeSmall
}
}
DankActionButton {
id: resetBorderThicknessBtn
buttonSize: 20
iconName: "refresh"
iconSize: 12
backgroundColor: Theme.surfaceContainerHigh
iconColor: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
onClicked: {
SettingsData.dankBarBorderThickness = 1
}
}
Item {
width: Theme.spacingS
height: 1
}
}
DankSlider {
id: borderThicknessSlider
width: parent.width
height: 24
value: SettingsData.dankBarBorderThickness ?? 1
minimum: 1
maximum: 10
unit: "px"
showValue: true
wheelEnabled: false
thumbOutlineColor: Theme.surfaceContainerHigh
onSliderValueChanged: newValue => {
SettingsData.dankBarBorderThickness = newValue
}
Binding {
target: borderThicknessSlider
property: "value"
value: SettingsData.dankBarBorderThickness ?? 1
restoreMode: Binding.RestoreBinding
}
}
}
}
}
Rectangle {

View File

@@ -76,6 +76,381 @@ Item {
width: parent.width
spacing: Theme.spacingXL
StyledRect {
width: parent.width
height: gammaSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0
Column {
id: gammaSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "brightness_6"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Gamma Control")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
id: nightModeToggle
width: parent.width
text: I18n.tr("Night Mode")
description: "Apply warm color temperature to reduce eye strain. Use automation settings below to control when it activates."
checked: DisplayService.nightModeEnabled
onToggled: checked => {
DisplayService.toggleNightMode()
}
Connections {
function onNightModeEnabledChanged() {
nightModeToggle.checked = DisplayService.nightModeEnabled
}
target: DisplayService
}
}
Column {
width: parent.width
spacing: 0
leftPadding: Theme.spacingM
rightPadding: Theme.spacingM
DankDropdown {
width: parent.width - parent.leftPadding - parent.rightPadding
text: I18n.tr("Temperature")
description: I18n.tr("Color temperature for night mode")
currentValue: SessionData.nightModeTemperature + "K"
options: {
var temps = []
for (var i = 2500; i <= 6000; i += 500) {
temps.push(i + "K")
}
return temps
}
onValueChanged: value => {
var temp = parseInt(value.replace("K", ""))
SessionData.setNightModeTemperature(temp)
}
}
}
DankToggle {
id: automaticToggle
width: parent.width
text: I18n.tr("Automatic Control")
description: "Only adjust gamma based on time or location rules."
checked: SessionData.nightModeAutoEnabled
onToggled: checked => {
if (checked && !DisplayService.nightModeEnabled) {
DisplayService.toggleNightMode()
} else if (!checked && DisplayService.nightModeEnabled) {
DisplayService.toggleNightMode()
}
SessionData.setNightModeAutoEnabled(checked)
}
Connections {
target: SessionData
function onNightModeAutoEnabledChanged() {
automaticToggle.checked = SessionData.nightModeAutoEnabled
}
}
}
Column {
id: automaticSettings
width: parent.width
spacing: Theme.spacingS
visible: SessionData.nightModeAutoEnabled
Connections {
target: SessionData
function onNightModeAutoEnabledChanged() {
automaticSettings.visible = SessionData.nightModeAutoEnabled
}
}
Item {
width: parent.width
height: 45 + Theme.spacingM
DankTabBar {
id: modeTabBarNight
width: 200
height: 45
anchors.horizontalCenter: parent.horizontalCenter
model: [{
"text": "Time",
"icon": "access_time"
}, {
"text": "Location",
"icon": "place"
}]
Component.onCompleted: {
currentIndex = SessionData.nightModeAutoMode === "location" ? 1 : 0
Qt.callLater(updateIndicator)
}
onTabClicked: index => {
console.log("Tab clicked:", index, "Setting mode to:", index === 1 ? "location" : "time")
DisplayService.setNightModeAutomationMode(index === 1 ? "location" : "time")
currentIndex = index
}
Connections {
target: SessionData
function onNightModeAutoModeChanged() {
modeTabBarNight.currentIndex = SessionData.nightModeAutoMode === "location" ? 1 : 0
Qt.callLater(modeTabBarNight.updateIndicator)
}
}
}
}
Column {
width: parent.width
spacing: Theme.spacingM
visible: SessionData.nightModeAutoMode === "time"
Column {
spacing: Theme.spacingXS
anchors.horizontalCenter: parent.horizontalCenter
Row {
spacing: Theme.spacingM
StyledText {
text: ""
width: 50
height: 20
}
StyledText {
text: I18n.tr("Hour")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: 70
horizontalAlignment: Text.AlignHCenter
}
StyledText {
text: I18n.tr("Minute")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: 70
horizontalAlignment: Text.AlignHCenter
}
}
Row {
spacing: Theme.spacingM
StyledText {
text: I18n.tr("Start")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
width: 50
height: 40
verticalAlignment: Text.AlignVCenter
}
DankDropdown {
dropdownWidth: 70
currentValue: SessionData.nightModeStartHour.toString()
options: {
var hours = []
for (var i = 0; i < 24; i++) {
hours.push(i.toString())
}
return hours
}
onValueChanged: value => {
SessionData.setNightModeStartHour(parseInt(value))
}
}
DankDropdown {
dropdownWidth: 70
currentValue: SessionData.nightModeStartMinute.toString().padStart(2, '0')
options: {
var minutes = []
for (var i = 0; i < 60; i += 5) {
minutes.push(i.toString().padStart(2, '0'))
}
return minutes
}
onValueChanged: value => {
SessionData.setNightModeStartMinute(parseInt(value))
}
}
}
Row {
spacing: Theme.spacingM
StyledText {
text: I18n.tr("End")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
width: 50
height: 40
verticalAlignment: Text.AlignVCenter
}
DankDropdown {
dropdownWidth: 70
currentValue: SessionData.nightModeEndHour.toString()
options: {
var hours = []
for (var i = 0; i < 24; i++) {
hours.push(i.toString())
}
return hours
}
onValueChanged: value => {
SessionData.setNightModeEndHour(parseInt(value))
}
}
DankDropdown {
dropdownWidth: 70
currentValue: SessionData.nightModeEndMinute.toString().padStart(2, '0')
options: {
var minutes = []
for (var i = 0; i < 60; i += 5) {
minutes.push(i.toString().padStart(2, '0'))
}
return minutes
}
onValueChanged: value => {
SessionData.setNightModeEndMinute(parseInt(value))
}
}
}
}
}
Column {
property bool isLocationMode: SessionData.nightModeAutoMode === "location"
visible: isLocationMode
spacing: Theme.spacingM
width: parent.width
DankToggle {
width: parent.width
text: I18n.tr("Auto-location")
description: DisplayService.geoclueAvailable ? "Use automatic location detection (geoclue2)" : "Geoclue service not running - cannot auto-detect location"
checked: SessionData.nightModeLocationProvider === "geoclue2"
enabled: DisplayService.geoclueAvailable
onToggled: checked => {
if (checked && DisplayService.geoclueAvailable) {
SessionData.setNightModeLocationProvider("geoclue2")
SessionData.setLatitude(0.0)
SessionData.setLongitude(0.0)
} else {
SessionData.setNightModeLocationProvider("")
}
}
}
Column {
width: parent.width
spacing: Theme.spacingM
visible: SessionData.nightModeLocationProvider !== "geoclue2"
leftPadding: Theme.spacingM
StyledText {
text: I18n.tr("Manual Coordinates")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
}
Row {
spacing: Theme.spacingL
Column {
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Latitude")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
width: 120
height: 40
text: SessionData.latitude.toString()
placeholderText: "0.0"
onTextChanged: {
const lat = parseFloat(text) || 0.0
if (lat >= -90 && lat <= 90) {
SessionData.setLatitude(lat)
}
}
}
}
Column {
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Longitude")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
width: 120
height: 40
text: SessionData.longitude.toString()
placeholderText: "0.0"
onTextChanged: {
const lon = parseFloat(text) || 0.0
if (lon >= -180 && lon <= 180) {
SessionData.setLongitude(lon)
}
}
}
}
}
StyledText {
text: I18n.tr("Uses sunrise/sunset times to automatically adjust night mode based on your location.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width - parent.leftPadding
wrapMode: Text.WordWrap
}
}
}
}
}
}
StyledRect {
width: parent.width
height: screensInfoSection.implicitHeight + Theme.spacingL * 2

View File

@@ -329,6 +329,69 @@ Item {
}
}
// Icon Size Section
StyledRect {
width: parent.width
height: iconSizeSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
visible: SettingsData.showDock
opacity: visible ? 1 : 0
Column {
id: iconSizeSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "photo_size_select_large"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Icon Size")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankSlider {
width: parent.width
height: 24
value: SettingsData.dockIconSize
minimum: 24
maximum: 96
unit: ""
showValue: true
wheelEnabled: false
thumbOutlineColor: Theme.surfaceContainerHigh
onSliderValueChanged: newValue => {
SettingsData.setDockIconSize(newValue)
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
// Dock Spacing Section
StyledRect {
width: parent.width

View File

@@ -448,15 +448,80 @@ Item {
DankTextField {
width: parent.width
text: SessionData.launchPrefix
text: SettingsData.launchPrefix
placeholderText: "Enter launch prefix (e.g., 'uwsm-app')"
onTextEdited: {
SessionData.setLaunchPrefix(text)
SettingsData.setLaunchPrefix(text)
}
}
}
}
StyledRect {
width: parent.width
height: sortingSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
Column {
id: sortingSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "sort_by_alpha"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Sort Alphabetically")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - parent.children[0].width
- parent.children[1].width
- sortToggle.width - Theme.spacingM * 3
height: 1
}
DankToggle {
id: sortToggle
width: 32
height: 18
checked: SettingsData.sortAppsAlphabetically
anchors.verticalCenter: parent.verticalCenter
onToggled: checked => {
SettingsData.setSortAppsAlphabetically(checked)
}
}
}
StyledText {
width: parent.width
text: I18n.tr("When enabled, apps are sorted alphabetically. When disabled, apps are sorted by usage frequency.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
}
}
StyledRect {
width: parent.width
height: recentlyUsedSection.implicitHeight + Theme.spacingL * 2
@@ -533,7 +598,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter
onClicked: {
AppUsageHistoryData.appUsageRanking = {}
SettingsData.saveSettings()
AppUsageHistoryData.saveSettings()
}
}
}
@@ -660,7 +725,7 @@ Item {
|| {})
delete currentRanking[modelData.id]
AppUsageHistoryData.appUsageRanking = currentRanking
SettingsData.saveSettings()
AppUsageHistoryData.saveSettings()
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -256,10 +256,9 @@ FocusScope {
anchors.bottomMargin: pluginDelegate.isExpanded ? settingsContainer.height : 0
hoverEnabled: true
cursorShape: pluginDelegate.hasSettings ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: pluginDelegate.hasSettings
onClicked: {
if (pluginDelegate.hasSettings) {
pluginsTab.expandedPluginId = pluginsTab.expandedPluginId === pluginDelegate.pluginId ? "" : pluginDelegate.pluginId
}
pluginsTab.expandedPluginId = pluginsTab.expandedPluginId === pluginDelegate.pluginId ? "" : pluginDelegate.pluginId
}
}
@@ -504,6 +503,10 @@ FocusScope {
clip: true
focus: pluginDelegate.isExpanded && pluginDelegate.hasSettings
Keys.onPressed: event => {
event.accepted = true
}
Rectangle {
anchors.fill: parent
color: Theme.surfaceContainerHighest

View File

@@ -14,70 +14,42 @@ Item {
property var cachedFontFamilies: []
property var cachedMonoFamilies: []
property var cachedIconThemes: []
property var cachedMatugenSchemes: []
property bool fontsEnumerated: false
function enumerateFonts() {
var fonts = ["Default"]
var fonts = []
var availableFonts = Qt.fontFamilies()
var rootFamilies = []
var seenFamilies = new Set()
for (var i = 0; i < availableFonts.length; i++) {
var fontName = availableFonts[i]
if (fontName.startsWith("."))
continue
if (fontName === SettingsData.defaultFontFamily)
continue
var rootName = fontName.replace(
/ (Thin|Extra Light|Light|Regular|Medium|Semi Bold|Demi Bold|Bold|Extra Bold|Black|Heavy)$/i,
"").replace(
/ (Italic|Oblique|Condensed|Extended|Narrow|Wide)$/i,
"").replace(/ (UI|Display|Text|Mono|Sans|Serif)$/i,
function (match, suffix) {
return match
}).trim()
if (!seenFamilies.has(rootName) && rootName !== "") {
seenFamilies.add(rootName)
rootFamilies.push(rootName)
}
fonts.push(fontName)
}
cachedFontFamilies = fonts.concat(rootFamilies.sort())
var monoFonts = ["Default"]
var monoFamilies = []
var seenMonoFamilies = new Set()
fonts.sort()
fonts.unshift("Default")
cachedFontFamilies = fonts
var monoFonts = []
for (var j = 0; j < availableFonts.length; j++) {
var fontName2 = availableFonts[j]
if (fontName2.startsWith("."))
continue
if (fontName2 === SettingsData.defaultMonoFontFamily)
continue
var lowerName = fontName2.toLowerCase()
if (lowerName.includes("mono") || lowerName.includes(
"code") || lowerName.includes(
"console") || lowerName.includes(
"terminal") || lowerName.includes(
"courier") || lowerName.includes(
"dejavu sans mono") || lowerName.includes(
"jetbrains") || lowerName.includes(
"fira") || lowerName.includes(
"hack") || lowerName.includes(
"source code") || lowerName.includes(
"ubuntu mono") || lowerName.includes("cascadia")) {
var rootName2 = fontName2.replace(
/ (Thin|Extra Light|Light|Regular|Medium|Semi Bold|Demi Bold|Bold|Extra Bold|Black|Heavy)$/i,
"").replace(
/ (Italic|Oblique|Condensed|Extended|Narrow|Wide)$/i,
"").trim()
if (!seenMonoFamilies.has(rootName2) && rootName2 !== "") {
seenMonoFamilies.add(rootName2)
monoFamilies.push(rootName2)
}
if (lowerName.includes("mono") || lowerName.includes("code") ||
lowerName.includes("console") || lowerName.includes("terminal") ||
lowerName.includes("courier") || lowerName.includes("jetbrains") ||
lowerName.includes("fira") || lowerName.includes("hack") ||
lowerName.includes("source code") || lowerName.includes("cascadia")) {
monoFonts.push(fontName2)
}
}
cachedMonoFamilies = monoFonts.concat(monoFamilies.sort())
monoFonts.sort()
monoFonts.unshift("Default")
cachedMonoFamilies = monoFonts
}
Component.onCompleted: {
@@ -85,6 +57,9 @@ Item {
enumerateFonts()
fontsEnumerated = true
}
SettingsData.detectAvailableIconThemes()
cachedIconThemes = SettingsData.availableIconThemes
cachedMatugenSchemes = Theme.availableMatugenSchemes.map(function (option) { return option.label })
}
DankFlickable {
@@ -653,7 +628,7 @@ Item {
id: matugenPaletteDropdown
text: I18n.tr("Matugen Palette")
description: "Select the palette algorithm used for wallpaper-based colors"
options: Theme.availableMatugenSchemes.map(function (option) { return option.label })
options: cachedMatugenSchemes
currentValue: Theme.getMatugenScheme(SettingsData.matugenScheme).label
enabled: Theme.matugenAvailable
opacity: enabled ? 1 : 0.4
@@ -929,6 +904,294 @@ Item {
}
}
StyledRect {
width: parent.width
height: fontSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
Column {
id: fontSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "font_download"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Font Settings")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankDropdown {
text: I18n.tr("Font Family")
description: I18n.tr("Select system font family")
currentValue: {
if (SettingsData.fontFamily === SettingsData.defaultFontFamily)
return "Default"
else
return SettingsData.fontFamily || "Default"
}
enableFuzzySearch: true
popupWidthOffset: 100
maxPopupHeight: 400
options: cachedFontFamilies
onValueChanged: value => {
if (value.startsWith("Default"))
SettingsData.setFontFamily(SettingsData.defaultFontFamily)
else
SettingsData.setFontFamily(value)
}
}
DankDropdown {
text: I18n.tr("Font Weight")
description: I18n.tr("Select font weight")
currentValue: {
switch (SettingsData.fontWeight) {
case Font.Thin:
return "Thin"
case Font.ExtraLight:
return "Extra Light"
case Font.Light:
return "Light"
case Font.Normal:
return "Regular"
case Font.Medium:
return "Medium"
case Font.DemiBold:
return "Demi Bold"
case Font.Bold:
return "Bold"
case Font.ExtraBold:
return "Extra Bold"
case Font.Black:
return "Black"
default:
return "Regular"
}
}
options: ["Thin", "Extra Light", "Light", "Regular", "Medium", "Demi Bold", "Bold", "Extra Bold", "Black"]
onValueChanged: value => {
var weight
switch (value) {
case "Thin":
weight = Font.Thin
break
case "Extra Light":
weight = Font.ExtraLight
break
case "Light":
weight = Font.Light
break
case "Regular":
weight = Font.Normal
break
case "Medium":
weight = Font.Medium
break
case "Demi Bold":
weight = Font.DemiBold
break
case "Bold":
weight = Font.Bold
break
case "Extra Bold":
weight = Font.ExtraBold
break
case "Black":
weight = Font.Black
break
default:
weight = Font.Normal
break
}
SettingsData.setFontWeight(weight)
}
}
DankDropdown {
text: I18n.tr("Monospace Font")
description: I18n.tr("Select monospace font for process list and technical displays")
currentValue: {
if (SettingsData.monoFontFamily === SettingsData.defaultMonoFontFamily)
return "Default"
return SettingsData.monoFontFamily || "Default"
}
enableFuzzySearch: true
popupWidthOffset: 100
maxPopupHeight: 400
options: cachedMonoFamilies
onValueChanged: value => {
if (value === "Default")
SettingsData.setMonoFontFamily(SettingsData.defaultMonoFontFamily)
else
SettingsData.setMonoFontFamily(value)
}
}
Rectangle {
width: parent.width
height: 60
radius: Theme.cornerRadius
color: "transparent"
Column {
anchors.left: parent.left
anchors.right: fontScaleControls.left
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Font Scale")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Scale all font sizes")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
}
}
Row {
id: fontScaleControls
width: 180
height: 36
anchors.right: parent.right
anchors.rightMargin: 0
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankActionButton {
buttonSize: 32
iconName: "remove"
iconSize: Theme.iconSizeSmall
enabled: SettingsData.fontScale > 1.0
backgroundColor: Theme.surfaceContainerHigh
iconColor: Theme.surfaceText
onClicked: {
var newScale = Math.max(1.0, SettingsData.fontScale - 0.05)
SettingsData.setFontScale(newScale)
}
}
StyledRect {
width: 60
height: 32
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r,
Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
StyledText {
anchors.centerIn: parent
text: (SettingsData.fontScale * 100).toFixed(
0) + "%"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
}
}
DankActionButton {
buttonSize: 32
iconName: "add"
iconSize: Theme.iconSizeSmall
enabled: SettingsData.fontScale < 2.0
backgroundColor: Theme.surfaceContainerHigh
iconColor: Theme.surfaceText
onClicked: {
var newScale = Math.min(2.0,
SettingsData.fontScale + 0.05)
SettingsData.setFontScale(newScale)
}
}
}
}
}
}
StyledRect {
width: parent.width
height: portalSyncSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
Row {
id: portalSyncSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
DankIcon {
name: "sync"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM - syncToggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("Sync Mode with Portal")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Sync dark mode with settings portals for system-wide theme hints")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
DankToggle {
id: syncToggle
width: 48
height: 32
checked: SettingsData.syncModeWithPortal
anchors.verticalCenter: parent.verticalCenter
onToggled: checked => SettingsData.setSyncModeWithPortal(checked)
}
}
}
// System Configuration Warning
Rectangle {
width: parent.width
@@ -992,6 +1255,7 @@ Item {
}
DankDropdown {
width: parent.width - Theme.iconSize - Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("Icon Theme")
description: "DankShell & System Icons\n(requires restart)"
@@ -999,10 +1263,7 @@ Item {
enableFuzzySearch: true
popupWidthOffset: 100
maxPopupHeight: 236
options: {
SettingsData.detectAvailableIconThemes()
return SettingsData.availableIconThemes
}
options: cachedIconThemes
onValueChanged: value => {
SettingsData.setIconTheme(value)
if (Quickshell.env("QT_QPA_PLATFORMTHEME") != "gtk3" &&

View File

@@ -1,378 +0,0 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Widgets
Item {
id: timeTab
DankFlickable {
anchors.fill: parent
anchors.topMargin: Theme.spacingL
clip: true
contentHeight: mainColumn.height
contentWidth: width
Column {
id: mainColumn
width: parent.width
spacing: Theme.spacingXL
// Time Format
StyledRect {
width: parent.width
height: timeSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
Column {
id: timeSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "schedule"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM
- toggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("24-Hour Format")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Use 24-hour time format instead of 12-hour AM/PM")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
DankToggle {
id: toggle
anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.use24HourClock
onToggled: checked => {
return SettingsData.setClockFormat(
checked)
}
}
}
}
}
// Date Format Section
StyledRect {
width: parent.width
height: dateSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
Column {
id: dateSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "calendar_today"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Date Format")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankDropdown {
height: 50
text: I18n.tr("Top Bar Format")
description: "Preview: " + (SettingsData.clockDateFormat ? new Date().toLocaleDateString(Qt.locale(), SettingsData.clockDateFormat) : new Date().toLocaleDateString(Qt.locale(), "ddd d"))
currentValue: {
if (!SettingsData.clockDateFormat || SettingsData.clockDateFormat.length === 0) {
return "System Default"
}
// Find matching preset or show "Custom"
const presets = [{
"format": "ddd d",
"label": "Day Date"
}, {
"format": "ddd MMM d",
"label": "Day Month Date"
}, {
"format": "MMM d",
"label": "Month Date"
}, {
"format": "M/d",
"label": "Numeric (M/D)"
}, {
"format": "d/M",
"label": "Numeric (D/M)"
}, {
"format": "ddd d MMM yyyy",
"label": "Full with Year"
}, {
"format": "yyyy-MM-dd",
"label": "ISO Date"
}, {
"format": "dddd, MMMM d",
"label": "Full Day & Month"
}]
const match = presets.find(p => {
return p.format
=== SettingsData.clockDateFormat
})
return match ? match.label: I18n.tr("Custom: ") + SettingsData.clockDateFormat
}
options: ["System Default", "Day Date", "Day Month Date", "Month Date", "Numeric (M/D)", "Numeric (D/M)", "Full with Year", "ISO Date", "Full Day & Month", "Custom..."]
onValueChanged: value => {
const formatMap = {
"System Default": "",
"Day Date": "ddd d",
"Day Month Date": "ddd MMM d",
"Month Date": "MMM d",
"Numeric (M/D)": "M/d",
"Numeric (D/M)": "d/M",
"Full with Year": "ddd d MMM yyyy",
"ISO Date": "yyyy-MM-dd",
"Full Day & Month": "dddd, MMMM d"
}
if (value === "Custom...") {
customFormatInput.visible = true
} else {
customFormatInput.visible = false
SettingsData.setClockDateFormat(
formatMap[value])
}
}
}
DankDropdown {
height: 50
text: I18n.tr("Lock Screen Format")
description: "Preview: " + (SettingsData.lockDateFormat ? new Date().toLocaleDateString(Qt.locale(), SettingsData.lockDateFormat) : new Date().toLocaleDateString(Qt.locale(), Locale.LongFormat))
currentValue: {
if (!SettingsData.lockDateFormat || SettingsData.lockDateFormat.length === 0) {
return "System Default"
}
// Find matching preset or show "Custom"
const presets = [{
"format": "ddd d",
"label": "Day Date"
}, {
"format": "ddd MMM d",
"label": "Day Month Date"
}, {
"format": "MMM d",
"label": "Month Date"
}, {
"format": "M/d",
"label": "Numeric (M/D)"
}, {
"format": "d/M",
"label": "Numeric (D/M)"
}, {
"format": "ddd d MMM yyyy",
"label": "Full with Year"
}, {
"format": "yyyy-MM-dd",
"label": "ISO Date"
}, {
"format": "dddd, MMMM d",
"label": "Full Day & Month"
}]
const match = presets.find(p => {
return p.format
=== SettingsData.lockDateFormat
})
return match ? match.label: I18n.tr("Custom: ") + SettingsData.lockDateFormat
}
options: ["System Default", "Day Date", "Day Month Date", "Month Date", "Numeric (M/D)", "Numeric (D/M)", "Full with Year", "ISO Date", "Full Day & Month", "Custom..."]
onValueChanged: value => {
const formatMap = {
"System Default": "",
"Day Date": "ddd d",
"Day Month Date": "ddd MMM d",
"Month Date": "MMM d",
"Numeric (M/D)": "M/d",
"Numeric (D/M)": "d/M",
"Full with Year": "ddd d MMM yyyy",
"ISO Date": "yyyy-MM-dd",
"Full Day & Month": "dddd, MMMM d"
}
if (value === "Custom...") {
customLockFormatInput.visible = true
} else {
customLockFormatInput.visible = false
SettingsData.setLockDateFormat(
formatMap[value])
}
}
}
DankTextField {
id: customFormatInput
width: parent.width
visible: false
placeholderText: I18n.tr("Enter custom top bar format (e.g., ddd MMM d)")
text: SettingsData.clockDateFormat
onTextChanged: {
if (visible && text)
SettingsData.setClockDateFormat(text)
}
}
DankTextField {
id: customLockFormatInput
width: parent.width
visible: false
placeholderText: I18n.tr("Enter custom lock screen format (e.g., dddd, MMMM d)")
text: SettingsData.lockDateFormat
onTextChanged: {
if (visible && text)
SettingsData.setLockDateFormat(text)
}
}
Rectangle {
width: parent.width
height: formatHelp.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.1)
border.width: 0
Column {
id: formatHelp
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Format Legend")
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
font.weight: Font.Medium
}
Row {
width: parent.width
spacing: Theme.spacingL
Column {
width: (parent.width - Theme.spacingL) / 2
spacing: 2
StyledText {
text: I18n.tr("• d - Day (1-31)")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: I18n.tr("• dd - Day (01-31)")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: I18n.tr("• ddd - Day name (Mon)")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: I18n.tr("• dddd - Day name (Monday)")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: I18n.tr("• M - Month (1-12)")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
Column {
width: (parent.width - Theme.spacingL) / 2
spacing: 2
StyledText {
text: I18n.tr("• MM - Month (01-12)")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: I18n.tr("• MMM - Month (Jan)")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: I18n.tr("• MMMM - Month (January)")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: I18n.tr("• yy - Year (24)")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: I18n.tr("• yyyy - Year (2024)")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
}
}
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,390 +0,0 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Widgets
Item {
id: weatherTab
DankFlickable {
anchors.fill: parent
anchors.topMargin: Theme.spacingL
clip: true
contentHeight: mainColumn.height
contentWidth: width
Column {
id: mainColumn
width: parent.width
spacing: Theme.spacingXL
// Enable Weather
StyledRect {
width: parent.width
height: enableWeatherSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
Column {
id: enableWeatherSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "cloud"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM
- enableToggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("Enable Weather")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Show weather information in top bar and control center")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
DankToggle {
id: enableToggle
anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.weatherEnabled
onToggled: checked => {
return SettingsData.setWeatherEnabled(
checked)
}
}
}
}
}
// Temperature Unit
StyledRect {
width: parent.width
height: temperatureSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
visible: SettingsData.weatherEnabled
opacity: visible ? 1 : 0
Column {
id: temperatureSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "thermostat"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM
- temperatureToggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("Use Fahrenheit")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Use Fahrenheit instead of Celsius for temperature")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
DankToggle {
id: temperatureToggle
anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.useFahrenheit
onToggled: checked => {
return SettingsData.setTemperatureUnit(
checked)
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
// Location Settings
StyledRect {
width: parent.width
height: locationSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
visible: SettingsData.weatherEnabled
opacity: visible ? 1 : 0
Column {
id: locationSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "location_on"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM
- autoLocationToggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("Auto Location")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Automatically determine your location using your IP address")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
DankToggle {
id: autoLocationToggle
anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.useAutoLocation
onToggled: checked => {
return SettingsData.setAutoLocation(
checked)
}
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
visible: !SettingsData.useAutoLocation
Rectangle {
width: parent.width
height: 1
color: Theme.outline
opacity: 0.2
}
StyledText {
text: I18n.tr("Custom Location")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
Row {
width: parent.width
spacing: Theme.spacingM
Column {
width: (parent.width - Theme.spacingM) / 2
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Latitude")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: latitudeInput
width: parent.width
height: 48
placeholderText: "40.7128"
backgroundColor: Theme.surfaceVariant
normalBorderColor: Theme.primarySelected
focusedBorderColor: Theme.primary
keyNavigationTab: longitudeInput
Component.onCompleted: {
if (SettingsData.weatherCoordinates) {
const coords = SettingsData.weatherCoordinates.split(',')
if (coords.length > 0) {
text = coords[0].trim()
}
}
}
Connections {
target: SettingsData
function onWeatherCoordinatesChanged() {
if (SettingsData.weatherCoordinates) {
const coords = SettingsData.weatherCoordinates.split(',')
if (coords.length > 0) {
latitudeInput.text = coords[0].trim()
}
}
}
}
onTextEdited: {
if (text && longitudeInput.text) {
const coords = text + "," + longitudeInput.text
SettingsData.weatherCoordinates = coords
SettingsData.saveSettings()
}
}
}
}
Column {
width: (parent.width - Theme.spacingM) / 2
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Longitude")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: longitudeInput
width: parent.width
height: 48
placeholderText: "-74.0060"
backgroundColor: Theme.surfaceVariant
normalBorderColor: Theme.primarySelected
focusedBorderColor: Theme.primary
keyNavigationTab: locationSearchInput
keyNavigationBacktab: latitudeInput
Component.onCompleted: {
if (SettingsData.weatherCoordinates) {
const coords = SettingsData.weatherCoordinates.split(',')
if (coords.length > 1) {
text = coords[1].trim()
}
}
}
Connections {
target: SettingsData
function onWeatherCoordinatesChanged() {
if (SettingsData.weatherCoordinates) {
const coords = SettingsData.weatherCoordinates.split(',')
if (coords.length > 1) {
longitudeInput.text = coords[1].trim()
}
}
}
}
onTextEdited: {
if (text && latitudeInput.text) {
const coords = latitudeInput.text + "," + text
SettingsData.weatherCoordinates = coords
SettingsData.saveSettings()
}
}
}
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Location Search")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
font.weight: Font.Medium
}
DankLocationSearch {
id: locationSearchInput
width: parent.width
currentLocation: ""
placeholderText: I18n.tr("New York, NY")
keyNavigationBacktab: longitudeInput
onLocationSelected: (displayName, coordinates) => {
SettingsData.setWeatherLocation(displayName, coordinates)
const coords = coordinates.split(',')
if (coords.length >= 2) {
latitudeInput.text = coords[0].trim()
longitudeInput.text = coords[1].trim()
}
}
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
}
}

View File

@@ -65,6 +65,16 @@ Item {
checked)
}
}
DankToggle {
width: parent.width
text: I18n.tr("Window Scrolling")
description: "Scroll through windows, rather than workspaces"
checked: SettingsData.workspaceScrolling
visible: CompositorService.isNiri
onToggled: checked => {
return SettingsData.setWorkspaceScrolling(checked)
}
}
DankToggle {
width: parent.width
@@ -184,7 +194,126 @@ Item {
description: "Use animated wave progress bars for media playback"
checked: SettingsData.waveProgressEnabled
onToggled: checked => {
return SettingsData.setWaveProgressEnabled(checked)
return SettingsData.setWaveProgressEnabled(checked);
}
}
}
}
StyledRect {
width: parent.width
height: updaterSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0
Column {
id: updaterSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "refresh"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("System Updater")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
width: parent.width
text: I18n.tr("Use Custom Command")
description: I18n.tr("Use custom command for update your system")
checked: SettingsData.updaterUseCustomCommand
onToggled: checked => {
if (!checked) {
updaterCustomCommand.text = "";
updaterTerminalCustomClass.text = "";
SettingsData.setUpdaterCustomCommand("");
SettingsData.setUpdaterTerminalAdditionalParams("");
}
return SettingsData.setUpdaterUseCustomCommandEnabled(checked);
}
}
Column {
width: parent.width - Theme.spacingM * 2
spacing: Theme.spacingXS
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
StyledText {
text: I18n.tr("System update custom command")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: updaterCustomCommand
width: parent.width
height: 48
placeholderText: "myPkgMngr --sysupdate"
backgroundColor: Theme.surfaceVariant
normalBorderColor: Theme.primarySelected
focusedBorderColor: Theme.primary
Component.onCompleted: {
if (SettingsData.updaterCustomCommand) {
text = SettingsData.updaterCustomCommand;
}
}
onTextEdited: {
SettingsData.setUpdaterCustomCommand(text.trim());
}
}
}
Column {
width: parent.width - Theme.spacingM * 2
spacing: Theme.spacingXS
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
StyledText {
text: I18n.tr("Terminal custom additional parameters")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: updaterTerminalCustomClass
width: parent.width
height: 48
placeholderText: "-T udpClass"
backgroundColor: Theme.surfaceVariant
normalBorderColor: Theme.primarySelected
focusedBorderColor: Theme.primary
Component.onCompleted: {
if (SettingsData.updaterTerminalAdditionalParams) {
text = SettingsData.updaterTerminalAdditionalParams;
}
}
onTextEdited: {
SettingsData.setUpdaterTerminalAdditionalParams(text.trim());
}
}
}
}
@@ -396,6 +525,151 @@ Item {
}
}
}
StyledRect {
width: parent.width
height: notificationSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
Column {
id: notificationSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "notifications"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Notification Popups")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Column {
width: parent.width
spacing: 0
leftPadding: Theme.spacingM
rightPadding: Theme.spacingM
DankDropdown {
width: parent.width - parent.leftPadding - parent.rightPadding
text: I18n.tr("Popup Position")
description: I18n.tr("Choose where notification popups appear on screen")
currentValue: {
if (SettingsData.notificationPopupPosition === -1) {
return "Top Center"
}
switch (SettingsData.notificationPopupPosition) {
case SettingsData.Position.Top:
return "Top Right"
case SettingsData.Position.Bottom:
return "Bottom Left"
case SettingsData.Position.Left:
return "Top Left"
case SettingsData.Position.Right:
return "Bottom Right"
default:
return "Top Right"
}
}
options: ["Top Right", "Top Left", "Top Center", "Bottom Right", "Bottom Left"]
onValueChanged: value => {
switch (value) {
case "Top Right":
SettingsData.setNotificationPopupPosition(SettingsData.Position.Top)
break
case "Top Left":
SettingsData.setNotificationPopupPosition(SettingsData.Position.Left)
break
case "Top Center":
SettingsData.setNotificationPopupPosition(-1)
break
case "Bottom Right":
SettingsData.setNotificationPopupPosition(SettingsData.Position.Right)
break
case "Bottom Left":
SettingsData.setNotificationPopupPosition(SettingsData.Position.Bottom)
break
}
SettingsData.sendTestNotifications()
}
}
}
}
}
StyledRect {
width: parent.width
height: osdRow.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
Row {
id: osdRow
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
DankIcon {
name: "tune"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM - osdToggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("Always Show OSD Percentage")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Display volume and brightness percentage values by default in OSD popups")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
DankToggle {
id: osdToggle
anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.osdAlwaysShowValue
onToggleCompleted: checked => {
SettingsData.setOsdAlwaysShowValue(checked)
}
}
}
}
}
}
}

View File

@@ -22,6 +22,10 @@ DankPopout {
triggerScreen = screen;
}
Ref {
service: SystemUpdateService
}
popupWidth: 400
popupHeight: 500
triggerX: Screen.width - 600 - Theme.spacingL
@@ -216,7 +220,7 @@ DankPopout {
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 24 - Theme.spacingM
width: parent.width - Theme.spacingM
spacing: 2
StyledText {
@@ -258,14 +262,14 @@ DankPopout {
width: parent.width
height: 48
spacing: Theme.spacingM
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: parent.height
radius: Theme.cornerRadius
color: updateMouseArea.containsMouse ? Theme.primaryHover : Theme.secondaryHover
opacity: SystemUpdateService.updateCount > 0 ? 1.0 : 0.5
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
}
@@ -302,14 +306,14 @@ DankPopout {
}
}
}
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: parent.height
radius: Theme.cornerRadius
color: closeMouseArea.containsMouse ? Theme.errorPressed : Theme.secondaryHover
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
}
@@ -349,4 +353,4 @@ DankPopout {
}
}
}
}

View File

@@ -67,7 +67,7 @@ PanelWindow {
}
}
width: Math.min(900, messageText.implicitWidth + statusIcon.width + Theme.spacingM + (ToastService.hasDetails ? (expandButton.width + closeButton.width + 4) : 0) + Theme.spacingL * 2 + Theme.spacingM * 2)
width: shouldBeVisible ? Math.min(900, messageText.implicitWidth + statusIcon.width + Theme.spacingM + (ToastService.hasDetails ? (expandButton.width + closeButton.width + 4) : 0) + Theme.spacingL * 2 + Theme.spacingM * 2) : frozenWidth
height: toastContent.height + Theme.spacingL * 2
anchors.horizontalCenter: parent.horizontalCenter
y: Theme.barHeight - 4 + SettingsData.dankBarSpacing + 2
@@ -206,84 +206,121 @@ PanelWindow {
Rectangle {
width: parent.width
height: detailsText.height + Theme.spacingS * 2
height: detailsColumn.height + Theme.spacingS * 2
color: Qt.rgba(0, 0, 0, 0.2)
radius: Theme.cornerRadius / 2
visible: toast.expanded && ToastService.hasDetails
anchors.horizontalCenter: parent.horizontalCenter
StyledText {
id: detailsText
text: ToastService.currentDetails
font.pixelSize: Theme.fontSizeSmall
color: {
switch (ToastService.currentLevel) {
case ToastService.levelError:
case ToastService.levelWarn:
return SessionData.isLightMode ? Theme.surfaceText : Theme.background
default:
return Theme.surfaceText
}
}
isMonospace: true
Column {
id: detailsColumn
anchors.left: parent.left
anchors.right: copyButton.left
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Theme.spacingS
anchors.rightMargin: Theme.spacingS
wrapMode: Text.Wrap
}
DankActionButton {
id: copyButton
iconName: "content_copy"
iconSize: Theme.iconSizeSmall
iconColor: {
switch (ToastService.currentLevel) {
case ToastService.levelError:
case ToastService.levelWarn:
return SessionData.isLightMode ? Theme.surfaceText : Theme.background
default:
return Theme.surfaceText
}
}
buttonSize: Theme.iconSizeSmall + 8
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: Theme.spacingS
anchors.margins: Theme.spacingS
spacing: Theme.spacingS
property bool showTooltip: false
onClicked: {
Quickshell.execDetached(
["wl-copy", ToastService.currentDetails])
showTooltip = true
tooltipTimer.start()
}
Timer {
id: tooltipTimer
interval: 1500
onTriggered: copyButton.showTooltip = false
StyledText {
id: detailsText
text: ToastService.currentDetails
font.pixelSize: Theme.fontSizeSmall
color: {
switch (ToastService.currentLevel) {
case ToastService.levelError:
case ToastService.levelWarn:
return SessionData.isLightMode ? Theme.surfaceText : Theme.background
default:
return Theme.surfaceText
}
}
visible: ToastService.currentDetails.length > 0
width: parent.width - Theme.spacingS * 2
anchors.horizontalCenter: parent.horizontalCenter
wrapMode: Text.Wrap
}
Rectangle {
visible: copyButton.showTooltip
width: tooltipLabel.implicitWidth + 16
height: tooltipLabel.implicitHeight + 8
color: Theme.surfaceContainer
radius: Theme.cornerRadius
border.width: 1
border.color: Theme.outlineMedium
y: -height - 4
x: -width / 2 + copyButton.width / 2
width: parent.width - Theme.spacingS * 2
height: commandText.height + Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
color: Qt.rgba(0, 0, 0, 0.3)
radius: Theme.cornerRadius / 2
visible: ToastService.currentCommand.length > 0
StyledText {
id: tooltipLabel
anchors.centerIn: parent
text: root.copiedText
id: commandText
text: ToastService.currentCommand
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
color: {
switch (ToastService.currentLevel) {
case ToastService.levelError:
case ToastService.levelWarn:
return SessionData.isLightMode ? Theme.surfaceText : Theme.background
default:
return Theme.surfaceText
}
}
isMonospace: true
anchors.left: parent.left
anchors.right: copyButton.visible ? copyButton.left : parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Theme.spacingS / 2
anchors.rightMargin: Theme.spacingS / 2
wrapMode: Text.Wrap
}
DankActionButton {
id: copyButton
iconName: "content_copy"
iconSize: Theme.iconSizeSmall
iconColor: {
switch (ToastService.currentLevel) {
case ToastService.levelError:
case ToastService.levelWarn:
return SessionData.isLightMode ? Theme.surfaceText : Theme.background
default:
return Theme.surfaceText
}
}
buttonSize: Theme.iconSizeSmall + 8
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: Theme.spacingS / 2
visible: ToastService.currentCommand.length > 0
property bool showTooltip: false
onClicked: {
Quickshell.execDetached(["wl-copy", ToastService.currentCommand])
showTooltip = true
tooltipTimer.start()
}
Timer {
id: tooltipTimer
interval: 1500
onTriggered: copyButton.showTooltip = false
}
Rectangle {
visible: copyButton.showTooltip
width: tooltipLabel.implicitWidth + 16
height: tooltipLabel.implicitHeight + 8
color: Theme.surfaceContainer
radius: Theme.cornerRadius
border.width: 1
border.color: Theme.outlineMedium
y: -height - 4
x: -width / 2 + copyButton.width / 2
StyledText {
id: tooltipLabel
anchors.centerIn: parent
text: root.copiedText
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
}
}
}
}
}

View File

@@ -22,9 +22,9 @@ Item {
command: []
onExited: (code) => {
if (pendingSceneId !== "") {
const cacheHome = StandardPaths.writableLocation(StandardPaths.CacheLocation).toString()
const cacheHome = StandardPaths.writableLocation(StandardPaths.GenericCacheLocation).toString()
const baseDir = Paths.strip(cacheHome)
const outDir = baseDir + "/dankshell/we_screenshots"
const outDir = baseDir + "/DankMaterialShell/we_screenshots"
const outPath = outDir + "/" + pendingSceneId + ".jpg"
Quickshell.execDetached(["mkdir", "-p", outDir])

View File

@@ -4,8 +4,9 @@
"description": "Example plugin with Control Center detail dropdown",
"version": "1.0.0",
"author": "DankMaterialShell",
"icon": "settings",
"type": "widget",
"capabilities": ["control-center"],
"component": "./DetailExampleWidget.qml",
"icon": "settings",
"permissions": ["settings_read", "settings_write"]
}

View File

@@ -4,8 +4,9 @@
"description": "Example plugin with Control Center toggle widget",
"version": "1.0.0",
"author": "DankMaterialShell",
"icon": "toggle_on",
"type": "widget",
"capabilities": ["control-center"],
"component": "./ControlCenterExampleWidget.qml",
"icon": "toggle_on",
"permissions": ["settings_read", "settings_write"]
}

View File

@@ -4,8 +4,10 @@
"description": "Display cycling emojis in your bar with a handy emoji picker popout",
"version": "1.0.0",
"author": "AvengeMedia",
"icon": "mood",
"type": "widget",
"capabilities": ["emoji-search", "clipboard", "dankbar-widget"],
"component": "./EmojiWidget.qml",
"icon": "mood",
"settings": "./EmojiSettings.qml",
"permissions": [
"settings_read",

View File

@@ -4,8 +4,10 @@
"description": "Demonstrates dynamic variant creation for plugins",
"version": "1.0.0",
"author": "DMS",
"icon": "widgets",
"type": "widget",
"capabilities": ["multiple-usecases", "variants"],
"component": "./VariantWidget.qml",
"icon": "widgets",
"settings": "./VariantSettings.qml",
"permissions": [
"settings_read",

View File

@@ -0,0 +1,133 @@
import QtQuick
import Quickshell
import qs.Services
Item {
id: root
// Plugin properties
property var pluginService: null
property string trigger: "#"
// Plugin interface signals
signal itemsChanged()
Component.onCompleted: {
console.log("LauncherExample: Plugin loaded")
// Load custom trigger from settings
if (pluginService) {
trigger = pluginService.loadPluginData("launcherExample", "trigger", "#")
}
}
// Required function: Get items for launcher
function getItems(query) {
const baseItems = [
{
name: "Test Item 1",
icon: "lightbulb",
comment: "This is a test item that shows a toast notification",
action: "toast:Test Item 1 executed!",
categories: ["LauncherExample"]
},
{
name: "Test Item 2",
icon: "star",
comment: "Another test item with different action",
action: "toast:Test Item 2 clicked!",
categories: ["LauncherExample"]
},
{
name: "Test Item 3",
icon: "favorite",
comment: "Third test item for demonstration",
action: "toast:Test Item 3 activated!",
categories: ["LauncherExample"]
},
{
name: "Example Copy Action",
icon: "content_copy",
comment: "Demonstrates copying text to clipboard",
action: "copy:This text was copied by the launcher plugin!",
categories: ["LauncherExample"]
},
{
name: "Example Script Action",
icon: "terminal",
comment: "Demonstrates running a simple command",
action: "script:echo 'Hello from launcher plugin!'",
categories: ["LauncherExample"]
}
]
if (!query || query.length === 0) {
return baseItems
}
// Filter items based on query
const lowerQuery = query.toLowerCase()
return baseItems.filter(item => {
return item.name.toLowerCase().includes(lowerQuery) ||
item.comment.toLowerCase().includes(lowerQuery)
})
}
// Required function: Execute item action
function executeItem(item) {
if (!item || !item.action) {
console.warn("LauncherExample: Invalid item or action")
return
}
console.log("LauncherExample: Executing item:", item.name, "with action:", item.action)
const actionParts = item.action.split(":")
const actionType = actionParts[0]
const actionData = actionParts.slice(1).join(":")
switch (actionType) {
case "toast":
showToast(actionData)
break
case "copy":
copyToClipboard(actionData)
break
case "script":
runScript(actionData)
break
default:
console.warn("LauncherExample: Unknown action type:", actionType)
showToast("Unknown action: " + actionType)
}
}
// Helper functions for different action types
function showToast(message) {
if (typeof ToastService !== "undefined") {
ToastService.showInfo("LauncherExample", message)
} else {
console.log("LauncherExample Toast:", message)
}
}
function copyToClipboard(text) {
Quickshell.execDetached(["sh", "-c", "echo -n '" + text + "' | wl-copy"])
showToast("Copied to clipboard: " + text)
}
function runScript(command) {
console.log("LauncherExample: Would run script:", command)
showToast("Script executed: " + command)
// In a real plugin, you might create a Process component here
// For demo purposes, we just show what would happen
}
// Watch for trigger changes
onTriggerChanged: {
if (pluginService) {
pluginService.savePluginData("launcherExample", "trigger", trigger)
}
}
}

View File

@@ -0,0 +1,244 @@
import QtQuick
import QtQuick.Controls
import qs.Widgets
FocusScope {
id: root
property var pluginService: null
implicitHeight: settingsColumn.implicitHeight
height: implicitHeight
Column {
id: settingsColumn
anchors.fill: parent
anchors.margins: 16
spacing: 16
Text {
text: "Launcher Example Plugin Settings"
font.pixelSize: 18
font.weight: Font.Bold
color: "#FFFFFF"
}
Text {
text: "This plugin demonstrates the launcher plugin system with example items and actions."
font.pixelSize: 14
color: "#CCFFFFFF"
wrapMode: Text.WordWrap
width: parent.width - 32
}
Rectangle {
width: parent.width - 32
height: 1
color: "#30FFFFFF"
}
Column {
spacing: 12
width: parent.width - 32
Text {
text: "Trigger Configuration"
font.pixelSize: 16
font.weight: Font.Medium
color: "#FFFFFF"
}
Text {
text: noTriggerToggle.checked ? "Items will always show in the launcher (no trigger needed)." : "Set the trigger text to activate this plugin. Type the trigger in the launcher to filter to this plugin's items."
font.pixelSize: 12
color: "#CCFFFFFF"
wrapMode: Text.WordWrap
width: parent.width
}
Row {
spacing: 12
CheckBox {
id: noTriggerToggle
text: "No trigger (always show)"
checked: loadSettings("noTrigger", false)
contentItem: Text {
text: noTriggerToggle.text
font.pixelSize: 14
color: "#FFFFFF"
leftPadding: noTriggerToggle.indicator.width + 8
verticalAlignment: Text.AlignVCenter
}
indicator: Rectangle {
implicitWidth: 20
implicitHeight: 20
radius: 4
border.color: noTriggerToggle.checked ? "#4CAF50" : "#60FFFFFF"
border.width: 2
color: noTriggerToggle.checked ? "#4CAF50" : "transparent"
Rectangle {
width: 12
height: 12
anchors.centerIn: parent
radius: 2
color: "#FFFFFF"
visible: noTriggerToggle.checked
}
}
onCheckedChanged: {
saveSettings("noTrigger", checked)
if (checked) {
saveSettings("trigger", "")
} else {
saveSettings("trigger", triggerField.text || "#")
}
}
}
}
Row {
spacing: 12
anchors.left: parent.left
anchors.right: parent.right
visible: !noTriggerToggle.checked
Text {
text: "Trigger:"
font.pixelSize: 14
color: "#FFFFFF"
anchors.verticalCenter: parent.verticalCenter
}
DankTextField {
id: triggerField
width: 100
height: 40
text: loadSettings("trigger", "#")
placeholderText: "#"
backgroundColor: "#30FFFFFF"
textColor: "#FFFFFF"
onTextEdited: {
const newTrigger = text.trim()
saveSettings("trigger", newTrigger || "#")
saveSettings("noTrigger", newTrigger === "")
}
}
Text {
text: "Examples: #, !, @, !ex, etc."
font.pixelSize: 12
color: "#AAFFFFFF"
anchors.verticalCenter: parent.verticalCenter
}
}
}
Rectangle {
width: parent.width - 32
height: 1
color: "#30FFFFFF"
}
Column {
spacing: 8
width: parent.width - 32
Text {
text: "Example Items:"
font.pixelSize: 14
font.weight: Font.Medium
color: "#FFFFFF"
}
Column {
spacing: 4
leftPadding: 16
Text {
text: "• Test Item 1, 2, 3 - Show toast notifications"
font.pixelSize: 12
color: "#CCFFFFFF"
}
Text {
text: "• Example Copy Action - Copy text to clipboard"
font.pixelSize: 12
color: "#CCFFFFFF"
}
Text {
text: "• Example Script Action - Demonstrate script execution"
font.pixelSize: 12
color: "#CCFFFFFF"
}
}
}
Rectangle {
width: parent.width - 32
height: 1
color: "#30FFFFFF"
}
Column {
spacing: 8
width: parent.width - 32
Text {
text: "Usage:"
font.pixelSize: 14
font.weight: Font.Medium
color: "#FFFFFF"
}
Column {
spacing: 4
leftPadding: 16
bottomPadding: 24
Text {
text: "1. Open Launcher (Ctrl+Space or click launcher button)"
font.pixelSize: 12
color: "#CCFFFFFF"
}
Text {
text: noTriggerToggle.checked ? "2. Items are always visible in the launcher" : "2. Type your trigger (default: #) to filter to this plugin"
font.pixelSize: 12
color: "#CCFFFFFF"
}
Text {
text: noTriggerToggle.checked ? "3. Search works normally with plugin items included" : "3. Optionally add search terms: '# test' to find test items"
font.pixelSize: 12
color: "#CCFFFFFF"
}
Text {
text: "4. Select an item and press Enter to execute its action"
font.pixelSize: 12
color: "#CCFFFFFF"
}
}
}
}
function saveSettings(key, value) {
if (pluginService) {
pluginService.savePluginData("launcherExample", key, value)
}
}
function loadSettings(key, defaultValue) {
if (pluginService) {
return pluginService.loadPluginData("launcherExample", key, defaultValue)
}
return defaultValue
}
}

View File

@@ -0,0 +1,206 @@
# LauncherExample Plugin
A demonstration plugin that showcases the DMS launcher plugin system capabilities.
## Purpose
This plugin serves as a comprehensive example for developers creating launcher plugins for DMS. It demonstrates:
- **Plugin Structure**: Proper manifest, launcher, and settings components
- **Trigger System**: Customizable trigger strings for plugin activation (including empty triggers)
- **Item Management**: Providing searchable items to the launcher
- **Action Execution**: Handling different types of actions (toast, copy, script)
- **Settings Integration**: Configurable plugin settings with persistence
## Features
### Example Items
- **Test Items 1-3**: Demonstrate toast notifications
- **Copy Action**: Shows clipboard integration
- **Script Action**: Demonstrates command execution
### Trigger System
- **Default Trigger**: `#` (configurable in settings)
- **Empty Trigger Option**: Items can always be visible without needing a trigger
- **Usage**: Type `#` in launcher to filter to this plugin (when trigger is set)
- **Search**: Type `# test` to search within plugin items
### Action Types
- `toast:message` - Shows toast notification
- `copy:text` - Copies text to clipboard
- `script:command` - Executes shell command (demo only)
## File Structure
```
PLUGINS/LauncherExample/
├── plugin.json # Plugin manifest
├── LauncherExampleLauncher.qml # Main launcher component
├── LauncherExampleSettings.qml # Settings interface
└── README.md # This documentation
```
## Installation
1. **Plugin Directory**: Copy to `~/.config/DankMaterialShell/plugins/LauncherExample`
2. **Enable Plugin**: Settings → Plugins → Enable "LauncherExample"
3. **Configure**: Set custom trigger in plugin settings if desired
## Usage
### With Trigger (Default)
1. Open launcher (Ctrl+Space or launcher button)
2. Type `#` to activate plugin trigger
3. Browse available items or add search terms
4. Press Enter to execute selected item
### Without Trigger (Empty Trigger Mode)
1. Enable "No trigger (always show)" in plugin settings
2. Open launcher - plugin items are always visible
3. Search works normally with plugin items included
4. Press Enter to execute selected item
### Search Examples
- `#` - Show all plugin items (with trigger enabled)
- `# test` - Show items matching "test"
- `# copy` - Show items matching "copy"
- `test` - Show all items matching "test" (with empty trigger enabled)
## Developer Guide
### Plugin Contract
**Launcher Component Requirements**:
```qml
// Required properties
property var pluginService: null
property string trigger: "#"
// Required signals
signal itemsChanged()
// Required functions
function getItems(query): array
function executeItem(item): void
```
**Item Structure**:
```javascript
{
name: "Item Name", // Display name
icon: "icon_name", // Material icon
comment: "Description", // Subtitle text
action: "type:data", // Action to execute
categories: ["PluginName"] // Category array
}
```
**Action Format**: `type:data` where:
- `type` - Action handler (toast, copy, script, etc.)
- `data` - Action-specific data
### Settings Integration
```qml
// Save setting
pluginService.savePluginData("pluginId", "key", value)
// Load setting
pluginService.loadPluginData("pluginId", "key", defaultValue)
```
### Trigger Configuration
The trigger can be configured in two ways:
1. **Empty Trigger** (No Trigger Mode):
- Check "No trigger (always show)" in settings
- Saves `trigger: ""` and `noTrigger: true`
- Items always appear in launcher alongside regular apps
2. **Custom Trigger**:
- Enter any string (e.g., `#`, `!`, `@`, `!ex`)
- Uncheck "No trigger" checkbox
- Items only appear when trigger is typed
### Manifest Structure
```json
{
"id": "launcherExample",
"name": "LauncherExample",
"type": "launcher",
"capabilities": ["launcher"],
"component": "./LauncherExampleLauncher.qml",
"settings": "./LauncherExampleSettings.qml",
"permissions": ["settings_read", "settings_write"]
}
```
Note: The `trigger` field in the manifest is optional and serves as the default trigger value.
## Extending This Plugin
### Adding New Items
```qml
function getItems(query) {
return [
{
name: "My Item",
icon: "custom_icon",
comment: "Does something cool",
action: "custom:action_data",
categories: ["LauncherExample"]
}
]
}
```
### Adding New Actions
```qml
function executeItem(item) {
const actionParts = item.action.split(":")
const actionType = actionParts[0]
const actionData = actionParts.slice(1).join(":")
switch (actionType) {
case "custom":
handleCustomAction(actionData)
break
}
}
```
### Custom Trigger Logic
```qml
Component.onCompleted: {
if (pluginService) {
trigger = pluginService.loadPluginData("launcherExample", "trigger", "#")
}
}
onTriggerChanged: {
if (pluginService) {
pluginService.savePluginData("launcherExample", "trigger", trigger)
}
}
```
## Best Practices
1. **Unique Triggers**: Choose triggers that don't conflict with other plugins
2. **Clear Descriptions**: Write helpful item comments
3. **Error Handling**: Gracefully handle action failures
4. **Performance**: Return results quickly in getItems()
5. **Cleanup**: Destroy temporary objects in executeItem()
6. **Empty Trigger Support**: Consider if your plugin should support empty trigger mode
## Testing
Test the plugin by:
1. Installing and enabling in DMS
2. Testing with trigger enabled
3. Testing with empty trigger (no trigger mode)
4. Trying each action type
5. Testing search functionality
6. Verifying settings persistence
This plugin provides a solid foundation for building more sophisticated launcher plugins with custom functionality!

View File

@@ -0,0 +1,17 @@
{
"id": "launcherExample",
"name": "LauncherExample",
"description": "Example launcher plugin demonstrating the launcher plugin system",
"version": "1.0.0",
"author": "DMS Team",
"icon": "extension",
"type": "launcher",
"capabilities": ["clipboard", "command-execution"],
"component": "./LauncherExampleLauncher.qml",
"settings": "./LauncherExampleSettings.qml",
"trigger": "#",
"permissions": [
"settings_read",
"settings_write"
]
}

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