1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-08-02 11:38:30 -04:00

Compare commits

..

1173 Commits

Author SHA1 Message Date
bbedward c12eafa1db Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-10-08 23:18:07 -04:00
bbedward 9e26d8755c Add os keyboard to greeter 2025-10-08 23:17:52 -04:00
Lukas Krejci 90bd30e351 add support for system updates on fedora (#353) 2025-10-08 22:19:40 -04:00
bbedward 3fb5d5c4f3 i18n: don't load en json - move animated terms to properties 2025-10-08 20:31:41 -04:00
bbedward 9f3685e4d5 clip spotlight result listview 2025-10-08 18:58:14 -04:00
bbedward 6565988952 Cleanup some of the backwards compat crap 2025-10-08 18:28:46 -04:00
bbedward 10b7a0875b Crop CircularImages not fit 2025-10-08 18:16:16 -04:00
bbedward bb4b9f1a58 Only ignore left/right keys in grid launcher mode 2025-10-08 17:55:24 -04:00
bbedward 981e527560 Remove artifacts of search fields overriding key inputs 2025-10-08 17:36:12 -04:00
bbedward 80b2ee719c Restore ipc target 2025-10-08 17:31:26 -04:00
bbedward 6103d6196f override cachyos logo 2025-10-08 16:35:49 -04:00
bbedward ed1a5bfded i18n: Add japanese + i18n service 2025-10-08 16:25:06 -04:00
bbedward 3909ce3350 Remove minimum menu size constraint 2025-10-08 15:23:09 -04:00
bbedward d64cd0b8a4 Fix launcher button mask 2025-10-08 15:12:47 -04:00
bbedward 676aa9f93f Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-10-08 14:51:59 -04:00
bbedward ebd48e2556 Many more logo customization options 2025-10-08 14:51:38 -04:00
bokicoder ad5871aae4 Update flake.lock (#347) 2025-10-08 14:15:38 -04:00
bbedward e327b1ca5b Add proxy service for legacy nmcli version 2025-10-08 14:00:51 -04:00
bbedward ad43ca11eb Insta fallback 2025-10-08 13:41:37 -04:00
omar 41ba76e2e2 Change lock activation to use lockLoader (#346)
Fixes broken lock button
2025-10-08 13:40:27 -04:00
bbedward 15e773434e Portal/Loginctl fallbacks for DMS_SOCKET unavail 2025-10-08 13:37:03 -04:00
bbedward ed118f4e7a Dropbox icon workaround 2025-10-08 12:54:06 -04:00
bbedward 3c420f2e30 Remove useless getter 2025-10-08 12:33:00 -04:00
bbedward 4e271d4f0e Fix search when appusage is unavailable 2025-10-08 12:07:46 -04:00
bbedward 27f9b3cd0b native NetworkManager + all native dbus bindings via dms
- Scrap janky NetworkService in favor of, dms' native NM integration
  socket
- Scrap all gdbus usage in favor of native dbus bindings in dms
  (loginctl, freedesktop)

It means that - some features won't work if running without dms wrapper.

But the trade off is certainly worth it, in the long-run for efficiency
improvements.
2025-10-08 12:03:50 -04:00
bbedward 1ed4abd347 Attempts to improve startup time 2025-10-08 10:02:54 -04:00
bbedward f71dd1ed54 No DankIcon in notifications 2025-10-08 09:05:33 -04:00
bokicoder 19828d3b06 fix preStart script for greeter (#339) 2025-10-08 08:12:41 -04:00
bokicoder d242e729f0 fix default-settings.json and default-session.json (#341) 2025-10-08 08:11:55 -04:00
Mirza Esaaf Shuja 8cd0d5faa5 fix: use correct hyprland config template for greeter (#338) 2025-10-07 20:48:27 -04:00
bbedward a741d892a9 Remove MultiEffect opacity 2025-10-07 16:01:05 -04:00
bbedward 9add3361e0 DankModal pixel snapping 2025-10-07 15:32:21 -04:00
bbedward 4aac70ab5f pass config_dir as an argument to matugen-worker 2025-10-07 13:46:41 -04:00
bbedward 980aec714a Fix typo in workspace mousearea 2025-10-07 13:39:53 -04:00
bbedward af8ee5af0f Switch to dispatch-style release workflow 2025-10-07 12:50:09 -04:00
bbedward 32d9aa0cf2 Separate bar font scale 2025-10-07 08:29:48 -04:00
bbedward 7e49631912 Migrate plugin settings to separate file 2025-10-07 08:22:21 -04:00
bbedward 43970d34aa Fix keyboard navi in file browser 2025-10-07 00:14:56 -04:00
bbedward abb3c40697 Thread font loading 2025-10-06 23:51:49 -04:00
bbedward 2757a41102 Add invert on mode change for launcher logo 2025-10-06 23:24:39 -04:00
bbedward 1f8bddaa5e Fix dank bar on overview clicks 2025-10-06 22:35:30 -04:00
bbedward 4c3b7ca60f Support prime-run 2025-10-06 20:50:36 -04:00
bbedward d9d83e5767 spotlight: remove categories 2025-10-06 20:42:21 -04:00
bbedward b4ebde47be Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-10-06 20:40:20 -04:00
bbedward ef9f76190d Implement sizing mechanism for bar widget content 2025-10-06 20:40:02 -04:00
Nek 71b96efca0 Allow user's matugen config to be ran by dms (#328)
* Allow user's matugen config to be ran by dms

matugen-workers.sh now also loads the configs and templates from `~/.config/matugen/dms` and handles them the same way dms does its own matugen configs.

* Do not auto create user's matugen directories
2025-10-06 20:33:44 -04:00
bbedward 7158e09b0e Customizable launcher logo 2025-10-06 20:08:21 -04:00
bbedward 8ef125bed2 Fix toast width 2025-10-06 19:42:22 -04:00
bbedward 0b11fb2fd5 Fix variant plugins in center section 2025-10-06 17:57:01 -04:00
bbedward 3871f3cf3d Aggresive focus restoration 2025-10-06 17:52:48 -04:00
bbedward 7c5d1ec0f6 launcher + dock menu improvements 2025-10-06 17:46:19 -04:00
bbedward b507b08e34 Re-org niri service & handle reconnects to socket 2025-10-06 16:46:05 -04:00
bbedward 1b06090f72 Remove terms that shouldnt be localized 2025-10-06 16:06:19 -04:00
bbedward 5460c20ac3 Localization framework 2025-10-06 16:00:50 -04:00
bbedward 2ccec607a0 Hide third party plugins, by default 2025-10-06 15:36:33 -04:00
bbedward 8a99fcf188 Set exec working directory to home when not defined 2025-10-06 15:28:58 -04:00
bbedward 1e2489ca76 Add support for plugin registry + install + management 2025-10-06 15:26:44 -04:00
bbedward 89793d2d62 plugins: support for multiple widgets per-plugin (variants) 2025-10-06 12:33:58 -04:00
bbedward 11a1af89f4 Allow removing force-padding on monitor widgets + plugin load fixes 2025-10-06 11:32:25 -04:00
bbedward e24ddb804d Filter out scratch_term ws on hyprland 2025-10-06 09:38:10 -04:00
bbedward 3524d365be Add Desktop Actions to launcher + dock 2025-10-06 09:35:50 -04:00
bbedward 2b3b9d037c re-add transitions 2025-10-06 09:10:58 -04:00
Bruno Cesar Rocha 5140cd9d7f fix: use correct icon on CPU temp on bar (#327) 2025-10-06 09:05:44 -04:00
Parthiv Seetharaman 2df9437b39 add nixos support for greeter (#298)
* add nixos support for greeter

* fix greeter config file access

* fix wallpaper perms and allow for adding extra compositor config

* fix greeter config files ownership

* set default for compositor.extraConfig

* update option docs about copying instead of symlinking

* explain configHome in doc further

* add nixos option to redirect greeter logs

* prevent possible errors in greetd preStart
2025-10-06 08:42:36 -04:00
purian23 db440b8a14 feat: Add a 1px border to Dankbar w/edge detection
-  No edge gap (spacing = 0 AND corner radius = 0): Draws 1px border only on the exposed edge (bottom for top bar, top for bottom bar, right for left bar, left for right bar)
- Has edge gap (spacing > 0 OR corner radius > 0): Draws 1px border around all sides
2025-10-05 23:56:40 -04:00
purian23 c3dd70bc99 fix: Color selection state 2025-10-05 23:21:00 -04:00
purian23 223e783bbc Update Notepad theme corner radius to match user settings 2025-10-05 22:43:49 -04:00
purian23 ca086dbf16 ColorPicker visual feedback upon invaid hex code 2025-10-05 22:35:27 -04:00
purian23 523422cf6c feat: Rebuilt DankColorPicker w/Color Sampling
- Fully custom built ColorDialog
- Replaces previous Wallpaper background color tool
- Requires hyprpicker for color sampling, thanks @Vaxry
2025-10-05 22:18:26 -04:00
bbedward 2dc310dcbc Add VPN widget for control center 2025-10-05 21:28:52 -04:00
bbedward c092cd2921 plugins: support control center plugins 2025-10-05 21:09:29 -04:00
bbedward 2b14ef76c9 resolve dms-colors dir dynamically in matugen-worker 2025-10-05 19:22:17 -04:00
bbedward fbbf10078f Fix focus issues with add widget dialog 2025-10-05 19:17:11 -04:00
Abhinav Chalise 2315d423c4 remove signal strength from ssidName (#323) 2025-10-05 19:01:03 -04:00
bbedward 9a43465ebf Fix initial light/dark mode state in cc 2025-10-05 15:28:46 -04:00
bbedward fc1444763d Update dms-colors matugen 2025-10-05 15:07:08 -04:00
purian23 804bf879ed Default to Zenity Color picker
- QT 6.9.3 removed the previous color picker we used
- Zenity is default in gnome based distros
- Kcolorchooser can be installed separately and will be preferred over Zenity
2025-10-04 22:59:02 -04:00
purian23 ad44f09421 feat: Display persistent OSD percentage option 2025-10-04 22:02:25 -04:00
purian23 df2469468b Refactor Notepad search 2025-10-04 14:38:30 -04:00
bbedward f8f4fe11eb Explicit null 2025-10-04 12:10:03 -04:00
bbedward 039a370add Explicit battery override 2025-10-04 12:07:59 -04:00
bbedward 6feebc086d Update readme 2025-10-04 09:47:28 -04:00
bbedward bc335c7d72 Remove arbitrary height limit on notification settings 2025-10-04 08:46:35 -04:00
bbedward a6dd7254b2 Clip system tab flickable 2025-10-04 08:40:17 -04:00
bbedward 7b1026c624 Include env to override battery device 2025-10-04 08:23:49 -04:00
bbedward 4758393cc1 Fix dock hide with padding 2025-10-04 08:17:41 -04:00
bbedward 52373a3a7d Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-10-04 08:12:51 -04:00
bbedward c30f9a2841 use wayland idle-inhibit when available 2025-10-04 08:12:18 -04:00
Abhinav Chalise 44d6f8f15c Fix toggle function to return correct theme mode (#309) 2025-10-04 07:36:23 -04:00
purian23 5ada12f989 feat: Add Notepad search function 2025-10-04 02:25:42 -04:00
bbedward d213045168 default instead of prefer-light 2025-10-04 01:18:43 -04:00
bbedward d83478239e plugins: add pillClickAction + PopoutService 2025-10-04 01:12:17 -04:00
purian23 3869955357 Update Notepad entry text color 2025-10-04 00:50:47 -04:00
bbedward 345d37edf8 Shift urgent workspace color 2025-10-04 00:11:49 -04:00
bbedward 2788ef28cf Update README 2025-10-03 23:21:46 -04:00
bbedward 0d5c1bb3df Add "daemon" type of plugins 2025-10-03 22:55:07 -04:00
bbedward c3d505cdad better proc usage 2025-10-03 19:35:13 -04:00
bbedward 90854e1dd4 version in about tab and ci 2025-10-03 19:31:07 -04:00
bbedward f96e3b04be Disable powermenu bg on cc 2025-10-03 18:29:06 -04:00
bbedward 44449e26a0 Handle urgent workspaces 2025-10-03 18:17:24 -04:00
bbedward ddc88fd360 Fix positioning of power menu 2025-10-03 17:21:43 -04:00
bbedward fedec450cb Keep the modal, but relatively positioned 2025-10-03 17:16:34 -04:00
bbedward 04ea742830 Reapply "Always use power menu modal"
This reverts commit 5a5c860cef.
2025-10-03 17:14:43 -04:00
bbedward 5a5c860cef Revert "Always use power menu modal"
This reverts commit 55d06a43f8.

mm, not sure how I feel about it
2025-10-03 17:14:08 -04:00
bbedward 55d06a43f8 Always use power menu modal 2025-10-03 16:54:10 -04:00
bbedward 71eecd6e7b Revert "betterbird/thunderbird matugen template"
This reverts commit 6f3019f84b.
2025-10-03 16:20:21 -04:00
bbedward 6f3019f84b betterbird/thunderbird matugen template 2025-10-03 16:16:47 -04:00
bbedward e95d3126b2 Modal/Popout layout alterations 2025-10-03 15:15:44 -04:00
bbedward 5da265bf0b Instruct fonts to be global (makes sense for greeter) 2025-10-03 13:52:39 -04:00
bbedward 2ce9c43b8c disable layer debug opt 2025-10-03 11:21:43 -04:00
bbedward 740b2f206c Alter loading behavior 2025-10-03 10:08:05 -04:00
bbedward af622bc7e7 Also concat local ones 2025-10-03 08:51:41 -04:00
bbedward 7816b50b27 Fallback greeter directories 2025-10-03 08:42:55 -04:00
bbedward 4cb7a909f7 Set default instead of prefer-light 2025-10-03 08:39:02 -04:00
bbedward 0fac88e171 namespace for notepad 2025-10-03 08:30:43 -04:00
bbedward b4ab9d9650 XDG_DATA_DIRS for greeter 2025-10-02 22:10:46 -04:00
bbedward 731db13c14 Fix bindings in center section & icon sizes 2025-10-02 21:41:24 -04:00
bbedward 414a1ad4d2 Tweak colors a bit 2025-10-02 20:15:20 -04:00
Bruno Cesar Rocha 16055fe96e fix: Plugin settings not loading existing settings (#294)
I notice the plugin settings tab was not loading the
existing plugin settings from the settings file.

It was properly writting to the file, but not reloading
on initialization.

added a loadValue() function so PluginSettings can call when the
pluginService is injected. Added isLoading state flag to prevent
the onItemsChanged from saving back settings when loading is happening.
Added null wise checks for properties that may be not ready yet
when loading.
2025-10-02 14:24:28 -04:00
bbedward 6140c398f0 Remove useless rounding 2025-10-02 14:14:16 -04:00
bbedward bd02923616 plugin readme update 2025-10-02 14:01:20 -04:00
bbedward 6021815fd3 Fix media player when on right edge 2025-10-02 13:49:53 -04:00
bbedward 8c4aba5479 Center add widget in cc 2025-10-02 13:45:52 -04:00
bbedward 2428b22171 Score usage data into app search 2025-10-02 13:42:05 -04:00
bbedward a3d30211f6 Fix missing screen info 2025-10-02 12:57:43 -04:00
bbedward 730300d211 de-dupe the pills 2025-10-02 12:48:21 -04:00
bbedward aaca31276b shift clock and date weights in vertical mode 2025-10-02 12:44:21 -04:00
bbedward 53fb927e36 niri: color and layout config generation 2025-10-02 12:34:17 -04:00
bbedward fb5aa0313e cleanup debug logs, fix center section plugins 2025-10-02 12:24:45 -04:00
bbedward 9b41eecbf1 Fix reactivity, different settings structure, etc, etc. 2025-10-02 12:13:49 -04:00
bbedward ae461b1caf Merge branch 'master' of github.com:bbedward/DankMaterialShell into wip/plugins 2025-10-02 00:22:32 -04:00
bbedward 57e36d6710 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-10-02 00:13:43 -04:00
bbedward a7c4f09c5b always blockLoading on fileview 2025-10-02 00:13:29 -04:00
bbedward 554ef16e49 moar 2025-10-01 23:37:03 -04:00
bbedward 082321f860 de-dupe env 2025-10-01 22:13:03 -04:00
bbedward df4f7b8c9e Set XDG_SESSION_TYPE in greeter
Always wayland, which is probably fine for our use case. Fixes gnome
2025-10-01 22:10:29 -04:00
bbedward 3f1742f074 lock+greeter: show keyboard layout widget, spacing adjustments 2025-10-01 21:08:55 -04:00
bbedward 4560d5c2d5 Fix overview auto hide bar 2025-10-01 18:16:27 -04:00
bbedward 0ca12d275c Abstract away plugin dev a little more 2025-10-01 17:47:39 -04:00
bbedward df9e834309 Some consistent styling for plugins 2025-10-01 14:04:17 -04:00
bbedward ab1c0bb129 Merge branch 'master' of github.com:bbedward/DankMaterialShell into wip/plugins 2025-10-01 13:38:49 -04:00
bbedward 5070e4c950 Cleanup imports 2025-10-01 13:09:43 -04:00
bbedward c13526ccad re-enable layer 2025-10-01 13:05:28 -04:00
bbedward 46e16a6c69 Greetd: Add a greeter 2025-10-01 13:04:48 -04:00
Bruno Cesar Rocha 53983933dc feat: Plugin System (#276)
* feat: Plugin System

* fix: merge conflicts
2025-10-01 11:28:10 -04:00
purian23 09f3ca39a1 feat: Top/Left/Right/Bottom Dock positioning 2025-09-30 21:56:58 -04:00
bbedward 42b4c91f35 update readme 2025-09-30 18:10:57 -04:00
bbedward 37120776be configurable animation speeds 2025-09-30 17:30:03 -04:00
bbedward d67bcb66c9 Keep SpotlightModal always loaded.
Tbh, probably minimal impact on memory - makes app launching snappier.
2025-09-30 17:07:57 -04:00
bbedward acf6e72f35 emacs/vim style navigation everywhere 2025-09-30 16:54:50 -04:00
Alex Birdsall 0964b271f5 Add emacs-/readline- and vim-style list navigation to app launcher (#278)
* Add emacs- and vim-style nav keys to appLauncher

* Minor typo fix in contributing docs
2025-09-30 16:46:37 -04:00
bbedward 1f7998fc44 Disable scale from DankModal 2025-09-30 16:45:30 -04:00
bbedward 7ac59f7bd0 Revert antialiasing addition 2025-09-30 16:43:04 -04:00
xdenotte bf238640af fix(popouts): disable layer compositing and scale to keep text sharp at 1.25 (#277) 2025-09-30 16:42:39 -04:00
bbedward 6e638cadb7 slightly thinner workspace pills + more forgiving mousearea 2025-09-30 15:49:13 -04:00
bbedward ab0759f441 antialiasing: true on rectangles 2025-09-30 13:28:07 -04:00
bbedward 123ec5c78a Flip text to native rendering 2025-09-30 13:17:09 -04:00
bbedward d262c67832 Fix VPN tooltip 2025-09-30 13:06:21 -04:00
bbedward fcb9a2838d Disable VPN icon rotation 2025-09-30 12:59:10 -04:00
bbedward 82d1672741 search field focus only 2025-09-30 12:36:58 -04:00
bbedward 657a8b6094 Per light-mode/dark-mode wallpaper option 2025-09-30 12:29:08 -04:00
bbedward 165f7e0720 fix exit anim 2025-09-30 11:05:15 -04:00
bbedward 91aba84c29 Tie modal loader to shouldBeVisible 2025-09-30 11:01:51 -04:00
bbedward 02c4ac1bc7 fix bar radius 2025-09-30 10:56:18 -04:00
bbedward c529959027 Tie volume OSD to dbus event 2025-09-30 10:11:35 -04:00
bbedward e875d1a5d7 meta: Vertical Bar, Notification Popup Position Options, ++
- CC Color picker widget
- Tooltips in more places
- Attempt to improve niri screen transitiosn
2025-09-30 09:51:18 -04:00
bbedward d280505b9f Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-09-29 16:50:24 -04:00
bbedward fdd9d0000b Dont do dumb mono font filtering 2025-09-29 16:50:13 -04:00
purian23 74c793eedf fix: Notepad refactor w/Dual displays 2025-09-29 14:38:58 -04:00
purian23 bd8976c620 feat: Docks refactor - Top/Bottom options 2025-09-28 23:55:59 -04:00
purian23 a3e2563f9b Smoother notepad closure animation 2025-09-28 22:41:15 -04:00
bbedward a310e3d8fa Fix contrast of monochrome theme 2025-09-28 13:26:18 -04:00
purian23 ae9da35af1 fix: Dank Bar per/Display options 2025-09-27 21:52:06 -04:00
bbedward b45837ff5c Tab and backtab keyboard navigation on supported content 2025-09-27 09:08:41 -04:00
Parthiv Seetharaman 3dd9ab8a29 add option to always show dock in overview (#257) 2025-09-27 08:55:21 -04:00
bbedward 5ebd2f6b8e Smaller top bar auto hide mask 2025-09-27 08:53:28 -04:00
purian23 977043ac92 feat: Long Live the DankBar > Top/Bottom positioning 2025-09-26 23:45:19 -04:00
bbedward 4c6182b79c Allow 0 spacing 2025-09-26 13:49:00 -04:00
bbedward 01125e092c Don't allow popouts to go off screen 2025-09-26 13:47:40 -04:00
bbedward 99ef447a52 Restore bluetooth audio codec functionality 2025-09-26 12:37:10 -04:00
purian23 4722ff9b97 Merge pull request #255 from sezaru/fix_nix_documentation
doc: Fixes DMS input URL for nix flake
2025-09-26 12:21:26 -04:00
Eduardo B. A. 23a19df79a doc: Fixes DMS input url for nix flake 2025-09-26 13:10:10 -03:00
Eduardo B. A. 97b86c6faa Optimize Nix Flake for Niri / Hyprland (#242)
* feat: Make niri stuff optional

* doc: Add some documentation about the flake

* Simplify modules.

* Overlay pkgs and simplify modules.

* Fix niri config levels.

* Fix documentation.

* Just pass the packages to the module

---------

Co-authored-by: Eduardo Barreto Alexandre <blibs@blobs.com>
Co-authored-by: Luis González <5774777+luis-agm@users.noreply.github.com>
2025-09-26 10:44:41 -04:00
bbedward e6296f20b9 default opaque 2025-09-26 00:25:15 -04:00
bbedward 0ee9dcc255 Revise catpuccin palettes 2025-09-26 00:16:34 -04:00
bbedward c7b4e2c49d wrap shaders in a loader 2025-09-25 23:33:57 -04:00
bbedward 066d1847e4 cc: fixes to edit mode 2025-09-25 21:37:32 -04:00
purian23 3155bf24c1 Revert "refactor: Add Drag & Drop to Control Center"
This reverts commit a207ed74ff.
2025-09-25 20:59:22 -04:00
purian23 92bb5b90aa Merge branch 'master' of github.com:AvengeMedia/DankMaterialShell 2025-09-25 19:32:42 -04:00
purian23 a207ed74ff refactor: Add Drag & Drop to Control Center 2025-09-25 19:29:36 -04:00
bbedward 9d224113c4 use a matugen template for dms-colors.json 2025-09-25 18:27:25 -04:00
bbedward 934f4b2210 ControlCenter styling consistency fixes 2025-09-25 18:19:07 -04:00
bbedward a6a41d4de1 Re-work theme approach to watch a file from matugen-worker 2025-09-25 16:49:57 -04:00
bbedward 185ee20f2d Fix idle monitor creation 2025-09-25 16:30:46 -04:00
bbedward 778b960130 Fix hibernate manual option 2025-09-25 16:11:18 -04:00
bbedward 184938d10a Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-09-25 15:59:21 -04:00
bbedward 25565af5f9 Some styling consistencies 2025-09-25 15:57:56 -04:00
purian23 0bd9a4f860 fix Control Center IPC via Escape key 2025-09-25 14:52:21 -04:00
purian23 fe64dd1dea Add IPC for Control Center 2025-09-25 14:23:45 -04:00
bbedward a4a59fd586 Theme consistency overhaul:
- Add surface shift option, start from surface by default (previously
  was surfaceContainer). Old colors can be attained by changing it back
to "container" in theme colors
- Remove borders on surface elements, mostly
- Fix popup distances
- Use surfaceContainer/sch by default on widgets
2025-09-25 12:46:37 -04:00
bbedward 7ccd2d9418 Add random transition effect 2025-09-25 09:06:38 -04:00
bbedward 974dd70a06 Correct top bar bg color 2025-09-25 08:36:23 -04:00
sam 1690e4f63b feat: expose matugen palette selection (#251)
Looks good, thanks!
2025-09-25 08:27:54 -04:00
bbedward ec75ef468b Remove obsolete setting 2025-09-24 19:15:37 -04:00
bbedward a57c1e6451 Fix edit mode of multiple disk usage widgets in control center 2025-09-24 18:13:36 -04:00
bbedward bdc79a13a9 just show mount path - wasted space = less 2025-09-24 18:09:36 -04:00
bbedward b893694977 Add disk usage component for TopBar and ControlCenter 2025-09-24 18:06:46 -04:00
bbedward 9be7d44765 disable matugen env var support, also dont reload ghostty if not
configured with dms theme
2025-09-24 17:09:15 -04:00
bbedward 3f8d8ca379 Update ghostty readme blurb to disable annoying popups 2025-09-24 16:39:33 -04:00
bbedward e50c3cceeb Add grouped apps option to the dock 2025-09-24 16:15:38 -04:00
bbedward 14e648911d Hyprland: fix focused app logic for empty workspaces 2025-09-24 15:08:22 -04:00
bbedward 3c42a618d4 auto-reload ghostty config on matugen generation 2025-09-24 14:38:45 -04:00
lonerorz bed2259944 Refactor workspaceSwitcher (#246)
* Refactor WorkspaceSwitcher

Implement async data loading, dynamic UI, and fix QML connection
warnings.

* Enhance WorkspaceSwitcher

Adjust width dynamically based on icon count for better space
utilization

* fix(WorkspaceSwitcher): correct ternary logic for placeholder workspaces
2025-09-24 14:36:38 -04:00
bbedward 7516d44de9 Fix focused app taking up space when no focused window
+ Fix niri retaining focused windows from last workspace
2025-09-23 16:59:15 -04:00
bbedward e9a61a4f81 Fix again widget positioning logic.
- This is complicated because widgets unload, and we already want to
  respect positioning even when unloaded.
- Plus in center section we want odd number for the center one to always
  be centered, etc, etc.
2025-09-23 16:30:49 -04:00
bbedward 7a7d7d053a Fix even number of widgets in top bar 2025-09-23 16:11:30 -04:00
bbedward 2067e44baf Do not default privacy indicator 2025-09-23 16:07:08 -04:00
bbedward 8e010478c7 Add labels for weather 2025-09-23 16:01:39 -04:00
bbedward ec4f0ff2ed slightly narrow HeaderPane 2025-09-23 15:34:04 -04:00
bbedward c04177e45d ControlCenter: Implement edit mode for customizing widgets 2025-09-23 14:38:01 -04:00
bbedward b9b1737639 fix iris bloom coordinates 2025-09-23 00:09:59 -04:00
bbedward 4468e4c6af Add none as a wallpaper transition type 2025-09-22 18:57:03 -04:00
bbedward cf5dec733d More wallpaper effects + per-monitor auto cycling 2025-09-22 17:51:59 -04:00
bbedward d9b9da4b3d Native text rednering in control center 2025-09-22 16:18:10 -04:00
bbedward d62ef89bc3 fix some sorting of niri toplevels 2025-09-22 16:09:00 -04:00
bbedward 1b681e68b9 Bar: fix show on overview mousearea 2025-09-22 15:52:45 -04:00
bbedward d15ee0c29b DankCircularImage for album art and update audio input slider 2025-09-22 15:49:10 -04:00
bbedward 62b7b30754 Suppress niri toasts on theme changes 2025-09-22 14:50:35 -04:00
bbedward aa52b586d6 also use shader for DankCircularImage 2025-09-22 12:31:16 -04:00
bbedward ca11735c1d Add wallpaper transition effects, courtesy of @Ly-Sec
- Just copied the shaders from noctalia since they're pretty awesome
2025-09-22 12:28:15 -04:00
bbedward 78683032aa Ensure media topbar widget is always unloaded 2025-09-22 10:24:44 -04:00
bbedward bd5064e567 Topbar: fix top bar input mask when hidden 2025-09-22 10:18:43 -04:00
bbedward 3862f0ced5 update monochrome pallette 2025-09-22 09:56:43 -04:00
bbedward 878c1d9385 Network widget: nowrap text 2025-09-22 09:52:06 -04:00
bbedward 0a5f635a2d Fix media player positioning
fixes #235
2025-09-22 09:47:34 -04:00
bbedward 6a963ed618 Add vesktop theme 2025-09-22 09:21:47 -04:00
bbedward 35f059b1dc Fix readme format 2025-09-22 09:14:48 -04:00
bbedward b7838e497d fix readme 2025-09-22 09:13:26 -04:00
bbedward 3edfff388c add pywalfox for matugen 2025-09-22 09:10:44 -04:00
bbedward 0bfaf2a8f8 Hide topbar all the way when hidden
fixes #234
2025-09-22 08:48:16 -04:00
bbedward 50f6dfa709 Fix excessive TopBar repaints, dont mask animating area 2025-09-22 00:48:15 -04:00
bbedward 897f48d151 Leverage DankCircularImage for profile pic 2025-09-21 23:45:00 -04:00
bbedward c43f58e1bf Fix running apps tooltip 2025-09-21 23:16:04 -04:00
bbedward de532fdeda do not put topbar on overlay layer 2025-09-21 23:05:55 -04:00
bbedward c21896b0df restore themes minus deepBlue 2025-09-21 23:02:14 -04:00
bbedward 1669be4dd9 re-generate stock generic themes with matugen and remove deepBlue 2025-09-21 22:54:45 -04:00
bbedward 81550229e8 allow overriding matugen type in custom themes 2025-09-21 22:31:45 -04:00
bbedward f38ffa1de5 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-09-21 22:06:21 -04:00
bbedward 66b493fb2b Add hibernate support to power menus 2025-09-21 22:06:12 -04:00
purian23 ceae254160 fix: Notification grouped image clipping 2025-09-21 21:12:25 -04:00
bbedward a3b3e49313 Remove power menu hints 2025-09-21 21:02:40 -04:00
bbedward 8fb17f7fd5 auto-focus first notification on modal when opened 2025-09-21 21:01:35 -04:00
bbedward c464bb3e47 make the volume knob in media player consistent 2025-09-21 20:56:57 -04:00
bbedward bd72cf3adf Fix generic theme contrasts 2025-09-21 20:31:14 -04:00
bbedward a734d9b497 Add monochrome theme courtesy of Vouk@discord 2025-09-21 20:17:30 -04:00
bbedward b18814e086 fix profile picture browser 2025-09-21 20:13:00 -04:00
bbedward 74417a18ea change wifi icon suite 2025-09-21 19:56:46 -04:00
purian23 227081c5c9 Update Topbar Autohide Fullscreen Edgecase 2025-09-21 16:20:17 -04:00
purian23 c3b3edcae8 Allow Tobar Autohide to work w/Modals 2025-09-21 15:28:15 -04:00
bbedward c8f87085a0 respect cornerRadius setting in more places 2025-09-21 11:47:28 -04:00
bbedward 118375f004 respect corner radius in settings 2025-09-21 11:44:00 -04:00
bbedward babcc66765 Re-style control center & sliders to be more android 16-y 2025-09-21 11:39:31 -04:00
bbedward 64c8e79bf2 lock screen: allow starting pam auth with empty password 2025-09-21 09:33:19 -04:00
bbedward 12e8e72bb2 Multiple widget color opts 2025-09-21 09:24:55 -04:00
bbedward 72b79c0f51 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-09-21 08:25:29 -04:00
bbedward 50e440f212 fix top bar mask area 2025-09-21 08:25:18 -04:00
purian23 7c1c64fefc fix: Notepad text opacity 2025-09-21 01:44:47 -04:00
purian23 2e4770f4be feat: Add Transparency override option to Notepad 2025-09-21 00:49:23 -04:00
bbedward 0b2bf0b83d simplify idleService 2025-09-20 21:07:53 -04:00
bbedward 1fbb614aa8 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-09-20 20:45:06 -04:00
bbedward 55a29d8504 minimum width for system monitoring topbar widgets 2025-09-20 20:44:50 -04:00
purian23 ffc13f71b7 feat: Add line numbers toggle to Notepad settings 2025-09-20 19:06:58 -04:00
purian23 d4816bd174 Modularize the Notepad 2025-09-20 14:22:13 -04:00
bbedward 3cac6e5eea locale-aware calendar grid 2025-09-20 12:45:40 -04:00
bbedward 50bc3db048 Add lock before suspend option to power settings 2025-09-20 12:20:19 -04:00
bbedward 2b99e61c65 Launcher: actually, don't spawn with niri by default 2025-09-20 11:56:07 -04:00
bbedward 306a0c0e2d Add support for custom launch prefix, launch apps with niri msg action
spawn on niri
2025-09-20 11:54:46 -04:00
bbedward 0ba4ee74b5 onDestruction handler in display service 2025-09-20 10:26:22 -04:00
bbedward 2b5a5115b7 cleanup vpn gdbus monitor 2025-09-20 10:11:17 -04:00
bbedward 8a7e386adb fix dangling gdbus processes 2025-09-20 10:09:28 -04:00
bbedward 6e6412fffc Readme update again 2025-09-19 21:13:45 -04:00
bbedward bc540056fc readme update 2025-09-19 21:11:41 -04:00
bbedward 0055ddbc8d update bluetooth not available visuals 2025-09-19 20:55:06 -04:00
bbedward 3376dc893d Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-09-19 17:01:09 -04:00
bbedward 4a673fe1b5 check disabled option on DankButtonGroup + always show bluetooth
placeholder
2025-09-19 17:00:53 -04:00
purian23 6639f6ead0 Update notification card source images 2025-09-19 16:49:04 -04:00
bbedward 2ce32e8bb5 add media player to lock screen 2025-09-19 16:25:43 -04:00
bbedward 4d13a3d909 more detailed player info in selection 2025-09-19 16:13:50 -04:00
bbedward 691b6da7a7 Implement IdleMonitor to replace swayidle/hypridle functionality 2025-09-19 15:59:40 -04:00
bbedward e6265c2f71 fix battery health properly
- apparently displayDevice does not expose all the right properties
2025-09-19 13:41:37 -04:00
bbedward de9bb43c26 janky battery health workaround 2025-09-19 11:34:16 -04:00
bbedward 1ae9802866 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-09-19 10:24:53 -04:00
bbedward 0e887408c5 niri: do screen transition when changing themes 2025-09-19 10:24:37 -04:00
Body 88b7657e34 stop app launcher from updating and reorganizing results on fade out when it is launching an app (#211) 2025-09-19 10:04:41 -04:00
bbedward 5fcffba73e goth corners off by default 2025-09-19 09:00:51 -04:00
bbedward 542536455b TopBar: add "goth" corners option for the swooping edge effect 2025-09-18 23:37:32 -04:00
bbedward 97f0acadb6 didnt mean to push topbar change 2025-09-18 19:01:32 -04:00
bbedward 32f3e579e7 parser fix 2025-09-18 18:22:37 -04:00
bbedward bc446dabaf network: never allow rescans during update, more selective parsing from
gdbus monitor
2025-09-18 18:19:06 -04:00
bbedward d8e2a73e0b reduce network refresh spam 2025-09-18 17:35:53 -04:00
purian23 ae6fd42163 fix: Widget popup warning 2025-09-17 16:43:16 -04:00
bbedward cc7363eee1 redesign battery popout and use button group in network detail 2025-09-17 15:33:26 -04:00
bbedward 38c4b33d15 update descriptions 2025-09-17 15:09:37 -04:00
bbedward 6c81aa0908 Add catpuccin theme + material 3 components
- Update DankToggle to be more representative of m3 design
- Add DankButtonGroup to represent a m3 button group
2025-09-17 14:54:34 -04:00
bbedward 102cc3c0b6 fix widget selection search 2025-09-17 11:58:04 -04:00
bbedward 02300024cf use Paths.strip helper everywhere for consistency sake 2025-09-17 11:08:51 -04:00
bbedward 344761df00 remove ensureDirectory from notepad 2025-09-17 10:14:49 -04:00
bbedward c0edaea716 fix style of font scale option 2025-09-17 10:12:06 -04:00
bbedward 75e660f78e ensure file:// prefix is trimmed from notepad 2025-09-17 09:25:43 -04:00
purian23 be4d9cadba fix: Save user prefs in topbar spacing 2025-09-16 20:59:32 -04:00
bbedward fdaac6ac8b calendar: simpler khal parsing
- assuming 12 is always MM 21 is always dd, 2013 is always yyyy
2025-09-16 13:59:54 -04:00
bbedward 5c540f826e support multiple khal date formats 2025-09-16 13:53:03 -04:00
bbedward 55b3bffccd Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-09-16 13:29:58 -04:00
bbedward 0cb2a96b3e scrap lock screen service
- was overcomplicated state syncing that isn't needed
2025-09-16 13:29:36 -04:00
purian23 9f13ac6963 Add font override options in Notepad 2025-09-15 23:03:57 -04:00
bbedward 19e148f65c more m3wave improvements and notification image async loading 2025-09-15 23:01:08 -04:00
bbedward 7d9334916c scrap topbar border 2025-09-15 22:13:35 -04:00
bbedward 50dedb55ac update firefox 2025-09-15 20:24:08 -04:00
purian23 38f61ceae2 Add Media click-through on Dashboard 2025-09-15 19:52:27 -04:00
bbedward fff4b8c817 reduce CPU burn of M3Wave 2025-09-15 19:00:59 -04:00
bbedward 0fe76b7575 minor layout adjustments to the media player buttons 2025-09-15 18:27:50 -04:00
bbedward 3c4719d430 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-09-15 17:48:02 -04:00
bbedward c04e2ae0ff tweak album art circle mask 2025-09-15 17:47:51 -04:00
purian23 fec921df10 feat: Update Notepad to use Metadata (READ ME)
- Potential breaking changes; SAVE your notes before updating
- The new system will store files locally to free up Session data
- Session metadata will be saved in JSON via: `notepad-session.json`
- Local files will be saved in: `/.local/state/DankMaterialShell/notepad-files`
2025-09-15 17:37:39 -04:00
bbedward 5186afce06 FrameAnimation instead of timer 2025-09-15 15:25:27 -04:00
bbedward 77ea6a9372 readme update 2025-09-15 13:30:53 -04:00
bbedward afb0435b5c firefox theme guidance 2025-09-15 13:27:22 -04:00
bbedward 903ef0cc72 make system updater widget more generic 2025-09-15 11:55:15 -04:00
Aziz Hasanain e4f86abda9 Add an ArchUpdater widget (#201) 2025-09-15 08:54:39 -04:00
bbedward 8ee43de145 handle remote album art 2025-09-14 22:52:25 -04:00
bbedward 83b29c5ed9 tweak colors 2025-09-14 22:09:57 -04:00
bbedward cdf4ca3b3b media player and gtk3/4 color updates 2025-09-14 22:07:19 -04:00
bbedward a0192e709e fix gtk3 colors and clean media player 2025-09-14 20:49:13 -04:00
bbedward 59c7246989 media player cleanups 2025-09-14 19:16:54 -04:00
purian23 76ee483b27 feat: Implement battery widget in Control Center 2025-09-14 16:40:09 -04:00
purian23 ba6c7ae28c feat: Implement Color Picker 2025-09-13 22:02:48 -04:00
purian23 07c1677145 Remove redundant timer check 2025-09-13 21:05:16 -04:00
purian23 3db1a2ccf2 fix: Esnure no volume UI clipping 2025-09-13 21:01:56 -04:00
purian23 6d84553da9 Update privacy indicator logging 2025-09-13 19:56:02 -04:00
purian23 77bc129dfc Media Update 2025-09-13 19:46:41 -04:00
purian23 2c451b9779 Media Hub redesign 2025-09-13 18:12:00 -04:00
Aziz Hasanain 3c6bb58088 Fix Notepad on Hyprland due to missing import (#195) 2025-09-13 13:49:38 -04:00
bbedward 0372c44d98 jpg 2025-09-13 11:56:38 -04:00
bbedward 8fc40ab12d remove debug opt 2025-09-12 14:11:20 -04:00
bbedward 9e1272029e re-add --screenshot 2025-09-12 14:08:32 -04:00
bbedward 27344a47e2 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-09-12 12:37:23 -04:00
bbedward c42681b9b9 hide wallpaper engine unless they are available 2025-09-12 12:37:06 -04:00
Aziz Hasanain 939f47286e Fix KeyboardLayout widget in Hyprland (#192) 2025-09-12 12:25:16 -04:00
Aziz Hasanain c8cb5dc146 Add support for linux-wallpaperengine wallpapers (#191)
* Add support for linux-wallpaperengine wallpapers

* Remove unnecessary onCompleted hook
2025-09-12 12:13:14 -04:00
bbedward c3759dc542 always use short form 2025-09-12 10:51:03 -04:00
bbedward 17b49ad1f9 longer weather interval and network service renice 2025-09-11 23:09:50 -04:00
bbedward 63c54b5611 re-nice weather curl commands 2025-09-11 22:52:01 -04:00
bbedward 988d5c2fc3 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-09-11 22:34:36 -04:00
bbedward 7a671b586c SystemClock for calendar overview 2025-09-11 22:34:22 -04:00
Lulu b36da8eba2 Add namespace to notification popup (#187) 2025-09-11 22:09:07 -04:00
bbedward 5b49e36da4 elide text in app drawer 2025-09-11 18:49:17 -04:00
purian23 2395274714 Add weather card click-through 2025-09-10 20:13:15 -04:00
bbedward d0cbe689f8 optional wavy media playback bars 2025-09-10 15:52:09 -04:00
bbedward 4ae157af35 update readme 2025-09-10 11:40:18 -04:00
bbedward fb94dd0c4a fix notifs 2025-09-10 11:19:11 -04:00
bbedward fb01d1af4b layout adjustments 2025-09-10 09:23:14 -04:00
bbedward 9438868706 manual weather coords 2025-09-10 08:49:12 -04:00
bbedward a77f6eb385 ensure state dir exists 2025-09-10 08:39:34 -04:00
bbedward 44ebd6a2ee fix double click to open dash 2025-09-10 08:29:09 -04:00
bbedward e5083fb3a4 remove old remnants from when it was just a calendar 2025-09-09 23:01:21 -04:00
bbedward 5a9c80540e IPCs for dash 2025-09-09 22:56:19 -04:00
bbedward 5bb489f3c4 layout adjustments 2025-09-09 22:41:33 -04:00
purian23 35ea621a17 fix Notepad warning 2025-09-09 20:27:01 -04:00
bbedward a4d1b6ad76 bring back center special positioning logic for DankDash 2025-09-09 20:24:52 -04:00
bbedward a67a6c7c1c DankDash: Replace CentCom center with a new widget 2025-09-09 20:00:31 -04:00
bbedward 1c7d8a55f3 fix wheel events propagation 2025-09-09 09:24:47 -04:00
bbedward c9d38ec6f3 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-09-09 00:35:50 -04:00
bbedward d961c7569b steam_app icon handling 2025-09-09 00:35:38 -04:00
purian23 70a971ade2 Merge remote changes 2025-09-09 00:27:03 -04:00
purian23 5d4ba8b38a Update to Native fileView 2025-09-09 00:25:40 -04:00
bbedward 537b4b3604 transmission fix 2025-09-08 23:58:01 -04:00
purian23 7105b09dc0 feat: Add tabs to Notepad 2025-09-08 23:08:47 -04:00
purian23 e0118987ad feat: Add fuzzy search to the Widget Selection modal 2025-09-08 22:26:36 -04:00
bbedward fca3c1ef96 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-09-08 22:02:09 -04:00
bbedward c867090f12 transmission icon fix 2025-09-08 22:01:59 -04:00
purian23 ee06338640 Notepad top layer & remove background click to close 2025-09-08 21:43:00 -04:00
purian23 751aee40a5 Follow theme transparency 2025-09-08 21:18:26 -04:00
purian23 252cc8f42a Notepad multi-size options 2025-09-08 20:44:46 -04:00
bbedward dc28015313 default to false 2025-09-08 19:53:07 -04:00
bbedward 47b6b365a1 make show on overview top bar independent 2025-09-08 19:52:27 -04:00
bbedward e2945a6a2a threaded redraws 2025-09-08 19:42:44 -04:00
bbedward 3aa8ea55e7 case 2025-09-08 17:28:22 -04:00
bbedward 2179468caf add open-topbar-on-overview for niri 2025-09-08 17:26:15 -04:00
bbedward 597f748e56 updoot 2025-09-08 17:05:19 -04:00
bbedward ac1d01a190 qt6ct readme update 2025-09-08 17:04:11 -04:00
bbedward fd623035c7 small readme tweak 2025-09-08 16:29:28 -04:00
bbedward cbcb9a7f89 readme updates 2025-09-08 16:22:53 -04:00
bbedward 9c2e5561d7 nixos readme blurb 2025-09-08 15:53:15 -04:00
bbedward 9292e15419 update readme 2025-09-08 15:36:01 -04:00
bbedward c78cb3c767 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-09-08 14:25:33 -04:00
bbedward 3a1a553a7d change weather provider to open-meteo
- seems more accurate and reliable than wttr.in
2025-09-08 14:24:40 -04:00
Rishi Vora cbf3d0f330 add more binds to nix module (#179) 2025-09-08 14:08:21 -04:00
Rishi Vora 9d416ddbd6 fix and cleanup flake (#178)
- switch to nixpkgs-unstable
- use a more sensible set in `forEachSystem`
- fix typos in options
- add hotkey-overlay title for binds
- use the new `dms` cli tool instead of qs ipc calls
2025-09-08 13:02:32 -04:00
bbedward 079faa0d40 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-09-08 12:13:35 -04:00
bbedward 4c18e6b01d fix theme persistence 2025-09-08 12:13:21 -04:00
purian23 fcd818b098 feat: Ability to scale DMS font size
- Note: ideally the scaling is best performed in niri or hyprland
- This option adds the ability to override on top of the existing compositor options
2025-09-07 22:57:47 -04:00
bbedward cd1c992abd updoot 2025-09-07 22:20:00 -04:00
bbedward cebcb4d5d9 set dock namespace to only the rect 2025-09-07 22:16:39 -04:00
bbedward 68243107ca modal + popout namespace 2025-09-07 22:04:38 -04:00
bbedward af952f6397 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-09-07 19:42:34 -04:00
bbedward 7d88f37f90 Add support for default-session.json 2025-09-07 19:41:57 -04:00
purian23 8abc94fd07 Add notepad file persistence & overwrite save options 2025-09-07 15:39:05 -04:00
Aleksandr Lebedev c23088c98a Fixed broken app icons on Niri (#170) 2025-09-07 07:21:43 -04:00
bbedward 072cbf284e hide brightness slider opt 2025-09-06 10:12:39 -04:00
bbedward f7dfd31256 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-09-06 10:06:15 -04:00
bbedward 2ef41555a9 option to hide power actions in lock scsreen 2025-09-06 10:05:49 -04:00
purian23 c9d7641d86 Update initial focus on IPC Notifications & Notepad 2025-09-05 23:40:09 -04:00
Eduardo B. A. 5f97fd4f7c Add config options to features (#167)
* feat: Add config option to set quickshell package

* refactor: Use configured quickshell package for ipc calls

* feat: Add config for all shell features in flake

Also add missing dependencies

* fix: Fixes niri binds

* fix: Fixes dgop package path

* fix: Fixes pkgs not being set

* fix: Use ${pkgs.system} for dms-cli package

---------

Co-authored-by: Eduardo Barreto Alexandre <git@dummy.com>
2025-09-05 22:57:58 -04:00
Eduardo B. A. 9ba3bfb4db [FIX] Allow setting quickshell package (#166)
* feat: Add config option to set quickshell package

* refactor: Use configured quickshell package for ipc calls

* fix: Fixes niri binds

---------

Co-authored-by: Eduardo Barreto Alexandre <git@dummy.com>
2025-09-05 22:24:07 -04:00
Eduardo B. A. 353ee355a3 Allow setting quickshell package (#165)
* feat: Add config option to set quickshell package

* refactor: Use configured quickshell package for ipc calls

---------

Co-authored-by: Eduardo Barreto Alexandre <git@dummy.com>
2025-09-05 21:38:01 -04:00
purian23 79c21e67dc Add additional savings opts to notepad 2025-09-05 20:57:35 -04:00
purian23 4963658bc2 Merge pull request #164 from sezaru/support_multiple_systems
feat: Add support for multiple systems in flake
2025-09-05 20:00:31 -04:00
bbedward 613faea714 readme update 2025-09-05 19:47:58 -04:00
bbedward 2d764d8cac readme heading fixes 2025-09-05 19:39:35 -04:00
bbedward bd29abc7e3 readme update 2025-09-05 19:33:58 -04:00
Eduardo Barreto Alexandre 3d3b2726c9 feat: Add support for multiple systems in flake 2025-09-05 19:58:42 -03:00
bbedward 0b76171151 add dms-cli as flake dependency 2025-09-05 18:51:26 -04:00
bbedward 8d674a4fdc per-monitor wallpapers 2025-09-05 16:08:32 -04:00
Kyle Moore 68157ca636 feat: add configurable per-monitor workspace filtering and system tray monitor selection (#163) 2025-09-05 15:36:23 -04:00
bbedward 18484bb9cc background-color transparent for niri colors 2025-09-05 10:20:25 -04:00
bbedward e02b2580c9 fix toast suppression 2025-09-05 09:27:44 -04:00
bbedward 0d6dbf5f99 suppress niri toast initially
- matugen will trigger it on startup if auto t heming is enabled
2025-09-05 09:07:04 -04:00
bbedward ba1125bc00 fix binding loop 2025-09-05 08:22:51 -04:00
purian23 af5094b479 feat: Enable multi-monitor notepad support
- Notepad will now open on the currently focused monitor/workspace display by default
2025-09-04 20:57:02 -04:00
purian23 4742636eb3 Multi-screen notepad support 2025-09-04 20:27:27 -04:00
bbedward c708f57e8f add namespaces to topbar and dock 2025-09-04 20:00:13 -04:00
bbedward 54b63a1f5f fix ipc return types 2025-09-04 15:59:27 -04:00
bbedward 3c3cffd7b6 bar ipc change from show to reveal 2025-09-04 15:53:54 -04:00
Aleksandr Lebedev e64124cce3 Fixes for better Virtual keyboard support and Workspace Switcher (#155)
* Disabling workspace animation if too many icons

* Fixed virtual keyboard on popups and modals

- Fixed VK sometimes appearing under popups and modals (that made the
keyboard unusable)
- Fixed VK not working on app drawer
- Left comments for future changes

* Added explanation for disabling animation

* Fixes comments
2025-09-04 10:34:42 -04:00
purian23 eecbd8c733 Another warning axed 2025-09-04 00:08:22 -04:00
purian23 9e0693ca8c Fix warnings 2025-09-03 23:58:53 -04:00
bbedward 998d390161 update contributing 2025-09-03 23:34:39 -04:00
bbedward 67a16da8c7 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-09-03 23:26:27 -04:00
bbedward 21867c842f modules cleanup and qmlfmt everywhere
- throw in 24H clock fix and app drawer fix too
2025-09-03 23:26:07 -04:00
purian23 f7c45acc26 Update notepad text input 2025-09-03 20:56:41 -04:00
purian23 178ccd3634 Notepad slideout redesign 2025-09-03 20:13:36 -04:00
bbedward b4e607e2b4 Allow solid colored wallpaper, fix fzf search 2025-09-03 16:29:47 -04:00
bbedward 3856ce14cd cleanup and qmlfmt some modules 2025-09-03 15:00:03 -04:00
bbedward d4db8a01fe better fuzzy search, sweeping clean and qmlfmt of Widgets 2025-09-03 12:52:03 -04:00
Aleksandr Lebedev 886c6877d5 Workspace switcher: max icons setting (#150) 2025-09-03 08:50:07 -04:00
bbedward 5146dcb3f7 fix width calculation 2025-09-03 01:09:37 -04:00
bbedward 73b832eddb cleanup and qmlfmt remaining modals
- qmlfmt sucks I know, but what else can I do
2025-09-03 00:50:15 -04:00
bbedward f5871ab27e Common modal folder 2025-09-03 00:07:38 -04:00
bbedward ae6a1b77c2 systematic cleanups and qmlfmt file browser modal 2025-09-03 00:00:25 -04:00
bbedward 93da303967 remove ClipboardListView 2025-09-02 23:37:22 -04:00
bbedward 2a8087cd27 cleanup clipboard history modal 2025-09-02 23:19:13 -04:00
bbedward 531d6334fb Systematic cleanup and qmlfmt of all services
- qmlfmt kinda sucks but it's what qt creator uses
2025-09-02 22:45:06 -04:00
bbedward 21089aa66e Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-09-02 21:23:29 -04:00
bbedward be42280aca fix named workspace icons not filtering by display 2025-09-02 21:23:18 -04:00
purian23 123058f770 Refactor Notepad: Keyboard shortcuts, md formats 2025-09-02 20:36:33 -04:00
bbedward 0872499f36 remove stale refs 2025-09-02 18:58:17 -04:00
bbedward cfbeb6062b merge the two workspace switcher widgets and add "Show apps" option 2025-09-02 18:55:36 -04:00
bbedward 213543acb3 tighten up lockscreen spacing 2025-09-02 18:34:39 -04:00
Aleksandr Lebedev 5bffb1ba10 Advanced Workspace Switcher Widget + Lockscreen Virtual Keyboard (#149)
* Virtual keyboard on lockscreen

Almost whole code was taken from https://github.com/LucasCodingM/customVirtualkeyboard

* AdvancedWorkspaceSwitcher + BottomBar

- AdvancedWorkspaceSwitcher shows opened apps and allows to move to
them
- focusWindow function for niri
- Bottom bar with AdvancedWorkspaceSwitcher

* Cleanup + Styling fixes

* Changed visibility defaults back to true

For advanced workspace switcher

* Formatting + resolved commets
2025-09-02 18:26:52 -04:00
bbedward 96db0581d3 drop useless connections blocks 2025-09-02 17:59:33 -04:00
bbedward c4438b3339 readme update 2025-09-02 17:25:50 -04:00
bbedward 154b90103e fix copr reference 2025-09-02 16:30:14 -04:00
bbedward 809274a294 top bar improvements 2025-09-02 15:58:34 -04:00
bbedward 544a17b0db locale-aware clock and date formatting 2025-09-02 13:37:33 -04:00
bbedward 3eddef40fb update elogind detection 2025-09-02 11:25:38 -04:00
bbedward cb1b0550a2 better hover effect 2025-09-01 20:24:42 -04:00
bbedward 5c9e9385e6 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-09-01 20:19:54 -04:00
bbedward 8dc0ad690b add close button to running apps 2025-09-01 20:19:40 -04:00
purian23 437d077bd6 feat: New Notepad widget w/Autosave
- IPC: qs -c dms ipc call notepad toggle
2025-09-01 19:46:01 -04:00
bbedward 53698040ab adjust wheel events for touchpads 2025-09-01 15:14:16 -04:00
jichang 26e27e2686 feat: Add mouse wheel support for app/workspace switching (#143) 2025-09-01 09:11:47 -04:00
Shayan 924f2f6ea7 fix: slider movement for edge cases at 2% and 97% fill (#142)
* fix: slider movement for edge cases at 2% and 97% fill

* fix: slider movement drag offset
2025-09-01 09:11:06 -04:00
BB 271f8bf78f Merge pull request #141 from lucianosrp/fix-readme-command
fix: update command in README
2025-09-01 09:10:25 -04:00
Luciano 03fc26215d fix: update command in README
Update qs bash script
2025-09-01 14:58:39 +08:00
bbedward 87f70c66ba color tweaks 2025-08-31 20:04:00 -04:00
bbedward 4b7a43fccd fix wheel propagation 2025-08-31 19:48:58 -04:00
bbedward 09fdd67b49 disable mousewheel sliders in settings 2025-08-31 18:48:30 -04:00
BB a22f89b687 Merge pull request #139 from devnullvoid/feat/vpn-connections
Feature: VPN Connection Widget for TopBar
2025-08-31 18:40:16 -04:00
Jon Rogers d91c3572af VPN profiles: include vpn.service-type for TYPE=vpn; show friendly protocol label in popout (OpenVPN, WireGuard, IPsec, etc.); fix active icon to support multi-active 2025-08-31 16:33:07 -04:00
Jon Rogers 585ceb96e4 VPN Popout: remove redundant Flickable implicitHeight; rely on clip for overflow bounds 2025-08-31 16:21:48 -04:00
Jon Rogers 86a4346fc9 VPN Popout: show profile type (WireGuard/VPN) under name; remove ambiguous Quick Connect button from header 2025-08-31 15:58:24 -04:00
Jon Rogers 1e7a0beaed VPN Popout: show only one header action (Quick Connect when none active, Disconnect All when any active) 2025-08-30 16:32:09 -04:00
Jon Rogers d1890c69c9 VPN multi-active: toggle per row fixes reconnection, add Quick Connect + Disconnect All, header/tooltips summarize multiple active; default allow multiple active 2025-08-30 16:28:42 -04:00
Jon Rogers 7f467b0a0d VPN: enforce single-active by default; gracefully handle multi-active state; header summary fix; popout rows are full-row actions with correct active highlighting 2025-08-30 16:12:26 -04:00
Jon Rogers c924d60aeb remove VPN widget from default settings 2025-08-30 15:53:40 -04:00
Jon Rogers 40da170f66 VPN popout: full-row connect/disconnect like power profiles; darker details container; quick connect aligned right; styling aligned with Battery popout 2025-08-30 15:40:05 -04:00
Jon Rogers b9c4822c27 VPN Popout: match Battery popout styling (width, shadow rings, header close button, container colors); fix detail sizing 2025-08-30 15:07:49 -04:00
bbedward a890189530 try to handle image better 2025-08-30 14:44:31 -04:00
Jon Rogers 04ce154d36 VPN Detail: align action buttons right via RowLayout, add hover color by state, retain pointer cursor 2025-08-30 14:31:11 -04:00
Jon Rogers 7cc2c0acef TopBar VPN: fix centered icon by anchoring DankIcon center; remove inner wrapper 2025-08-30 14:15:31 -04:00
Jon Rogers 449418f537 TopBar VPN: icon-only widget with consistent background; fix imports; quiet VpnDetail anchors 2025-08-30 14:06:43 -04:00
Jon Rogers 952e5604d9 Add NetworkManager VPN integration: VpnService + Control Center detail; move to TopBar VPN widget with popout; fix logs and parsing; default top bar item 'vpn' added; minor layout fixes 2025-08-30 13:57:47 -04:00
bbedward e55c97185a remove arbiritary height cap 2025-08-30 12:47:02 -04:00
bbedward 7d3196c3cf Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-08-30 10:59:52 -04:00
bbedward 62c7202b33 fix bluetooth codec modal 2025-08-30 10:59:42 -04:00
purian23 ad23261478 Auto enable & disable nightMode on automation 2025-08-30 00:30:22 -04:00
purian23 e693857c39 Enchance nightMode toggle indicators 2025-08-29 23:53:38 -04:00
bbedward bd39a92d16 remove uptime: 2025-08-29 22:25:11 -04:00
Purian23 b8bffe97fe Merge pull request #128 from asaadmohammed74:keyboard-layout-widget
added keyboard layout widget (niri only)
2025-08-29 17:26:38 -04:00
asaadmohammed74 1030f4ba75 added keyboard layout widget (niri only) 2025-08-29 23:34:33 +03:00
bbedward 61a3dc4033 minor tweaks 2025-08-29 14:57:30 -04:00
bbedward 48643582e9 move light mode above night mode 2025-08-29 14:25:05 -04:00
bbedward 108fdd9b7f night mode repairs 2025-08-29 14:23:37 -04:00
bbedward 3746b1cfad no BT auto scan 2025-08-29 12:06:13 -04:00
bbedward 9b113c05c3 fix wifi toggling logic and simplify gammastep 2025-08-29 12:04:52 -04:00
BB 2672cb792c Merge pull request #127 from gonengazit/master
change DesktopEntries.byId to DesktopEntries.heuristicLookup
2025-08-29 11:48:39 -04:00
bbedward 76181bafff redesign control center 2025-08-29 11:48:11 -04:00
Gonen Gazit d37ccd86ee change DesktopEntries.byId to DesktopEntries.heuristicLookup
If there isn't an exact id match - it also looks for the WMStartupClass
field in .desktop files
2025-08-29 16:55:22 +03:00
purian23 64a26aabb8 Night Mode cleanup 2025-08-28 23:38:11 -04:00
purian23 9d8b196644 Updates to Night Mode Automation 2025-08-28 23:16:53 -04:00
purian23 324d6c13b4 feat: Night Mode Automation 2025-08-28 21:37:05 -04:00
purian23 48a78c39e2 Initial commit for nightMode automation 2025-08-28 17:18:59 -04:00
bbedward 6f11891b1c fix nil error 2025-08-28 11:34:54 -04:00
bbedward 29ee15e477 control center container width calculations 2025-08-28 10:46:42 -04:00
bbedward 77f40a7201 shorten modal duration 2025-08-28 10:12:48 -04:00
bbedward ddbebd8f7d Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-08-28 09:19:40 -04:00
bbedward 59d3e99ced fix infinite settings loop 2025-08-28 09:19:28 -04:00
BB 5b7de4789c Merge pull request #125 from yunxi177/patch-1
Fix READNE.md matugen package name error
2025-08-28 08:47:53 -04:00
云溪 503ff07c9d Fix READNE.md matugen package name error
matugen is not available in the official Arch repositories; use the AUR package matugen-bin instead
2025-08-28 14:10:47 +08:00
bbedward d7f14fada4 symlink flake to dms 2025-08-27 23:23:51 -04:00
bbedward 0aa064916c missing import 2025-08-27 18:15:10 -04:00
bbedward 9bdbefffe2 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-08-27 18:12:03 -04:00
bbedward c2765f655c more uwsm hacks 2025-08-27 18:11:56 -04:00
purian23 cab91e927e ReadMe 2025-08-27 18:07:44 -04:00
bbedward 74e64c178d dank16: ensure contrast against background 2025-08-27 15:04:32 -04:00
bbedward 0a25b6bc8e fallback backdrop on lock screen 2025-08-27 14:51:34 -04:00
bbedward 033098038b fix position 2025-08-27 11:39:53 -04:00
bbedward 174025eaff tweak backdrop 2025-08-27 11:35:33 -04:00
bbedward b4aba30ba8 change backdrop logo 2025-08-27 11:33:58 -04:00
bbedward ab9f482ffd fix light mode ipc 2025-08-27 09:00:56 -04:00
bbedward 4122cfca4c stock wallpaper tweak 2025-08-26 23:40:03 -04:00
bbedward f8b10204f8 fix notif modal close 2025-08-26 23:37:01 -04:00
bbedward 346e00d395 uwsm fix 2025-08-26 23:19:18 -04:00
bbedward 38b23d7fb0 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-08-26 22:54:50 -04:00
bbedward 21b1d79752 DankBackdrop and resizing control center 2025-08-26 22:54:30 -04:00
purian23 eda3ee8d3b fix: Workspace padding settings 2025-08-26 22:03:13 -04:00
BB dfd5524516 Merge pull request #121 from Vantesh/master
Add UWSM session check to prefer uwsm stop over compositor exit
2025-08-26 19:57:34 -04:00
Vantesh 4ca64a85bc feat:implement uwsm session check 2025-08-27 02:34:46 +03:00
Vantesh 5aa34b898c feat: implement uwsm session check on logout 2025-08-27 00:51:33 +03:00
bbedward 257df891ee hyprland running apps improvement 2025-08-26 15:55:32 -04:00
bbedward e2df1da5be hyprland for running apps workspace only 2025-08-26 15:29:35 -04:00
BB 50ea9daca6 Merge pull request #119 from gonengazit/master
Add option to only show apps in the current workspace in the running apps widget
2025-08-26 15:12:35 -04:00
bbedward 9e16368222 fix clipboard history issues 2025-08-26 14:59:55 -04:00
Gonen Gazit baea0ecc92 Add option to only show apps in the current workspace in the running apps widget
niri only currently - but should be simple enough to add support for
others
2025-08-26 21:54:00 +03:00
bbedward b887940dce missing dock from display config 2025-08-26 14:23:34 -04:00
bbedward 6e75e2b06c stylistic tweaks to personalization 2025-08-26 12:52:50 -04:00
bbedward 3d3fd6f724 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-08-26 11:48:02 -04:00
bbedward 05d3fd2a19 Add keyboard navi and wallpaper IPCs 2025-08-26 11:47:48 -04:00
BB cec24e37c3 Merge pull request #117 from Vantesh/master
Update time format to HH:MM
2025-08-26 09:02:56 -04:00
bbedward aed4c0ad17 renaame file 2025-08-26 00:11:08 -04:00
bbedward 426e19d1cc create dir 2025-08-26 00:03:15 -04:00
bbedward bf475776a9 suppress toast 2025-08-25 23:59:15 -04:00
bbedward 08b555d724 copy default-settings.json to settings.json if it exists 2025-08-25 23:47:02 -04:00
bbedward d48f5b0b3f Allow exclusive zone modifier 2025-08-25 23:29:31 -04:00
bbedward f074333c35 exit 0 2025-08-25 23:21:26 -04:00
bbedward 793f85e53f Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-08-25 23:20:10 -04:00
bbedward 473429238f re-work mmatugen meta theming
able to handle more rapid changes now, and colors look better
2025-08-25 23:19:29 -04:00
purian23 f4f949ebbc feat: PowerMenu modal 2025-08-25 22:08:17 -04:00
Vantesh 42c006fb2c Revert time format to HH:MM in checkTimeBasedCycling function 2025-08-26 03:17:15 +03:00
Purian23 673ec76fa7 Merge pull request #116 from xdenotte/master
fix: ensure matugen script exits 0 on success
2025-08-25 20:07:50 -04:00
Vantesh bb81378b20 feat: Use HH:MM format for 24hr clock system only 2025-08-26 03:07:36 +03:00
Victor Muthiani 6785a1b854 Merge branch 'AvengeMedia:master' into master 2025-08-26 03:12:16 +03:00
Vantesh c04699accf Update time format to HH:MM (e.g. 03:00) 2025-08-26 03:03:31 +03:00
xdenotte fce47ffed7 fix: ensure matugen script exits 0 on success
on nixos this resulted in constant "Failed to generate system themes" errors after each wallpaper change
2025-08-26 01:29:14 +02:00
bbedward a185679cd1 more matugen b16 improvements 2025-08-25 17:51:48 -04:00
bbedward 1c290fa9d7 actually use matugen colors on b16 2025-08-25 16:49:46 -04:00
bbedward fb8aad52c4 add kitty matugen+b16 support 2025-08-25 16:02:16 -04:00
bbedward adb15a7945 fix spacer reactivity 2025-08-25 15:44:20 -04:00
bbedward 3d54723ec6 media player: debounce on seek, fix binding issue, set cava source 2025-08-25 14:52:44 -04:00
bbedward b7ed526cf3 track non-sinks too 2025-08-25 12:29:44 -04:00
bbedward fada218723 Custom py ghostty generator 2025-08-25 11:28:28 -04:00
bbedward 4a0825e27f fix toast style 2025-08-25 10:19:00 -04:00
bbedward 7e03076a20 issue templates 2025-08-25 10:06:38 -04:00
bbedward ce3c1528f9 fixing spotify app id in top levels 2025-08-25 09:56:48 -04:00
bbedward a0e825c37e no purple grid fallbacks, rely on system icons only 2025-08-25 09:23:45 -04:00
bbedward fb7a019786 link badges 2025-08-25 08:37:04 -04:00
bbedward 1ab7cb62aa bind to all PW sinks 2025-08-24 23:58:28 -04:00
bbedward 3d4a394ba9 cleanup audio services 2025-08-24 23:46:53 -04:00
bbedward d4598d7738 update bug template 2025-08-24 22:16:57 -04:00
bbedward 08cc026c1f profile image ipc 2025-08-24 21:59:06 -04:00
bbedward f3c42f7f93 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-08-24 20:33:22 -04:00
bbedward 86bb04434d exclude noDisplay runInTerminal DesktopEntries 2025-08-24 20:32:49 -04:00
BB e3e0e397f0 Merge pull request #106 from xdenotte/master
Changed parsing and added function for fix wifi state
2025-08-24 20:29:33 -04:00
xdenotte ccbd7c155f Changed parsing and added function for fix wifi state 2025-08-24 19:56:56 +02:00
purian23 c0884f53a6 Merge branch 'pr-103' 2025-08-24 13:51:31 -04:00
asaadmohammed74 d405dac854 added a network speed monitor widget 2025-08-24 20:35:49 +03:00
bbedward d3427c7b19 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-08-24 12:06:25 -04:00
bbedward 9862636ab5 matugen: don't generate configs for not installed apps 2025-08-24 12:04:39 -04:00
bbedward 1d68caec37 Merge branch 'master' of github.com:AvengeMedia/DankMaterialShell 2025-08-24 10:32:31 -04:00
bbedward b998cbd3e6 display configs 2025-08-24 10:30:46 -04:00
bbedward b504bf1617 fix spacer sizes 2025-08-24 00:26:19 -04:00
bbedward afa60696c8 color 2025-08-23 20:16:21 -04:00
bbedward 1afc99b648 aur badges 2025-08-23 20:13:47 -04:00
bbedward 083a531ccb correctly represent network details 2025-08-23 18:12:48 -04:00
bbedward 1dfc7dc26e no background opt for topbar 2025-08-23 17:45:18 -04:00
bbedward 8eaa1a2d7e rename size 2025-08-23 15:45:17 -04:00
bbedward d706043c78 top bar manual show/hide 2025-08-23 15:33:42 -04:00
bbedward b8a2a3d613 change wifi icon set 2025-08-23 15:26:42 -04:00
bbedward ea7a143552 fix workspace padding 2025-08-23 15:16:51 -04:00
bbedward 3f6d330f5f scalable topbar 2025-08-23 15:15:07 -04:00
bbedward 9d7b617cd6 fix niri numbers 2025-08-23 13:43:56 -04:00
bbedward 75e04137de fix some dock behaviors on hyprland 2025-08-23 13:37:54 -04:00
bbedward 5e91aaa13e icon visible check 2025-08-23 13:26:14 -04:00
bbedward 1e2fdc5f24 Simplify mess 2025-08-23 13:22:30 -04:00
bbedward ad19107530 hyprland repairs 2025-08-23 13:14:26 -04:00
bbedward f175c10efc more matugen tweaks 2025-08-23 01:07:20 -04:00
bbedward abbbb35a39 readme headings 2025-08-22 23:59:34 -04:00
bbedward 311b981f69 modify ghostty colors 2025-08-22 23:50:39 -04:00
purian23 f0649975db Update DMS to lowercase for synergy 2025-08-22 23:19:04 -04:00
purian23 07fa4a4484 Simplify ReadMe & Docs IPC commands 2025-08-22 23:02:46 -04:00
bbedward 0eff022adc missing import 2025-08-22 14:16:40 -04:00
bbedward d7c98e73c8 simplify hyprland 2025-08-22 14:08:52 -04:00
bbedward 586cc61b0e elogind change 2025-08-22 12:55:55 -04:00
bbedward beb7d9b47e remove lock screen delays 2025-08-22 12:39:19 -04:00
bbedward dd99cb7ef8 unified SessionService to support elogind equivalents of systemd
commands
2025-08-22 11:40:20 -04:00
bbedward de17d53f1e Add link 2025-08-22 11:06:49 -04:00
bbedward ae3b77a257 theme repairs:
- gtk3 colloid light/dark fix
- fix missing gtk4 missing variables
- matugen script handle light/dark preference
- fix state of display tab light/dark toggle
2025-08-22 10:57:22 -04:00
bbedward 51b73be68c Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-08-22 08:43:41 -04:00
bbedward 8d79132806 missing scripts 2025-08-22 08:43:31 -04:00
BB a56623f1ff Merge pull request #84 from linyinfeng/flake
nix flake lock
2025-08-22 08:39:57 -04:00
Lin Yinfeng 00965b1879 nix flake lock 2025-08-22 18:33:51 +08:00
bbedward 9e88960963 Fix system theming messages and usage 2025-08-21 23:26:02 -04:00
bbedward 491d0a6f68 Add bluetooth codec switching, via @Vantesh 2025-08-21 22:26:26 -04:00
bbedward ca352e5c52 handle notif spam better 2025-08-21 19:09:04 -04:00
bbedward a6948f9c26 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-08-21 17:48:15 -04:00
bbedward 098a19acd1 fix notification dismiss performance 2025-08-21 17:47:14 -04:00
bbedward 41a0ae7de5 use rssi for network icon 2025-08-21 17:11:06 -04:00
bbedward bcd02d4e90 missing nvidia color 2025-08-21 14:36:24 -04:00
BB cbc14dddfe Merge pull request #78 from xdenotte/master
Small fix bash script for NixOS systems
2025-08-21 13:26:48 -04:00
xdenotte 3b327b93ba Update another bash script 2025-08-21 18:52:27 +02:00
xdenotte 2c8268497f Small fix bash script for NixOS systems 2025-08-21 18:42:03 +02:00
bbedward 1a8766a291 Revert "aggressive colors"
This reverts commit 32e9f481bc.
2025-08-21 09:17:54 -04:00
bbedward 32e9f481bc aggressive colors 2025-08-21 01:34:45 -04:00
bbedward be264df572 Always generate matugen regarldess of auto theme 2025-08-21 01:26:33 -04:00
bbedward b4249af68e matugen = not dependento n GTK and QT, update ghostty colors 2025-08-20 23:20:22 -04:00
bbedward 443a7f00f1 matugen ghostty template 2025-08-20 23:14:30 -04:00
bbedward 7c95d8df3b Fix non-dynamic themes not persisting 2025-08-20 23:02:55 -04:00
bbedward aedf78453e Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-08-20 22:54:12 -04:00
bbedward 37768e8bfe add dgop matugen template 2025-08-20 22:54:02 -04:00
purian23 070597f347 @ReadMe 2025-08-20 22:47:37 -04:00
bbedward cc60414487 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-08-20 22:37:02 -04:00
bbedward 5faf972890 show file path on custom theme tooltip 2025-08-20 22:36:53 -04:00
purian23 308a972fbd Update ReadMe w/Hyprland 2025-08-20 22:35:03 -04:00
bbedward e3ba365d39 conidtionally watch changes 2025-08-20 21:43:31 -04:00
bbedward f02d7f1f40 switch out social links when on hyprland 2025-08-20 21:33:03 -04:00
bbedward d20d944d1b fix auto focus app drawer 2025-08-20 21:26:09 -04:00
bbedward bad07df500 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-08-20 21:20:39 -04:00
bbedward 0e513185e0 support for custom themes 2025-08-20 21:20:33 -04:00
purian23 88d4dad21d feat: Complete core hyprland port 2025-08-20 21:02:34 -04:00
purian23 712b4986be Initial hyprland workspace support 2025-08-20 20:01:56 -04:00
bbedward 0a846fd1ee Clean up entire theme system 2025-08-20 19:30:45 -04:00
BB daaf1ae74c Merge pull request #77 from xdenotte/master
Fixed Blurriness of wallpaper
2025-08-20 18:50:26 -04:00
xdenotte e5d5e0c420 Fixed Blurriness of wallpaper 2025-08-21 00:47:36 +02:00
BB e5773c9c6a Merge pull request #76 from xdenotte/master
Fix Wallpaper preview
2025-08-20 18:06:24 -04:00
xdenotte b38afe9d9e Fix Wallpaper preview 2025-08-21 00:02:11 +02:00
bbedward be4c09e56d compositor service & use toplevels instead of niri data 2025-08-20 17:31:10 -04:00
bbedward 835d46a7af proper KColorScheme generation 2025-08-20 15:40:23 -04:00
bbedward ee8ab26d45 control center: open relevant tab based on click area, configurable
icons
2025-08-20 13:45:46 -04:00
bbedward 8c7b72fb6c Fix OSDs disappearing and toast exit anim 2025-08-20 11:06:47 -04:00
bbedward 1f24ad51f8 DankOSD and fix exit animations 2025-08-20 10:41:55 -04:00
bbedward 9b7a41e282 bright brightness OSD 2025-08-20 10:25:28 -04:00
bbedward d30bb71b39 prettier links 2025-08-20 10:10:09 -04:00
bbedward 3041582dab Add tooltips 2025-08-20 00:44:36 -04:00
bbedward af84de4d37 bigger matrix logo 2025-08-20 00:37:37 -04:00
bbedward f25f26df60 increase width 2025-08-20 00:34:43 -04:00
bbedward 61c26f6c70 better settings 2025-08-20 00:30:52 -04:00
bbedward b688bbfe83 qmlfmt with 4 space 2025-08-20 00:05:14 -04:00
bbedward 6e0977c719 consistency in top bar tab of settings 2025-08-19 23:58:54 -04:00
bbedward b45a7709bb more re-work to settings 2025-08-19 23:44:44 -04:00
bbedward 2c99fbd50f update settings modal footer 2025-08-19 22:46:35 -04:00
bbedward 73154f7e5f Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-08-19 22:32:00 -04:00
bbedward 9871da775f Cleanup wifi signal strength icon 2025-08-19 22:31:50 -04:00
purian23 72ca663c31 Settings Reorganization 2025-08-19 20:49:03 -04:00
purian23 3f57c6dab7 Update keyboard nav icons & tweak notification animations 2025-08-19 20:19:23 -04:00
bbedward 6367828036 MouseArea for links 2025-08-19 19:37:44 -04:00
bbedward 85b687bfe9 fix notif center popout keyboard events 2025-08-19 19:35:41 -04:00
bbedward 72677dfad7 small tweaks 2025-08-19 19:12:07 -04:00
bbedward 54f53f3b86 change color of dank header 2025-08-19 19:08:16 -04:00
bbedward 0d775f370b Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-08-19 19:05:22 -04:00
bbedward c9adb8faa3 Completely redesign settings 2025-08-19 19:05:12 -04:00
BB 64e8a81015 Merge pull request #67 from xdenotte/patch-1
Remove some not defined attributes
2025-08-19 18:13:40 -04:00
xdenotte 843a3d7b01 Remove some not defined attributes 2025-08-19 23:46:33 +02:00
BB 97c47d3a45 Merge pull request #66 from xdenotte/master
Add some fallbacks to fix wifi state
2025-08-19 17:36:04 -04:00
xdenotte 42a6b05fd4 Add some fallbacks 2025-08-19 23:30:21 +02:00
bbedward c87b66fe35 fix notif button state 2025-08-19 16:17:04 -04:00
bbedward 9a9ab8b047 update qtct matugen template 2025-08-19 16:07:59 -04:00
bbedward c276bb20f2 exclusive focus modals 2025-08-19 15:50:19 -04:00
bbedward dd10b93afe leave icon theme alone when its system default 2025-08-19 15:47:51 -04:00
bbedward 2a28f99831 refactor all modals and popouts so they retain animations on exit 2025-08-19 15:44:43 -04:00
bbedward 5fba96f345 Merge branch 'master' of github.com:bbedward/dank-material-dark-shell 2025-08-19 11:06:21 -04:00
bbedward 0d8ae1e09b incorporate ddcutil support 2025-08-19 11:06:11 -04:00
BB 3708d218fd Merge pull request #64 from ryzendew/patch-1
Update README.md for proper markdown
2025-08-19 10:35:04 -04:00
Mattscreative ebc14c8fa8 Update README.md for proper markdown 2025-08-19 11:29:55 -03:00
purian23 6d05974c3d Remove redundant focused name if appTitle & appName match 2025-08-19 10:14:27 -04:00
bbedward 8eb17c28b1 tweak systray icon sizing 2025-08-18 16:30:18 -04:00
bbedward 109d92367a update readme and add ipc doc 2025-08-18 15:42:25 -04:00
bbedward 481c18d575 Support expanded mode on running apps 2025-08-18 15:11:38 -04:00
bbedward 525ea5ce1c dock: re-work to separate pins from all open windows 2025-08-18 14:57:30 -04:00
bbedward b75342bf93 Add FocusedApp compact mode and fix SystemTray to use IconImage 2025-08-18 14:14:23 -04:00
bbedward d638e54ed7 Add RunningApps tooltip 2025-08-18 14:05:27 -04:00
bbedward be9bd388c2 RunningApps widget
- Sorts by monitor, workspace, then position (on a new enough niri
  version)
2025-08-18 11:17:33 -04:00
bbedward 4414b863c7 Ensure cava only runs when media is playing 2025-08-17 17:51:20 -04:00
bbedward ce3e4cda20 update readme 2025-08-17 14:59:02 -04:00
bbedward 49c642a7fc account for topbar spacing on all popouts 2025-08-17 14:43:12 -04:00
bbedward fc15397a1e ability to turn top bar square and adjust spacing 2025-08-17 14:34:35 -04:00
bbedward be33eb14dd correctly use monospace 2025-08-17 12:34:56 -04:00
bbedward 358665e5a1 hug toasts tighter to the top 2025-08-17 12:33:05 -04:00
bbedward 24b44186bd handle new ConfigReloaded event 2025-08-17 12:27:52 -04:00
bbedward d8082f70b3 slight efficiency improvement 2025-08-16 17:55:57 -04:00
bbedward c560d2d964 keyboard navi improvements 2025-08-16 15:05:53 -04:00
bbedward 63ac17e676 extract matugen json only 2025-08-16 12:37:02 -04:00
bbedward 7dfb2b6a99 keyboard navigation on clipboard history 2025-08-16 12:34:36 -04:00
bbedward 4135250bb7 DankGridView in file browser 2025-08-16 11:55:45 -04:00
bbedward d920c535f1 Tweak loaders of file browser and centcom 2025-08-16 11:53:54 -04:00
bbedward 115e9e0f81 add clear all keybinding to notifs 2025-08-16 10:32:06 -04:00
bbedward eb48c3c1b5 fix workspace # 2025-08-15 23:23:39 -04:00
bbedward 2612776a87 remove dumb tooltip 2025-08-15 23:12:02 -04:00
bbedward af9607fdbe Add systemd-inhibit widget 2025-08-15 21:52:44 -04:00
bbedward f67b90cfbe add weather refresh button 2025-08-15 17:09:54 -04:00
purian23 aec9e6c718 fix: Update rare TopBar visibility loadout on certain hardware 2025-08-15 14:37:40 -04:00
bbedward 4bc784deb2 fix wonky keyboard behavior on launchers 2025-08-15 12:26:45 -04:00
bbedward 02202efbba Revert "potential topbar loading fix"
This reverts commit 22780e40b1.
2025-08-15 12:19:06 -04:00
bbedward 22780e40b1 potential topbar loading fix 2025-08-15 12:02:56 -04:00
bbedward 0becbf4b4f remove useless change 2025-08-15 11:12:01 -04:00
bbedward ff70343fa0 fix some topbar issues 2025-08-15 09:32:58 -04:00
bbedward bf493b39fd optimize wallpaper 2025-08-14 23:43:07 -04:00
bbedward b9df98a1f5 fixes to media player position 2025-08-14 16:41:42 -04:00
bbedward c7634e2c23 readme update 2025-08-14 14:26:12 -04:00
bbedward faacdf2be3 fix wrapping text 2025-08-14 12:31:28 -04:00
bbedward 7dfa2f2b3f unbrick notification keyboard navi 2025-08-14 12:12:12 -04:00
bbedward 4ec36df98d fix notif modal not re-opening 2025-08-14 09:37:46 -04:00
bbedward c9be03ddaf create lock screen singleton 2025-08-13 21:01:35 -04:00
bbedward c3cccda37e fix undefined warnings on lockscreen 2025-08-13 20:19:12 -04:00
bbedward d8ae92932c danker lockscreen 2025-08-13 19:52:48 -04:00
bbedward 181e95acbc updoot space facts 2025-08-13 19:37:44 -04:00
bbedward f399bb1916 Fix audio tab in control center
- Swap between input and output components properly in loader
2025-08-13 19:13:36 -04:00
bbedward eb666477b9 DankActionButton notification center header buttons 2025-08-13 19:11:40 -04:00
bbedward 5a772505b1 re-factor battery popout to not be scrollable 2025-08-13 19:09:40 -04:00
bbedward ab2889c86f wrap all app category in function 2025-08-13 18:36:24 -04:00
bbedward cc6e465be1 bring back lazy loaders 2025-08-13 18:10:20 -04:00
bbedward 83c97eb245 revert popout lazy loaders 2025-08-13 18:06:27 -04:00
bbedward 37b97feeb0 direct b inding topbar 2025-08-13 17:56:32 -04:00
bbedward 7c32ed7bfb fix clipboard text focus 2025-08-13 17:50:53 -04:00
bbedward b9684ea2e2 spotlight focus fix 2025-08-13 17:38:51 -04:00
bbedward 4b8e529496 fix loading of spotlight 2025-08-13 16:47:58 -04:00
bbedward 79d69ed2b8 fix modal unloading 2025-08-13 16:15:57 -04:00
bbedward 141dd7c66b ensure loader patterns everywhere 2025-08-13 14:06:12 -04:00
bbedward f64dd538d5 new network service 2025-08-13 12:29:48 -04:00
bbedward a1bd7aaa3f fix clipboard auto-focus on open 2025-08-13 10:40:16 -04:00
bbedward 4363776545 fix categories 2025-08-13 10:18:10 -04:00
bbedward e218e77380 improve err handling 2025-08-13 10:02:52 -04:00
bbedward 1b85a7ef4d Fix signal 2025-08-13 09:56:37 -04:00
bbedward 67ac3d1c9d re-work nightmode setting to use gammastep 2025-08-13 09:53:46 -04:00
bbedward 3bbf8c5844 fix loginctl monitoring 2025-08-12 23:53:07 -04:00
bbedward bbd23171cc fix loader patterns in settings
- fix matugen command
- fix focused window being wrong sometimes
2025-08-12 23:31:58 -04:00
bbedward 6e92df95e9 pragma 2025-08-12 21:36:36 -04:00
bbedward fdf769883d Improve brightness device detection 2025-08-12 21:28:14 -04:00
bbedward 1037f010ed use loader pattern 2025-08-12 20:01:27 -04:00
bbedward a76f0de18e Add DankIconPicker 2025-08-12 19:55:06 -04:00
bbedward fc1beca5e7 lock screen format override 2025-08-12 19:25:33 -04:00
bbedward ac5a0fe995 Customizable date format 2025-08-12 19:21:04 -04:00
bbedward 2f6390cea2 support multi-brightness devices 2025-08-12 19:10:57 -04:00
bbedward 1e23c6b12c configurable timeouts, keyboard improvements 2025-08-12 14:28:53 -04:00
bbedward acc8272994 Merge branch 'master' of github.com:bbedward/DankMaterialShell into notif-work 2025-08-12 12:09:02 -04:00
bbedward 87af4fc325 monitor logind dbus events 2025-08-12 12:00:56 -04:00
bbedward f3bf7de377 fix lock check 2025-08-12 11:35:30 -04:00
bbedward 6a113d6dda many improvements 2025-08-12 11:32:33 -04:00
bbedward 0869677d49 Merge branch 'master' of github.com:bbedward/DankMaterialShell into notif-work 2025-08-12 11:05:25 -04:00
BB f27b0f5cd1 Merge pull request #42 from jiboli/master
Update Colors.qml
2025-08-12 11:00:22 -04:00
bbedward 4bf4dc20cc Check lock status on startup 2025-08-12 10:54:39 -04:00
bbedward de91eb8afa stash n otifs 2025-08-12 10:44:55 -04:00
João Henrique Riboli 0eddbad0ce Update Colors.qml
Made some modifications on parsing the json from matugen output, making shure only json stuff get parsed
2025-08-12 11:10:33 -03:00
bbedward 5c3d79eef7 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-08-11 16:59:05 -04:00
bbedward 938e0cf4fb make unmute when ipc volume changes 2025-08-11 16:58:45 -04:00
BB a0f6bf6601 Merge pull request #37 from bluelinden/master
Remove dependency and import of Niri HM module
2025-08-10 23:18:09 -04:00
bbedward 729f5a1b9c fix typo 2025-08-10 23:12:55 -04:00
bbedward 9bc5de8a8a dgop was rebranded 2025-08-10 22:21:54 -04:00
blue linden a8215188c6 remove unintentional niri reference 2025-08-10 21:33:42 -04:00
blue linden b7f03a72a4 remove dependency on niri module
user should be the one initializing niri module, because they may want
it configured a specific way, or may want to use DMS independent of
Sodiboo's module.
2025-08-10 21:32:50 -04:00
bbedward 5b259db341 requires dankgop 0.0.3+ 2025-08-10 14:48:53 -04:00
bbedward e87bd3fc59 fix lock screen clock 2025-08-10 14:19:23 -04:00
bbedward 1a4e16fc7a fix system trya item activation 2025-08-10 14:15:56 -04:00
bbedward 62e1aa8ae8 remove sample filtering 2025-08-10 13:43:50 -04:00
bbedward 78c38a898c battery view improvements 2025-08-10 13:35:59 -04:00
bbedward d32b09c999 use loader pattern in control center 2025-08-10 13:17:08 -04:00
bbedward 47f507da76 remove animation 2025-08-10 12:51:47 -04:00
purian23 7f44293f8d feat: Implement Topbar Autohide options 2025-08-10 08:43:39 -04:00
purian23 11173aac96 feat: Opt to display notifications over fullscreen apps 2025-08-10 08:15:59 -04:00
purian23 a73da19db2 fix: Notification timestamp tracking 2025-08-10 07:40:56 -04:00
bbedward fa14cc6141 make text scroll speed relative to text length 2025-08-10 00:36:51 -04:00
bbedward 1e6191bc33 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-08-10 00:19:17 -04:00
bbedward 4b4fdfa72c Scrolling text media widget and larger size variant 2025-08-10 00:18:48 -04:00
purian23 f69b599c43 Update process handling 2025-08-10 00:02:48 -04:00
bbedward 42abed62fe add wallpaper cycling feature 2025-08-09 23:50:45 -04:00
bbedward 01b5ae46b1 try to minimize bad data before enough cpu samples are done 2025-08-09 21:01:39 -04:00
bbedward 2d74753ab4 correct dankgop sampling approach 2025-08-09 20:33:21 -04:00
bbedward 218eefbe19 Revert "Implement some data smoothing"
This reverts commit 8bef7ded1e.
2025-08-09 20:01:26 -04:00
bbedward 8bef7ded1e Implement some data smoothing 2025-08-09 17:26:57 -04:00
bbedward b3310e3edd dankgop sampling version 2025-08-09 17:16:30 -04:00
bbedward 3f5eb1844c migrate to dankgop monitoring 2025-08-09 16:50:02 -04:00
bbedward a0c5f31ffa second path to enable GPU temps 2025-08-09 10:53:17 -04:00
bbedward 44277cd0f3 small tweak to GPU when temp not available 2025-08-09 00:39:04 -04:00
bbedward b65917113a lighter sysmon script 2025-08-09 00:07:43 -04:00
bbedward 8a4db0b3d7 separate scripts 2025-08-08 23:49:03 -04:00
bbedward e20d0be73d GPU warning 2025-08-08 22:53:03 -04:00
bbedward 6b44c93547 subtle update 2025-08-08 21:19:06 -04:00
bbedward f973f1b16c separate static sysinfo call 2025-08-08 20:44:25 -04:00
bbedward 0a22565cbd anotehr qmlfmt round and extra GPU data 2025-08-08 19:17:53 -04:00
bbedward 6c8e6568dc Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-08-08 17:05:17 -04:00
bbedward 927dc507da fix sysmon errors 2025-08-08 17:03:00 -04:00
bbedward bae2bac8f4 show cpu percent relative to a core 2025-08-08 16:32:59 -04:00
bbedward 682a8cd7d6 fix the f'd lock screen 2025-08-08 16:29:24 -04:00
bbedward 3bc7baa19a fix cpu burning of privacy indicator 2025-08-08 16:07:26 -04:00
bbedward f5e52c55c0 corner radius unified + setting 2025-08-08 16:05:10 -04:00
bbedward 4d408c65f2 replace qmlformat with a better tool
still not perfect, but well - what can ya do
2025-08-08 15:55:37 -04:00
bbedward 8dc6e2805d Merge branch 'master' of github.com:bbedward/dank-material-dark-shell 2025-08-08 14:44:28 -04:00
bbedward 03276b7d9a feat: add temperature widgets, separate ram/cpu widgets, update
calculations
- make CPU calculations per-process mirror gnome (of all CPUs)
2025-08-08 14:43:43 -04:00
BB c1f55b39e4 Merge pull request #29 from cashmeredev/master
Nix flake keybinding configuration fix
2025-08-08 13:26:51 -04:00
bbedward 105353c2a4 Merge branch 'master' of github.com:bbedward/dank-material-dark-shell 2025-08-08 12:28:17 -04:00
bbedward ac68245af5 replace dumb function 2025-08-08 12:28:08 -04:00
cashmere 5bc91ffa3d fix dynamic system declaration 2025-08-08 16:56:08 +02:00
cashmere 0abdd73a8d do not compile whole niri project 2025-08-08 16:51:54 +02:00
cashmere 0f6d10ffeb fix set keybind error. 2025-08-08 16:30:35 +02:00
purian23 78a40d3677 Updated scrollbar base 2025-08-07 21:48:29 -04:00
bbedward faa2fb8a4a Update theme doc in readme 2025-08-07 18:25:57 -04:00
bbedward 4523528d77 fix warnings 2025-08-07 17:33:15 -04:00
bbedward d84e48f5dd Fix tray menu position 2025-08-07 17:20:46 -04:00
bbedward 9b7ca88e11 native tray menu 2025-08-07 17:01:52 -04:00
bbedward 3dae0ec5e3 niri: connect directly to socket rather than running commands 2025-08-07 15:52:02 -04:00
bbedward fc13ed5c3a Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-08-07 15:24:35 -04:00
bbedward 98d2ca24a8 workaround for all the terrible default QT scrolling behavior
- DankGridView, DankFlickable, DankListView\
- Touchpad behavior greatly improved by preserving momentum
- Fixed janky nested scrollers in control center
2025-08-07 15:22:40 -04:00
bbedward 02ab8e2db5 scroll improvement 2025-08-07 12:00:24 -04:00
bbedward 07c33b42eb readme polkit update 2025-08-07 11:45:30 -04:00
bbedward bbfb38e391 load dankshell pam config if exists 2025-08-07 10:10:09 -04:00
bbedward 6cacdb1083 fix clipboard hist modal 2025-08-07 08:59:12 -04:00
bbedward 3bd3c511b7 remove theme env overrides 2025-08-07 08:54:35 -04:00
purian23 d2ba398d68 Update more scrolling enhancements 2025-08-07 00:14:56 -04:00
purian23 722fd36c0a Updates to scrolling logic 2025-08-06 23:18:30 -04:00
bbedward 633927da9a also check mute 2025-08-06 23:06:34 -04:00
bbedward 16d4a75283 simplify privacy service 2025-08-06 22:31:55 -04:00
purian23 d036684842 feat: Initial Privacy inidicators implementation 2025-08-06 20:48:38 -04:00
bbedward 732f31d3c1 fix wifi password modal 2025-08-06 20:33:19 -04:00
bbedward 1005edae0a calculate memory by pss instead of rss 2025-08-06 18:36:52 -04:00
bbedward abed2e1e3c add brightness and micmute OSDs, fix audio tab bug 2025-08-06 17:33:12 -04:00
bbedward 6b74d109dc update words 2025-08-06 17:00:21 -04:00
bbedward a2736c0854 Merge branch 'master' of github.com:bbedward/dank-material-dark-shell 2025-08-06 16:55:53 -04:00
bbedward 55bf61ee00 add issue templates 2025-08-06 16:55:43 -04:00
BB 2162d355cd Merge pull request #17 from bluelinden/master
Finalize home manager module
2025-08-06 14:01:34 -04:00
blue linden f50541b2d9 Merge branch 'bbedward:master' into master 2025-08-06 13:01:47 -04:00
blue linden 5daba918a1 Get home-manager module working 2025-08-06 13:00:57 -04:00
bbedward 055fee576c switch to state dir and dock fixes 2025-08-06 12:00:09 -04:00
bbedward 18aa557ef1 config refacotr: separate settings.json, session.json, appusage.json 2025-08-06 11:47:24 -04:00
blue linden 3950ef2429 Autostart DankMaterialShell on Niri startup 2025-08-06 09:15:11 -04:00
BB defc50eec6 Merge pull request #12 from bluelinden/master
Add a Nix Flake containing a Home Manager module and a package
2025-08-05 23:43:25 -04:00
BB 383a22cee5 Merge pull request #11 from lonerOrz/uptime
fix(uptime): replace 'uptime -p' with /proc/uptime parsing
2025-08-05 23:41:49 -04:00
blue linden be2ed80d36 Merge branch 'bbedward:master' into master 2025-08-05 23:27:45 -04:00
blue linden 90efd89ba4 Create package and (untested) Home Manager module 2025-08-05 23:26:32 -04:00
loner 4d2a65dc7e fix(uptime): replace 'uptime -p' with /proc/uptime parsing
On NixOS, the default `uptime` command from coreutils does not support
the `-p` option.
This replaces the previous `bash`-based solution with a call to
`/proc/uptime`,
and parses the result in JavaScript to produce a human-readable format
like:

    "up 1 day, 2 hours, 5 minutes"

This makes uptime reporting fully compatible with NixOS and more
portable.
2025-08-06 11:01:42 +08:00
purian23 7fcdf1ea88 Update brightnessctl logic 2025-08-05 22:58:28 -04:00
purian23 7e21453533 Remove false screen backup 2025-08-05 22:10:09 -04:00
bbedward e132a754e8 fix control center positioning 2025-08-05 22:04:25 -04:00
purian23 7695078cb8 Implement compact toggles for clock & media topbar widgets 2025-08-05 20:33:01 -04:00
bbedward 1bd4eeb736 make screen to popouts for positioning data 2025-08-05 18:08:13 -04:00
bbedward bc11194454 fix default center widgets 2025-08-05 17:43:24 -04:00
bbedward 081f8533c4 fix light/dark toggle and settings not re-opening after escape 2025-08-05 17:32:53 -04:00
bbedward c88dc67dbc add disable weather option 2025-08-05 15:57:01 -04:00
bbedward 55bb63f2a5 fix nil error 2025-08-05 15:42:16 -04:00
bbedward 283673a314 integrate light/dark mode and profile image with desktop portal 2025-08-05 15:40:34 -04:00
bbedward 13f37ed48d different name 2025-08-05 09:21:31 -04:00
bbedward 154b1e12f6 create-release alternative 2025-08-05 09:18:55 -04:00
bbedward fccaf06baa escape key handling 2025-08-05 09:15:27 -04:00
bbedward ef9e80bfdf awlasy center centcom 2025-08-05 08:21:31 -04:00
bbedward f064ce791a fix app drawer context menus 2025-08-05 08:09:28 -04:00
bbedward 54d8ca450f weather auto location option 2025-08-05 07:34:46 -04:00
bbedward 4bf525ce0d fix enum ref 2025-08-05 07:28:52 -04:00
bbedward 7021061ffd notifs/dock on TopLayer 2025-08-05 07:22:33 -04:00
bbedward 16e3b18e8e Make popups relative based on top bar position 2025-08-04 23:42:35 -04:00
bbedward ccd9f00201 fix lockscreen screenshot 2025-08-04 21:50:47 -04:00
bbedward 9c19889ac6 screenshots 2025-08-04 21:48:38 -04:00
bbedward 63243e3ae5 readme updoot 2025-08-04 21:33:55 -04:00
bbedward 739f3eff01 small fix 2025-08-04 19:13:10 -04:00
bbedward 436c7e2234 dock: create an initial basic dock 2025-08-04 19:10:20 -04:00
bbedward 9dbb6b094f small fix to calendar sizing 2025-08-02 22:17:10 -04:00
bbedward 7e5e3a3914 ghostty in readme 2025-08-02 16:45:10 -04:00
bbedward 599118c63e General code cleanups 2025-08-02 16:18:12 -04:00
bbedward 2cf7497324 fix process monitor CPU calculations 2025-08-02 14:55:33 -04:00
bbedward 21c40b58bc widgets: add spacer, divider, tweak interface 2025-08-02 13:10:39 -04:00
bbedward 2e85494236 always center topbar widgets 2025-08-02 12:19:33 -04:00
bbedward 723a9b1824 Merge branch 'master' of github.com:bbedward/dank-material-dark-shell 2025-08-02 12:13:31 -04:00
bbedward be301f829f make cava a singleton 2025-08-02 12:13:05 -04:00
purian23 8fb8281988 Logic updates to re-ordering topbar widgets 2025-08-01 23:14:19 -04:00
purian23 12c62720b3 Show default font to reflect users choice 2025-08-01 23:02:13 -04:00
bbedward 2f97c39647 lookup notif name by desktop entry if not present 2025-08-01 19:51:03 -04:00
bbedward 379f4f9933 add settings IPC 2025-08-01 13:57:49 -04:00
purian23 c794944f01 fix: Restore SystemTray 2025-08-01 13:28:04 -04:00
purian23 6293b0e502 Merge remote-tracking branch 'origin/master' 2025-08-01 12:32:05 -04:00
purian23 f66b5181ce feat: Implement drag & drop topbar widget sections in settings 2025-08-01 12:29:21 -04:00
bbedward 18ee37e18e theme revisions 2025-08-01 09:29:31 -04:00
bbedward be6ef5fc77 fix qt auto-theme 2025-08-01 08:06:21 -04:00
bbedward ce9f4efb5d doc tweaks 2025-07-31 22:52:46 -04:00
bbedward 9f9352b84a colloid note 2025-07-31 22:29:17 -04:00
bbedward b1b2177f51 theme: update import in gtk.css 2025-07-31 22:26:12 -04:00
purian23 b26d83d301 Fedora compatibility & Icon theme updates 2025-07-30 19:10:46 -04:00
purian23 7b14094155 Hide system tray widget when empty 2025-07-30 18:25:04 -04:00
purian23 d8ab38a0af Restore Wifi Services UI 2025-07-29 22:20:06 -04:00
purian23 35c6efdcc5 Optimize services to reduce load 2025-07-29 21:56:41 -04:00
purian23 df147ff9b9 Update Readme w/Fedora guides & improvements 2025-07-29 11:36:36 -04:00
purian23 d656502342 Update auto theme logic 2025-07-28 21:35:52 -04:00
purian23 102b248cad feat: Integrate full shell dynamic theming system 2025-07-28 18:29:16 -04:00
bbedward 99e890cc17 ipc for light/dark mode 2025-07-28 15:31:27 -04:00
bbedward 199c1c3043 Dont double append icons 2025-07-28 14:17:08 -04:00
bbedward bde33b334a bad import removal 2025-07-28 13:31:47 -04:00
bbedward ad6ab67906 remove some outdated stuff 2025-07-28 13:30:13 -04:00
bbedward fd6cfcc2e6 Remove outdated check 2025-07-28 13:27:27 -04:00
bbedward b1db088828 update workspace indicators and qmlformat 2025-07-28 13:16:27 -04:00
bbedward 4581544585 weather: general improvements to setting 2025-07-28 12:57:59 -04:00
bbedward 3ca7e3a888 Remove PlainText from StyledText 2025-07-28 11:51:29 -04:00
bbedward 2ff9c54abb elide system text 2025-07-28 11:50:04 -04:00
bbedward e17bd982fe make icon theme searchable 2025-07-28 11:47:55 -04:00
bbedward d4e5e60e7b Adjust fonts 2025-07-28 11:41:48 -04:00
bbedward 73e1f0b6c0 font tweaks 2025-07-28 11:38:49 -04:00
bbedward 03b8370c14 font re-work, change default to inter variable + fira code 2025-07-28 11:24:24 -04:00
bbedward f16e7a506e more mono fonts 2025-07-27 23:37:29 -04:00
bbedward d9e6302ef8 Merge branch 'master' of github.com:bbedward/dank-material-dark-shell 2025-07-27 23:26:31 -04:00
bbedward b119fd511e Implement font family changes 2025-07-27 23:25:12 -04:00
purian23 2e84e5f74b Update notification dismiss definitions 2025-07-27 23:05:38 -04:00
bbedward 420891e047 Fix icon theme change 2025-07-27 22:52:16 -04:00
bbedward 77051d0d62 de-dupe scrollview 2025-07-27 20:56:09 -04:00
bbedward fae7f36a24 remove unused functions/dead code 2025-07-27 20:23:11 -04:00
bbedward da70434eba notifications: fix stuck externally dismissed notifs 2025-07-27 18:37:56 -04:00
bbedward 84036c57b6 fix lock screen multi-monitor input 2025-07-27 18:26:33 -04:00
purian23 0290446c0c Bluetooth UI tweak 2025-07-27 14:29:44 -04:00
purian23 384460712c Feat: add clipboard history image preview 2025-07-27 14:21:45 -04:00
purian23 2c8854f886 Add DnD tooltip 2025-07-27 00:40:02 -04:00
purian23 cb676d02b8 Update logic to conditionally hide focusedApp & systemTray if no data is available 2025-07-27 00:12:54 -04:00
purian23 43f7ae89cc Feature: Add topbar widget transparency appearance settings 2025-07-27 00:02:47 -04:00
purian23 ec69c64b1a Update notification content spacing and group color. 2025-07-26 22:29:13 -04:00
purian23 a681593eda Address active warnings 2025-07-26 22:13:09 -04:00
bbedward 0affd73f32 remove debug timer 2025-07-26 17:03:29 -04:00
bbedward e160a41347 fix app searching sorting with usage rankings 2025-07-26 16:50:57 -04:00
bbedward d2d8c4d74d cleanup markdown better, don't process code block content 2025-07-26 16:37:45 -04:00
bbedward 1472f00f4b use variants for multi display volume popup/toast/notification 2025-07-26 16:24:35 -04:00
bbedward 01a94a17de notifications: try to prevent zombies better, markdown, re-org 2025-07-26 16:08:58 -04:00
bbedward 484a947127 notification: general improvements to popups 2025-07-26 13:25:39 -04:00
bbedward ec1bf7c110 notifications: improve general layout and visual b ugs 2025-07-26 11:41:25 -04:00
bbedward 7b8efd9114 revise notification position and animations 2025-07-26 10:08:56 -04:00
purian23 308d84bfa1 fix: notification warnings & updates 2025-07-26 01:27:40 -04:00
bbedward 39e3107db6 notifications: reverse order top to bottom, fix warnings 2025-07-26 00:27:59 -04:00
bbedward 3b5ddab1ce notifications: improve resource usage 2025-07-25 23:06:07 -04:00
bbedward ed7370361e notifications: initial refactory of popups 2025-07-25 22:39:01 -04:00
bbedward d28df49f18 standardize some opacity colors 2025-07-25 18:19:22 -04:00
bbedward 1157b0336f revise listview/gridview scrolling 2025-07-25 17:46:03 -04:00
bbedward f26c01916b execDetached for process menu operations 2025-07-25 17:21:51 -04:00
bbedward e14afcefd4 notifications: improve notification center 2025-07-25 16:52:58 -04:00
bbedward 98ddc2805b fix readme 2025-07-25 11:45:35 -04:00
bbedward 3825486829 fix reactivity of app launcher 2025-07-25 11:28:11 -04:00
bbedward 786b8f3fa1 audio: create and connect volume popup to IPCs 2025-07-25 11:24:27 -04:00
purian23 1cf252599b Update to use existing DankSlider codebase 2025-07-24 23:13:23 -04:00
purian23 8e7bcabcf7 Add percentage to the volume control slider 2025-07-24 22:46:11 -04:00
bbedward 1197b9385c styledrect in widgets 2025-07-24 21:07:46 -04:00
bbedward 14a9828296 notifications: modularize 2025-07-24 20:58:57 -04:00
bbedward 0068774a8b subtle redesigns 2025-07-24 20:32:28 -04:00
bbedward aabb0ffcc5 filebrowser: bug fixes 2025-07-24 19:55:41 -04:00
bbedward fc52aa2c7a uncomment toast, and format 2025-07-24 19:46:05 -04:00
bbedward 762785b27a calendar: fix events not loading for right month 2025-07-24 19:19:26 -04:00
bbedward 4086e914a7 stylistic updates 2025-07-24 18:44:19 -04:00
bbedward 366930fc03 launcher: sort by usage frequency 2025-07-24 18:36:23 -04:00
bbedward 99f17de065 Merge branch 'master' of github.com:bbedward/dank-material-dark-shell 2025-07-24 16:29:44 -04:00
bbedward 0208f1e840 dankslider: tooltip with value 2025-07-24 16:29:31 -04:00
purian23 0db026f775 Recalculate compact modes 2025-07-24 16:05:30 -04:00
bbedward 0e968d910d settings: redesign 2025-07-24 16:02:53 -04:00
bbedward c6df2bc11d smarter distance calculation 2025-07-24 15:09:28 -04:00
bbedward 8bcac36be3 lockscreen: implement 2025-07-24 14:56:58 -04:00
purian23 71be8bdeec Update focused app & media control screen response 2025-07-24 14:01:22 -04:00
bbedward e3e3788a37 meta: many resource usage improvements and consolidations 2025-07-24 12:44:11 -04:00
bbedward ee2cbd708d meta: integrate wallpaper, FileBrowser, StateLayer
- A lot of this is implements patterns implemented by soramannew's
  caelestia-shell
2025-07-23 23:20:11 -04:00
purian23 a0735db7a4 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-07-23 18:48:39 -04:00
purian23 8c11e2f0cf Half Dank Status Achieved 2025-07-23 18:47:35 -04:00
bbedward 71f8b8ce9a fix context menus and general stuff 2025-07-23 18:30:43 -04:00
purian23 bb2a410039 Fix 2nd tier expansion 2025-07-23 17:40:00 -04:00
bbedward 4f63d5899b refactor: perf improvement stopping singletons with ref
Also switch to scale+opacity anims with custom curve
2025-07-23 16:54:19 -04:00
bbedward 2c57487046 appdrawer: remove background effect 2025-07-23 15:37:53 -04:00
bbedward 94b10159a9 refactor: app drawer de-dupe 2025-07-23 15:36:29 -04:00
bbedward c01da89311 mecha refactoring 2025-07-23 15:15:51 -04:00
bbedward 19adcf3578 refactor: mega refactoring of a bunch of things 2025-07-23 11:56:18 -04:00
bbedward 14eef59c9f refactors: more DankModals, remove GlobalDropdown and InputDialog 2025-07-23 11:11:56 -04:00
bbedward 56a857939b Merge branch 'master' of github.com:bbedward/dank-material-dark-shell 2025-07-23 10:04:21 -04:00
bbedward 70f8a127c6 processlist: breakout and de-dupe code, change styles
Also reverts bluetooth pairing modals since quickshell did not approve
of the PR
2025-07-23 10:03:29 -04:00
purian23 5b40396a8a Level up notifications 2025-07-23 00:04:16 -04:00
bbedward e94d2af9ae fix auto focusing text fields 2025-07-22 23:51:18 -04:00
bbedward 9c8c25711c sizing changes 2025-07-22 23:35:48 -04:00
bbedward 54a7281fa3 fix sizing of dialogs, re-add ipcs 2025-07-22 23:00:09 -04:00
bbedward 7232d66b5d layer effect instead 2025-07-22 21:20:55 -04:00
bbedward 2f50821db4 Focus text inputs by default 2025-07-22 21:16:00 -04:00
bbedward f1833a81a0 Add DankModal and SystemLogo widgets 2025-07-22 21:12:19 -04:00
bbedward 5cf04aa941 settings: complete re-organize and breakout 2025-07-22 19:51:16 -04:00
bbedward 3157622c8b bluetooth: pairing dialog
- Allows connecting pin/passkey/confirmation dialogs
- Fixes inability to connect to many devices
- Dependent on un-merged quickshell PR: https://github.com/quickshell-mirror/quickshell/pull/138
2025-07-22 19:20:10 -04:00
bbedward 02bd9bbc72 networking: improve wifi experience and bugs 2025-07-22 11:50:07 -04:00
bbedward a4da6921bd settings: restore location search for weather 2025-07-21 22:35:16 -04:00
bbedward 9730e4edae refactor: common DankTextField widget 2025-07-21 22:10:46 -04:00
bbedward 303dc70c28 clipboard: fix focus warning on exit 2025-07-21 21:37:19 -04:00
bbedward de8235d94f refactor: create DankActionButton and implement 2025-07-21 21:06:03 -04:00
bbedward dbf2b58b7f fix focus window lookups 2025-07-21 20:56:33 -04:00
bbedward 42183b308e Fix focused window 2025-07-21 20:52:58 -04:00
bbedward 4238bb4638 refactor: replace SettingsToggle with DankToggle 2025-07-21 20:46:57 -04:00
bbedward 1854395a0c Re-add padding as option 2025-07-21 20:43:23 -04:00
bbedward c47ecf8fb0 focusedwindow: fix various issues with workspaces 2025-07-21 17:36:15 -04:00
bbedward 135cf8f027 workspaces: remove arbitrary padding and minimum width 2025-07-21 17:25:00 -04:00
bbedward 6c9e0fb74c network: wifi indicator fix 2025-07-21 16:12:33 -04:00
bbedward 67f1c64dd6 network: cleanup state handling 2025-07-21 16:08:50 -04:00
bbedward 85cf5060e2 no workspace index by default 2025-07-21 15:53:34 -04:00
bbedward 81c232eb20 Improve NetworkTab view when wifi and ethernet interfaces present 2025-07-21 15:52:10 -04:00
bbedward 56662608d1 Remove pointless connections 2025-07-21 14:02:18 -04:00
purian23 5189a34d40 Add 3 workspace indicators by default 2025-07-21 01:20:10 -04:00
purian23 92908b6e0e DND Hotfix 2025-07-21 00:50:33 -04:00
purian23 6f54e592ba Implement user icon theme selection in settings 2025-07-21 00:34:45 -04:00
purian23 79a600cf0d Enhanced Notification System & Settings 2025-07-20 19:07:48 -04:00
purian23 30983b50d5 Remove redundant network header verbiage 2025-07-19 22:49:11 -04:00
purian23 2454d707d9 Fix workspace swticher index input 2025-07-19 22:41:48 -04:00
Purian23 a28764e8f9 Merge pull request #5 from Amadoabad/indexedWorkspaces
Added number indexing option for the workspace switcher
2025-07-19 22:34:43 -04:00
purian23 0d153c407d Combine network tab interface 2025-07-19 20:52:01 -04:00
amadoabad fa3d02552a Added number indexing option for the workspace switcher 2025-07-20 00:03:39 +03:00
bbedward 4202b8323b Remove dead code 2025-07-19 11:45:08 -04:00
bbedward 063e9bb40f bunch of cleanups 2025-07-19 10:56:32 -04:00
purian23 5626335602 Iterative notification updates 2025-07-19 03:02:27 -04:00
purian23 033d8b034b Implement 2nd tier group expansion on notifications 2025-07-18 23:21:10 -04:00
purian23 1355a77fd0 Implement icon for niri screenshots
- Fixed debug icon warning logic
- Add linux os environment script
2025-07-18 21:13:50 -04:00
bbedward f34ecf786a Migrate DankIcon stragglers 2025-07-18 20:02:57 -04:00
bbedward 1d9477bea3 Fix launcher fallback 2025-07-18 19:50:27 -04:00
bbedward b3aa4719d9 Only material icons rounded 2025-07-18 18:58:01 -04:00
bbedward 8d87fca419 bluetooth: trust device before connecting
Fixes devices not staying in history
2025-07-18 18:32:29 -04:00
bbedward 3c7106735c missing dank icons 2025-07-18 12:38:50 -04:00
bbedward d8938e8d15 initial structure refactor 2025-07-18 12:37:37 -04:00
bbedward 3a3f18c298 meta cleanup and refactors 2025-07-18 11:40:17 -04:00
bbedward 06607fa25e Merge branch 'master' of github.com:bbedward/dank-material-dark-shell 2025-07-17 23:20:48 -04:00
bbedward 42927eeb97 audio: un-dumbify audio service 2025-07-17 23:20:36 -04:00
purian23 353fa1220e Notifiy fix 2025-07-17 23:09:46 -04:00
bbedward 930572e1cb bluetooth: don't disable discovery before connect 2025-07-17 23:00:21 -04:00
bbedward 1e6c801507 Fallback icon font 2025-07-17 22:55:14 -04:00
bbedward fbae03de14 Update readme and make nerd fonts optional 2025-07-17 22:15:15 -04:00
bbedward 66671f757b bluetooth: cleanup service 2025-07-17 20:35:40 -04:00
bbedward a66d6c2b55 bluetooth: swap to signalStrength 2025-07-17 20:11:33 -04:00
bbedward 70b9a3cff4 qmlformat-all script 2025-07-17 18:15:35 -04:00
bbedward 26ad3810b4 qmlformat file 2025-07-17 18:00:31 -04:00
bbedward 023b6bc0d1 Initial qmlformat 2025-07-17 17:58:56 -04:00
bbedward cbb42df3a9 bluetooth: cleanup service usage, add rssi 2025-07-17 16:56:28 -04:00
bbedward 60395b5646 Merge branch 'master' of github.com:bbedward/dank-material-dark-shell 2025-07-17 15:22:47 -04:00
bbedward 7a40156893 meta: large-scale refactor progress 2025-07-17 15:21:37 -04:00
purian23 8a57c6a6b1 Notifications updates 2025-07-17 14:51:54 -04:00
bbedward 77cc9c288b refactor: start trimming AI bullcrap patterns 2025-07-17 13:25:35 -04:00
bbedward cc9b575a1d upower: more sensible usage of upower 2025-07-17 10:57:42 -04:00
bbedward 944e0fe129 audio: some cleanups to audio service 2025-07-17 09:08:59 -04:00
bbedward 6484ee0806 audioservice: fix shell crash on resume from sleep 2025-07-16 21:49:41 -04:00
bbedward b3db4a3015 processlist: fix context menu 2025-07-16 21:09:08 -04:00
bbedward 2f7e00a5a5 Missing mousearea 2025-07-16 18:52:20 -04:00
bbedward 457bbcd131 migrate to qs imports and .qmlls.ini pattern 2025-07-16 18:47:51 -04:00
bbedward a49911863d network: tweak preference settings for clarity 2025-07-16 17:20:30 -04:00
bbedward ed17b43d45 network: general improvements, itnegrate preference of wifi/ethernet 2025-07-16 16:35:32 -04:00
bbedward 2a84a6423b power: use native upower APIs 2025-07-16 12:35:46 -04:00
purian23 21bc4ece52 Notifications base overhaul 2025-07-16 01:31:30 -04:00
bbedward 8905b10773 weather: remove IP-based location
Probably better for privacy concerns just to use New York, NY as a
hardcoded default
2025-07-15 22:33:58 -04:00
bbedward e5ec8ad454 Merge branch 'master' of github.com:bbedward/dank-material-dark-shell 2025-07-15 22:25:04 -04:00
bbedward f4a33fe97b weather: fix location override 2025-07-15 22:24:50 -04:00
BB d75f782115 Merge pull request #1 from Amadoabad/patch-1
fixing typo in launch command
2025-07-15 21:40:18 -04:00
bbedward 064a914fec Merge branch 'master' of github.com:bbedward/dank-material-dark-shell 2025-07-15 21:31:48 -04:00
bbedward 994be601ae MultiEffect for rounded icons 2025-07-15 21:31:41 -04:00
purian23 03485a5084 Implemented Mouse wheel volume control on hover over the audio icon 2025-07-15 21:19:18 -04:00
purian23 b2b664f581 Update brightness controls for portable PC users 2025-07-15 20:28:03 -04:00
purian23 eb5950f8c8 Fixed battery status on laptops. Updated power profiles. 2025-07-15 18:42:56 -04:00
bbedward 651cb87596 Merge branch 'master' of github.com:bbedward/dank-material-dark-shell 2025-07-15 17:13:42 -04:00
bbedward ac2c327bbb weather: initial support for overriding location 2025-07-15 17:13:24 -04:00
purian23 195d5ec123 Merge branch 'master' of https://github.com/bbedward/dank-material-dark-shell 2025-07-15 16:41:47 -04:00
purian23 4ea04f57b4 Migrate notification system to native Quickshell NotificationServer API
- Replace custom NotificationGroupingService with native NotificationService
- Implement proper image/icon priority system (notification image → app icon → fallback)
- Add NotificationItem with image layering and elegant emoji fallbacks
- Create native popup and history components with smooth animations
- Fix Discord/Vesktop avatar display issues
- Clean up legacy notification components and demos
- Improve Material Design 3 theming consistency
2025-07-15 16:41:34 -04:00
bbedward 13ef607758 display: use ddcutil for brightness changing 2025-07-15 16:22:06 -04:00
bbedward 3df92697e0 processmonitor: add system info tab 2025-07-15 16:08:57 -04:00
bbedward 90999e8ac1 battery: use native UPower service 2025-07-15 15:57:12 -04:00
bbedward b10488c505 cava: pass config via stdin 2025-07-15 15:46:09 -04:00
bbedward 6010b964d8 Slider improvements 2025-07-15 15:37:37 -04:00
bbedward 9035cb750b Use integrated pipewire from quickshell 2025-07-15 15:26:01 -04:00
bbedward b7da76147f bluetooth: switch to native quickshell bluetooth service
removes dependency on bluetoothctl commands
2025-07-15 12:43:22 -04:00
bbedward 1396b0b582 Add Performance tab under Process monitor 2025-07-15 11:12:59 -04:00
Ahmed Abdo c37e3a4ea2 fixing typo in launch command 2025-07-15 16:21:19 +03:00
bbedward 0349b2c361 Fix cropped profile image 2025-07-14 21:56:22 -04:00
bbedward f388840443 Fix cursor style on inputs 2025-07-14 21:53:19 -04:00
purian23 e0bf5df74b Merge branch 'master' of https://github.com/bbedward/dank-material-dark-shell 2025-07-14 20:32:51 -04:00
purian23 56b801c294 Redesign of the notification center 2025-07-14 20:32:06 -04:00
bbedward 9f3358b253 Fix some log warnings 2025-07-14 16:11:40 -04:00
bbedward 6f1e23437c Fix the image input 2025-07-14 15:34:18 -04:00
bbedward ac18849052 Initial process list IPC popout 2025-07-14 15:28:40 -04:00
bbedward c9a256bb79 Remove dynamic borders 2025-07-14 15:06:56 -04:00
bbedward 3b16ae73c7 Popup transparency option 2025-07-14 15:00:58 -04:00
bbedward 0a4453ab3b Fix light mode toggle for matugen themes 2025-07-14 14:43:12 -04:00
bbedward 31f1360d1b ipc wallpaper handler, fix high CPU usage 2025-07-14 14:36:08 -04:00
bbedward 58d711b3e9 Fix event animation 2025-07-14 13:54:06 -04:00
bbedward fff7dace3b Animation improvements, fix SysTray 2025-07-14 13:50:00 -04:00
bbedward d5d45b11c3 Improve system monitor display 2025-07-14 13:17:06 -04:00
bbedward 4486e79afe Enhancements to launcher and other warning fixes
- Persist grid/list setting
- Alignment improvement
2025-07-14 12:46:36 -04:00
bbedward 603ac51df0 Better app launchers using rescan, and fuzzysort. GPL license.
FuzzySort was ripped from caelestia, which is the reason for GPL change.
2025-07-14 11:25:18 -04:00
purian23 5c5653a41a Notification updates 2025-07-14 10:51:17 -04:00
purian23 cf5c26522b Initial implementation of grouped notifications 2025-07-14 10:50:48 -04:00
purian23 68bcc2b049 Dep updates 2025-07-13 19:03:55 -04:00
bbedward 6145ec2a80 Column alignment fixes 2025-07-13 15:40:34 -04:00
bbedward a1b6c9e791 Add system resource dropdown 2025-07-13 15:36:41 -04:00
bbedward a9b5e0b09d Better audio device names 2025-07-13 14:39:25 -04:00
bbedward d12d82c43e Clip text 2025-07-13 12:17:11 -04:00
bbedward 19be4c6bbb Configurable widget displays 2025-07-13 11:48:25 -04:00
bbedward 755f13e06b Focused window service 2025-07-13 11:37:33 -04:00
bbedward 7981bbdbc2 Initial README 2025-07-13 11:22:54 -04:00
bbedward c87ec06ed7 Nicer notification click handlign 2025-07-13 11:12:08 -04:00
purian23 75b9c4e224 Updated image & theme logic and added proper close buttons to the notifications 2025-07-13 00:58:52 -04:00
purian23 aeef300ef0 Force circular image cropping on avatars 2025-07-13 00:01:25 -04:00
bbedward 44f6f3ca1a Code cleanups and some keyboard navigation on AppLauncher 2025-07-12 22:47:39 -04:00
bbedward f843d4dc7b Profile image support 2025-07-12 22:33:12 -04:00
bbedward e6821a73a5 Make recent apps a category 2025-07-12 22:00:40 -04:00
bbedward 2e94cf26a7 Conflict fixes 2025-07-12 21:50:42 -04:00
bbedward c2a12bb950 Merge branch 'master' of github.com:bbedward/dank-material-dark-shell 2025-07-12 21:47:10 -04:00
bbedward ed9af263fd Recently used apps 2025-07-12 21:47:02 -04:00
purian23 fff6e95186 Merge remote changes and resolve conflicts 2025-07-12 21:45:51 -04:00
purian23 979bb46a8b Remove duplicate transparency & theme options from the control center display 2025-07-12 21:44:28 -04:00
purian23 58ac3e0942 Remove duplicate transparency & theme options from the control center display 2025-07-12 21:43:09 -04:00
bbedward a6bc8d65e2 General fixes to spacing and control center 2025-07-12 21:31:53 -04:00
purian23 7cbc29423f Update media loading transitions 2025-07-12 19:30:01 -04:00
purian23 8cbbbb8b50 Add media controls on the top bar media widget 2025-07-12 19:23:12 -04:00
purian23 29272f47b0 Update Central Command Center bar widgets padding 2025-07-12 19:03:22 -04:00
purian23 95676fa562 Fix settings crash 2025-07-12 15:44:53 -04:00
bbedward 5a83ba4782 iRemove policy 2025-07-12 15:05:49 -04:00
bbedward 589f3f1907 Control center improvements 2025-07-12 15:05:18 -04:00
bbedward db05496dca Settings globally, refactor some menus 2025-07-12 14:33:50 -04:00
bbedward f61ee93484 Some more consistent spacing 2025-07-12 13:35:01 -04:00
bbedward 8a2b81aafb Implement calendar events with khal 2025-07-12 13:26:09 -04:00
bbedward 095606f6e9 Improve performance of slider 2025-07-12 12:06:33 -04:00
bbedward ba5217f2f1 Redesign center command center 2025-07-12 11:49:41 -04:00
bbedward 2a29f2a0f2 Simplify preferences 2025-07-12 10:58:06 -04:00
bbedward af0522ada5 Many bug fixes for media center, spotlight 2025-07-12 10:47:01 -04:00
purian23 2375b6e7bd Implement transparency background slider 2025-07-12 04:07:25 -04:00
purian23 959c8544bd Implement light & dark mode theme options 2025-07-12 03:53:53 -04:00
purian23 df3a56f9f4 UX update to widget padding heights, updated workspaces 2025-07-12 03:29:03 -04:00
purian23 e76b0ee443 Implement grid view on spotlight launcher. Updated navigation response on both lauchers 2025-07-12 02:46:28 -04:00
bbedward 307cc8700d Refresh app list 2025-07-11 22:49:23 -04:00
bbedward 4318fca4a2 Merge branch 'master' of github.com:bbedward/dank-material-dark-shell 2025-07-11 22:10:03 -04:00
bbedward abb1e09c7b iReposition audio indicator 2025-07-11 22:09:53 -04:00
purian23 ccc3b1fa35 Addidtional clipboard improvements 2025-07-11 22:09:37 -04:00
bbedward 962c56c0ce Modularlize the TopBar 2025-07-11 22:07:54 -04:00
bbedward ecbc835f10 Remove unused files and code 2025-07-11 21:56:13 -04:00
bbedward 0afe57e527 Workspace click fix 2025-07-11 21:50:41 -04:00
bbedward 483ce1cd93 event based workspace switches 2025-07-11 21:36:42 -04:00
purian23 eb6df3318a Fix clipboard theme and added total count 2025-07-11 20:28:08 -04:00
bbedward 2d2f3040b7 App launcher fixes 2025-07-11 20:08:51 -04:00
bbedward 2fc9fa9f8d Break out the ControlCenter into multiple widgets 2025-07-11 18:19:24 -04:00
bbedward 9e34c6fdfa Spotlight initial version 2025-07-11 18:10:27 -04:00
bbedward 75cb1ead2f Auto-detect wallpaper changes, prefs in ~/.config/DankMaterialShell 2025-07-11 16:14:59 -04:00
bbedward 5a9ef71f75 Merge branch 'master' of github.com:bbedward/dank-material-dark-shell 2025-07-11 15:45:25 -04:00
bbedward 688c4b85fb Media fixes 2025-07-11 15:45:18 -04:00
purian23 ea300794b2 Merge remote changes with CPU/RAM monitor widgets 2025-07-11 14:29:42 -04:00
purian23 95dcb25f97 Add CPU & RAM monitor widgets 2025-07-11 14:28:28 -04:00
bbedward d169f5d4a3 iBluetooth repair, input device support, some media player improvements 2025-07-11 13:50:10 -04:00
bbedward b4f73ceb7b Fix audio device change, set-wallpaper, some popup alignment 2025-07-11 12:05:20 -04:00
bbedward 0b4464fe2c Persist theme preference, dynamic auto theme with matugen 2025-07-11 11:41:41 -04:00
bbedward 6e64dfe499 Remove lsb_release dpeendency 2025-07-11 09:49:31 -04:00
bbedward 708fc55dd3 Merge branch 'master' of github.com:bbedward/dank-material-dark-shell 2025-07-11 09:41:52 -04:00
bbedward 4b3a0b15be Update positioning 2025-07-11 09:41:37 -04:00
purian23 f6b25968dc Implement floating panel design 2025-07-11 09:40:37 -04:00
purian23 c9d40bf7fb Implement basic color theme picker in Control Center. Remove color picker icon & App text from launcher button. 2025-07-11 01:13:58 -04:00
purian23 8e4f578b32 App drawer icon updates 2025-07-10 22:32:31 -04:00
purian23 e16dbdb8bf Merge remote changes with battery status & icon size updates 2025-07-10 21:38:35 -04:00
purian23 3a2a31d2ae Fixed battery status and adjusted battery & power icon sizes 2025-07-10 21:21:13 -04:00
bbedward cf1f610eff Scroll bars and fix position 2025-07-10 20:19:10 -04:00
bbedward 052e47614b iSome extra widgets and adjustments 2025-07-10 19:11:35 -04:00
bbedward c4975019e7 Bluetooth improvements, battery widget, notification fixes 2025-07-10 17:44:51 -04:00
bbedward 40b2a3af1e Modularlize the shell 2025-07-10 16:40:04 -04:00
bbedward 7cdeba1625 Fix multi-monitor 2025-07-10 14:14:16 -04:00
bbedward 53687266a1 add shell 2025-07-10 11:28:27 -04:00
BB 2ef160d210 Initial commit 2025-07-10 11:18:24 -04:00
386 changed files with 101136 additions and 32 deletions
+55
View File
@@ -0,0 +1,55 @@
---
name: Bug Report
about: Crashes or unexpected behaviors
title: ""
labels: "bug"
assignees: ""
---
<!-- If your issue is related to ICONS
- Purple and black checkerboards are QT's way of signalling an icon doesn't exist
- FIX: Configure a QT6 or Icon Pack in DMS Settings that has the icon you want
- Follow the [THEMING](https://github.com/AvengeMedia/DankMaterialShell/tree/master?tab=readme-ov-file#theming) section to ensure your QT environment variable is configured correctl for themes.
- Once done, configure an icon theme - either however you normally do with gtk3 or qt6ct, or through the built-in settings modal. -->
<!-- If your issue is related to APP LAUNCHER/DOCK/Running Apps being stale
Quickshell does not ever update its DesktopEntires.
There is an open PR for it, that has been stuck unmerged over there to fix it.
We unfortunately are at the mercy of quickshell to merge it.
Until then, newly installed and removed apps will not react until the
shell is restarted.
-->
## Compositor
- [ ] niri
- [ ] Hyprland
- [ ] Other (specify)
## Description
<!-- Brief description of the issue -->
## Expected Behavior
<!-- Describe what you expected to happen -->
## Steps to Reproduce
<!-- Please provide detailed steps to reproduce the issue -->
1.
2.
3.
## Error Messages/Logs
<!-- Please include any error messages, stack traces, or relevant logs -->
```
Paste error messages or logs here
```
## Screenshots/Recordings
<!-- If applicable, add screenshots or screen recordings -->
+31
View File
@@ -0,0 +1,31 @@
---
name: Request a Feature
about: New widgets, new widget behavior, etc.
title: ""
labels: "enhancement"
assignees: ""
---
## Feature Description
<!-- Brief description of the feature requested -->
## Use Case
<!-- Explain the purpose of this feature/why it'd be useful to you -->
## Compositor
Is this feature specific to one compositor?
- [ ] All compositors
- [ ] niri
- [ ] Hyprland
## Proposed Solution
<!-- If you have any ideas for how to implement this, please share! -->
## Alternatives/Existing Solutions
<!-- Include any similar/pre-existing products that solve this problem -->
+30
View File
@@ -0,0 +1,30 @@
---
name: Request Assistance or Support
about: Help with installation, usage, or general questions.
title: ""
labels: "support"
assignees: ""
---
## Compositor
- [ ] niri
- [ ] Hyprland
- [ ] other
## Description
<!-- Brief description of the support needed -->
## Solutions Tried
<!-- Describe what you've tried so far -->
<!-- Outlining what you've tried so far helps us make improvements to the user experience and documentation to avoid recurrent issues -->
## Configuration Details
<!-- Include any configuration if relevant -->
## Screenshots/Recordings
<!-- If applicable, add screenshots or screen recordings -->
+97
View File
@@ -0,0 +1,97 @@
# Release from a dispatch event from the danklinux repo
name: Create Release from DMS
on:
repository_dispatch:
types: [dms_release]
permissions:
contents: write
concurrency:
group: release-${{ github.event.client_payload.tag }}
cancel-in-progress: true
jobs:
create_release_from_dms:
runs-on: ubuntu-24.04
env:
TAG: ${{ github.event.client_payload.tag }}
DMS_REPO: ${{ github.event.client_payload.dms_repo }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Ensure VERSION and tag
run: |
set -euxo pipefail
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)"
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
# Push commit (if any) and tag
git push --follow-tags origin HEAD
- name: Generate Changelog
id: changelog
run: |
set -e
PREVIOUS_TAG=$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || echo "")
if [ -z "$PREVIOUS_TAG" ]; then
CHANGELOG=$(git log --oneline --pretty=format:"- %s (%h)" | head -50)
else
CHANGELOG=$(git log --oneline --pretty=format:"- %s (%h)" "${PREVIOUS_TAG}..${TAG}")
fi
cat > CHANGELOG.md << EOF
## What's Changed
$CHANGELOG
**Full Changelog**: https://github.com/${{ github.repository }}/compare/${PREVIOUS_TAG}...${TAG}
EOF
echo "changelog<<EOF" >> $GITHUB_OUTPUT
cat CHANGELOG.md >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Create/Update DankMaterialShell Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.TAG }}
name: Release ${{ env.TAG }}
body: ${{ steps.changelog.outputs.changelog }}
draft: false
prerelease: ${{ contains(env.TAG, '-') }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Download DMS release assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euxo pipefail
# gh is preinstalled on ubuntu-24.04; auth via GH_TOKEN env
gh release download "${TAG}" -R "${DMS_REPO}" --dir ./_dms_assets
- name: Attach DMS assets to DankMaterialShell release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.TAG }}
files: _dms_assets/**
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+104
View File
@@ -0,0 +1,104 @@
# C++ objects and libs
*.slo
*.lo
*.o
*.a
*.la
*.lai
*.so
*.so.*
*.dll
*.dylib
# Qt-es
object_script.*.Release
object_script.*.Debug
*_plugin_import.cpp
/.qmake.cache
/.qmake.stash
*.pro.user
*.pro.user.*
*.qbs.user
*.qbs.user.*
*.moc
moc_*.cpp
moc_*.h
qrc_*.cpp
ui_*.h
*.qmlc
*.jsc
Makefile*
*build-*
*.qm
*.prl
# Qt unit tests
target_wrapper.*
# QtCreator
*.autosave
# QtCreator Qml
*.qmlproject.user
*.qmlproject.user.*
# QtCreator CMake
CMakeLists.txt.user*
# QtCreator 4.8< compilation database
compile_commands.json
# QtCreator local machine specific files for imported projects
*creator.user*
*_qmlcache.qrc
UNUSED
.qmlls.ini
CLAUDE-activeContext.md
CLAUDE-temp.md
# Auto-generated theme files
*.generated.*
niri-colors.generated.kdl
ghostty-colors.generated.conf
# Notepad files (should be in ~/.local/state/DankMaterialShell/)
untitled-*.txt
file:*
notepad-files/
result
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Code coverage profiles and other test artifacts
*.out
coverage.*
*.coverprofile
profile.cov
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
go.work.sum
# env file
.env
# Editor/IDE
# .idea/
# .vscode/
View File
+702
View File
@@ -0,0 +1,702 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## AI Guidance
* Ignore GEMINI.md and GEMINI-*.md files
* After receiving tool results, carefully reflect on their quality and determine optimal next steps before proceeding. Use your thinking to plan and iterate based on this new information, and then take the best next action.
* For maximum efficiency, whenever you need to perform multiple independent operations, invoke all relevant tools simultaneously rather than sequentially.
* Before you finish, please verify your solution
* Do what has been asked; nothing more, nothing less.
* NEVER create files unless they're absolutely necessary for achieving your goal.
* ALWAYS prefer editing an existing file to creating a new one.
* NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.
* When you update or modify core context files, also update markdown documentation and memory bank
* When asked to commit changes, exclude CLAUDE.md and CLAUDE-*.md referenced memory bank system files from any commits.
## Memory Bank System
This project uses a structured memory bank system with specialized context files. Always check these files for relevant information before starting work:
### Core Context Files
* **CLAUDE-activeContext.md** - Current session state, goals, and progress (if exists)
* **CLAUDE-patterns.md** - Established code patterns and conventions (if exists)
* **CLAUDE-decisions.md** - Architecture decisions and rationale (if exists)
* **CLAUDE-troubleshooting.md** - Common issues and proven solutions (if exists)
* **CLAUDE-config-variables.md** - Configuration variables reference (if exists)
* **CLAUDE-temp.md** - Temporary scratch pad (only read when referenced)
**Important:** Always reference the active context file first to understand what's currently being worked on and maintain session continuity.
### Memory Bank System Backups
When asked to backup Memory Bank System files, you will copy the core context files above and @.claude settings directory to directory @/path/to/backup-directory. If files already exist in the backup directory, you will overwrite them.
## Project Overview
## Project Overview
This is a Quickshell-based desktop shell implementation with Material Design 3 dark theme. The shell provides a complete desktop environment experience with panels, widgets, and system integration services.
**Architecture**: Modular design with clean separation between UI components (Modules), system services (Services), and shared utilities (Common).
**Compositor Support**: Originally designed for niri, now also fully compatible with Hyprland. Both compositors are supported with their own configuration examples and keybind formats.
## Technology Stack
- **QML (Qt Modeling Language)** - Primary language for all UI components
- **Quickshell Framework** - QML-based framework for building desktop shells
- **Qt/QtQuick** - UI rendering and controls
- **Wayland** - Display server protocol
- **Matugen** - Dynamic theming system for wallpaper-based colors and system app theming
## Development Commands
Since this is a Quickshell-based project without traditional build configuration files, development typically involves:
```bash
# Run the shell (requires Quickshell to be installed)
quickshell -p shell.qml
# Or use the shorthand
qs -p .
# Run with verbose output for debugging
qs -v -p shell.qml
# Code formatting and linting
qmlfmt -t 4 -i 4 -b 250 -w /path/to/file.qml # Format a QML file (requires qmlfmt, do not use qmlformat)
qmllint **/*.qml # Lint all QML files for syntax errors
```
## Architecture Overview
### Modular Structure
The shell follows a clean modular architecture reduced from 4,830 lines to ~250 lines in shell.qml:
```
shell.qml # Main entry point (minimal orchestration)
├── Common/ # Shared resources (12 files)
│ ├── Theme.qml # Material Design 3 theme singleton
│ ├── SettingsData.qml # User preferences and configuration
│ ├── SessionData.qml # Session state management
│ ├── Colors.qml # Dynamic color scheme
│ └── [8 more utility files]
├── Services/ # System integration singletons (20 files)
│ ├── AudioService.qml
│ ├── NetworkService.qml
│ ├── BluetoothService.qml
│ ├── DisplayService.qml
│ ├── NotificationService.qml
│ ├── WeatherService.qml
│ ├── PluginService.qml
│ └── [14 more services]
├── Modules/ # UI components (93 files)
│ ├── TopBar/ # Panel components (13 files)
│ ├── ControlCenter/ # System controls (13 files)
│ ├── Notifications/ # Notification system (12 files)
│ ├── AppDrawer/ # Application launcher (3 files)
│ ├── Settings/ # Configuration interface (11 files)
│ ├── ProcessList/ # System monitoring (8 files)
│ ├── Dock/ # Application dock (6 files)
│ ├── Lock/ # Screen lock system (4 files)
│ └── [23 more module files]
├── Modals/ # Full-screen overlays (10 files)
│ ├── SettingsModal.qml
│ ├── ClipboardHistoryModal.qml
│ ├── ProcessListModal.qml
│ ├── PluginSettingsModal.qml
│ └── [7 more modals]
├── Widgets/ # Reusable UI controls (19 files)
│ ├── DankIcon.qml
│ ├── DankSlider.qml
│ ├── DankToggle.qml
│ ├── DankTabBar.qml
│ ├── DankGridView.qml
│ ├── DankListView.qml
│ └── [13 more widgets]
└── plugins/ # External plugins directory ($CONFIGPATH/DankMaterialShell/plugins/)
└── PluginName/ # Example Plugin structure
├── plugin.json # Plugin manifest
├── PluginNameWidget.qml # Widget component
└── PluginNameSettings.qml # Settings UI
```
### Component Organization
1. **Shell Entry Point** (`shell.qml`)
- Minimal orchestration layer (~250 lines)
- Imports and instantiates components
- Handles global state and property bindings
- Multi-monitor support using Quickshell's `Variants`
2. **Common/** - Shared resources
- `Theme.qml` - Material Design 3 theme singleton with consistent colors, spacing, fonts
- `Utilities.js` - Shared functions for workspace parsing, notifications, menu handling
3. **Services/** - System integration singletons
- **Pattern**: All services use `Singleton` type with `id: root`
- **Independence**: No cross-service dependencies
- **Examples**: AudioService, NetworkService, BluetoothService, DisplayService, WeatherService, NotificationService, CalendarService, BatteryService, NiriService, MprisController
- Services handle system commands, state management, and hardware integration
4. **Modules/** - UI components (93 files)
- **TopBar/**: Panel components with workspace switching, system indicators, media controls
- **ControlCenter/**: System controls for WiFi, Bluetooth, audio, display settings
- **Notifications/**: Complete notification system with center, popups, and keyboard navigation
- **AppDrawer/**: Application launcher with grid/list views and category filtering
- **Settings/**: Comprehensive configuration interface with multiple tabs
- **ProcessList/**: System monitoring with process management and performance metrics
- **Dock/**: Application dock with running apps and window management
- **Lock/**: Screen lock system with authentication
5. **Modals/** - Full-screen overlays (10 files)
- Modal system for settings, clipboard history, file browser, network info, power menu
- Unified modal management with consistent styling and keyboard navigation
6. **Widgets/** - Reusable UI controls (19 files)
- **DankIcon**: Centralized icon component with Material Design font integration
- **DankSlider**: Enhanced slider with animations and smart detection
- **DankToggle**: Consistent toggle switch component
- **DankTabBar**: Unified tab bar implementation
- **DankGridView**: Reusable grid view with adaptive columns
- **DankListView**: Reusable list view with configurable styling
- **DankTextField**: Styled text input with validation
- **DankDropdown**: Dropdown selection component
- **DankPopout**: Base popout component for overlays
- **StateLayer**: Material Design 3 interaction states
- **StyledRect/StyledText**: Themed base components
- **CachingImage**: Optimized image loading with caching
- **DankLocationSearch**: Location picker with search
- **SystemLogo**: Animated system branding component
7. **Plugins/** - External plugin system (`$CONFIGPATH/DankMaterialShell/plugins/`)
- **PluginService**: Discovers, loads, and manages plugin lifecycle
- **Dynamic Loading**: Plugins loaded at runtime from external directory
- **DankBar Integration**: Plugin widgets rendered alongside built-in widgets
- **Settings System**: Per-plugin settings with persistence
### Key Architectural Patterns
1. **Singleton Services Pattern**:
```qml
import QtQuick
import Quickshell
import Quickshell.Io
pragma Singleton
pragma ComponentBehavior: Bound
Singleton {
id: root
property type value: defaultValue
function performAction() { /* implementation */ }
}
```
2. **Smart Feature Detection**: Services detect system capabilities:
```qml
property bool featureAvailable: false
// Auto-hide UI elements when features unavailable
visible: ServiceName.featureAvailable
```
3. **Property Bindings**: Reactive UI updates through property binding
4. **Material Design Theming**: Consistent use of Theme singleton throughout
### Important Components
- **ControlCenter**: System controls (WiFi, Bluetooth, brightness, volume, night mode)
- **AppLauncher**: Full-featured app grid/list with 93+ applications, search, categories
- **ClipboardHistoryModal**: Complete clipboard management with cliphist integration
- **TopBar**: Per-monitor panels with workspace switching, clock, system tray
- **System App Theming**: Automatic GTK and Qt application theming using matugen templates
#### Key Widgets
- **DankIcon**: Centralized icon component with automatic Material Design font detection
- **DankSlider**: Enhanced slider with animations and smart detection
- **DankToggle**: Consistent toggle switch component
- **DankTabBar**: Unified tab bar implementation
- **DankGridView**: Reusable grid view with adaptive columns
- **DankListView**: Reusable list view with configurable styling
## Code Conventions
### QML Style Guidelines
1. **Structure and Formatting**:
- Use 4-space indentation
- `id` should be the first property
- Properties before signal handlers before child components
- Prefer property bindings over imperative code
- **CRITICAL**: NEVER add comments unless absolutely essential for complex logic understanding. Code should be self-documenting through clear naming and structure. Comments are a code smell indicating unclear implementation.
- Use guard statements, example `if (abc) { something() return;} somethingElse();`
- Don't use crazy ternary stuff, but use it for simple if else only. `propertyVal: a ? b : c`
2. **Naming Conventions**:
- **Services**: Use `Singleton` type with `id: root`
- **Components**: Use descriptive names (e.g., `DankSlider`, `TopBar`)
- **Properties**: camelCase for properties, PascalCase for types
3. **Null-Safe Operations**:
- **Use** `object?.property`
4. **Component Structure**:
```qml
// For regular components
Item {
id: root
property type name: value
signal customSignal(type param)
onSignal: { /* handler */ }
Component { /* children */ }
}
// For services (singletons)
Singleton {
id: root
property bool featureAvailable: false
property type currentValue: defaultValue
function performAction(param) { /* implementation */ }
}
```
### Import Guidelines
1. **Standard Import Order**:
```qml
import QtQuick
import QtQuick.Controls // If needed
import Quickshell
import Quickshell.Widgets
import Quickshell.Io // For Process, FileView
import qs.Common // For Theme, utilities
import qs.Services // For service access
import qs.Widgets // For reusable widgets (DankIcon, etc.)
```
2. **Service Dependencies**:
- Services should NOT import other services
- Modules and Widgets can import and use services via property bindings
- Use `Theme.propertyName` for consistent styling
- Use `DankIcon { name: "icon_name" }` for all icons instead of manual Text components
### Component Development Patterns
1. **Code Reuse - Search Before Writing**:
- **ALWAYS** search the codebase for existing functions before writing new ones
- Use `Grep` or `Glob` tools to find existing implementations (e.g., search for "getWifiIcon", "getDeviceIcon")
- Many utility functions already exist in Services/ and Common/ - reuse them instead of duplicating
- Examples of existing utility functions: `Theme.getBatteryIcon()`, `BluetoothService.getDeviceIcon()`, `WeatherService.getWeatherIcon()`
- If similar functionality exists, extend or refactor rather than duplicate
2. **Smart Feature Detection**:
```qml
// In services - detect capabilities
property bool brightnessAvailable: false
// In modules - adapt UI accordingly
DankSlider {
visible: DisplayService.brightnessAvailable
enabled: DisplayService.brightnessAvailable
value: DisplayService.brightnessLevel
}
```
3. **Reusable Components**:
- Create reusable widgets for common patterns (like DankSlider)
- Use configurable properties for different use cases
- Include proper signal handling with unique names (avoid `valueChanged`)
4. **Service Integration**:
- Services expose properties and functions
- Modules and Widgets bind to service properties for reactive updates
- Use service functions for actions: `ServiceName.performAction(value)`
- **CRITICAL**: DO NOT create wrapper functions for everything - bind directly to underlying APIs when possible
- Example: Use `BluetoothService.adapter.discovering = true` instead of `BluetoothService.startScan()`
- Example: Use `device.connect()` directly instead of `BluetoothService.connect(device.address)`
### Error Handling and Debugging
1. **Console Logging**:
```qml
// Use appropriate log levels
console.log("Info message") // General info
console.warn("Warning message") // Warnings
console.error("Error message") // Errors
// Include context in service operations
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Service failed:", serviceName, "exit code:", exitCode)
}
}
```
2. **Graceful Degradation**:
- Always check feature availability before showing UI
- Provide fallbacks for missing system tools
- Use `visible` and `enabled` properties appropriately
## Multi-Monitor Support
The shell uses Quickshell's `Variants` pattern for multi-monitor support:
- Each connected monitor gets its own top bar instance
- Workspace switchers are compositor-aware (Niri and Hyprland)
- Monitors are automatically detected by screen name (DP-1, DP-2, etc.)
- **Niri**: Workspaces are dynamically synchronized with Niri's per-output workspaces
- **Hyprland**: Integrates with Hyprland's workspace system and multi-monitor handling
## Common Development Tasks
### Testing and Validation
When modifying the shell:
1. **Test changes**: `qs -p .` (automatic reload on file changes)
2. **Code quality**: Run `./qmlformat-all.sh` or `qmlformat -i **/*.qml` and `qmllint **/*.qml` to ensure proper formatting and syntax
3. **Performance**: Ensure animations remain smooth (60 FPS target)
4. **Theming**: Use `Theme.propertyName` for Material Design 3 consistency
5. **Wayland compatibility**: Test on Wayland session
6. **Multi-monitor**: Verify behavior with multiple displays
7. **Compositor compatibility**: Test on both Niri and Hyprland when possible
8. **Feature detection**: Test on systems with/without required tools
### Adding New Modules
1. **Create component**:
```bash
# Create new module file
touch Modules/NewModule.qml
```
2. **Follow module patterns**:
- Use `Theme.propertyName` for styling
- Import `qs.Common` and `qs.Services` as needed
- Import `qs.Widgets` for reusable components
- Bind to service properties for reactive updates
- Consider per-screen vs global behavior
- Use `DankIcon` for icons instead of manual Text components
3. **Integration in shell.qml**:
```qml
NewModule {
id: newModule
// Configure properties
}
```
### Adding New Widgets
1. **Create component**:
```bash
# Create new widget file
touch Widgets/NewWidget.qml
```
2. **Follow widget patterns**:
- Use `Theme.propertyName` for styling
- Import `qs.Common` for theming
- Focus on reusability and composition
- Keep widgets simple and focused
- Use `DankIcon` for icons instead of manual Text components
### Adding New Services
1. **Create service**:
```qml
// Services/NewService.qml
import QtQuick
import Quickshell
import Quickshell.Io
pragma Singleton
pragma ComponentBehavior: Bound
Singleton {
id: root
property bool featureAvailable: false
property type currentValue: defaultValue
function performAction(param) {
// Implementation
}
}
```
2. **Use in modules**:
```qml
// In module files
property alias serviceValue: NewService.currentValue
SomeControl {
visible: NewService.featureAvailable
enabled: NewService.featureAvailable
onTriggered: NewService.performAction(value)
}
```
### Creating Plugins
Plugins are external, dynamically-loaded components that extend DankMaterialShell functionality. Plugins are stored in `~/.config/DankMaterialShell/plugins/` and have their settings isolated from core DMS settings.
**Plugin Types:**
- **Widget plugins** (`"type": "widget"` or omit type field): Display UI components in DankBar
- **Daemon plugins** (`"type": "daemon"`): Run invisibly in the background without UI
#### Widget Plugins
1. **Create plugin directory**:
```bash
mkdir -p ~/.config/DankMaterialShell/plugins/YourPlugin
```
2. **Create manifest** (`plugin.json`):
```json
{
"id": "yourPlugin",
"name": "Your Plugin",
"description": "Widget description",
"version": "1.0.0",
"author": "Your Name",
"icon": "extension",
"type": "widget",
"component": "./YourWidget.qml",
"settings": "./YourSettings.qml",
"permissions": ["settings_read", "settings_write"]
}
```
3. **Create widget component** (`YourWidget.qml`):
```qml
import QtQuick
import qs.Services
Rectangle {
id: root
property bool compactMode: false
property string section: "center"
property real widgetHeight: 30
property var pluginService: null
width: content.implicitWidth + 16
height: widgetHeight
radius: 8
color: "#20FFFFFF"
Component.onCompleted: {
if (pluginService) {
var data = pluginService.loadPluginData("yourPlugin", "key", defaultValue)
}
}
}
```
4. **Create settings component** (`YourSettings.qml`):
```qml
import QtQuick
import QtQuick.Controls
FocusScope {
id: root
property var pluginService: null
implicitHeight: settingsColumn.implicitHeight
height: implicitHeight
Column {
id: settingsColumn
anchors.fill: parent
anchors.margins: 16
spacing: 12
Text {
text: "Your Plugin Settings"
font.pixelSize: 18
font.weight: Font.Bold
}
// Your settings UI here
}
function saveSettings(key, value) {
if (pluginService) {
pluginService.savePluginData("yourPlugin", key, value)
}
}
function loadSettings(key, defaultValue) {
if (pluginService) {
return pluginService.loadPluginData("yourPlugin", key, defaultValue)
}
return defaultValue
}
}
```
5. **Enable plugin**:
- Open Settings → Plugins
- Click "Scan for Plugins"
- Toggle plugin to enable
- Add plugin ID to DankBar widget list
#### Daemon Plugins
Daemon plugins run invisibly in the background without any UI components. They're useful for monitoring system events, background tasks, or data synchronization.
1. **Create plugin directory**:
```bash
mkdir -p ~/.config/DankMaterialShell/plugins/YourDaemon
```
2. **Create manifest** (`plugin.json`):
```json
{
"id": "yourDaemon",
"name": "Your Daemon",
"description": "Background daemon description",
"version": "1.0.0",
"author": "Your Name",
"icon": "settings_applications",
"type": "daemon",
"component": "./YourDaemon.qml",
"permissions": ["settings_read", "settings_write"]
}
```
3. **Create daemon component** (`YourDaemon.qml`):
```qml
import QtQuick
import qs.Common
import qs.Services
Item {
id: root
property var pluginService: null
Connections {
target: SessionData
function onWallpaperPathChanged() {
console.log("Wallpaper changed:", SessionData.wallpaperPath)
if (pluginService) {
pluginService.savePluginData("yourDaemon", "lastEvent", Date.now())
}
}
}
Component.onCompleted: {
console.log("Daemon started")
}
}
```
4. **Enable daemon**:
- Open Settings → Plugins
- Click "Scan for Plugins"
- Toggle daemon to enable
- Daemon runs automatically in background
**Example**: See `PLUGINS/WallpaperWatcherDaemon/` for a complete daemon plugin that monitors wallpaper changes
**Plugin Directory Structure:**
```
~/.config/DankMaterialShell/
├── settings.json # Core DMS settings + plugin settings
│ └── pluginSettings: {
│ └── yourPlugin: {
│ ├── enabled: true,
│ └── customData: {...}
│ }
│ }
└── plugins/ # Plugin files directory
└── YourPlugin/ # Plugin directory (matches manifest ID)
├── plugin.json # Plugin manifest
├── YourWidget.qml # Widget component
└── YourSettings.qml # Settings UI (optional)
```
**Key Plugin APIs:**
- `pluginService.loadPluginData(pluginId, key, default)` - Load persistent data
- `pluginService.savePluginData(pluginId, key, value)` - Save persistent data
- `PluginService.enablePlugin(pluginId)` - Load plugin
- `PluginService.disablePlugin(pluginId)` - Unload plugin
**Important Notes:**
- Plugin settings are automatically injected by the PluginService via `item.pluginService = PluginService`
- Settings are stored in the main settings.json but namespaced under `pluginSettings.{pluginId}`
- Plugin directories must match the plugin ID in the manifest
- Use the injected `pluginService` property in both widget and settings components
### Debugging Common Issues
1. **Import errors**: Check import paths
2. **Singleton conflicts**: Ensure services use `Singleton` type with `id: root`
3. **Property binding issues**: Use property aliases for reactive updates
4. **Process failures**: Check system tool availability and command syntax
5. **Theme inconsistencies**: Always use `Theme.propertyName` instead of hardcoded values
### Best Practices Summary
- **Code Reuse**: ALWAYS search existing codebase before writing new functions - avoid duplication at all costs
- **No Comments**: Code should be self-documenting - comments indicate poor naming/structure
- **Modularity**: Keep components focused and independent
- **Reusability**: Create reusable components for common patterns using Widgets/
- **Responsiveness**: Use property bindings for reactive UI
- **Robustness**: Implement feature detection and graceful degradation
- **Consistency**: Follow Material Design 3 principles via Theme singleton
- **Performance**: Minimize expensive operations and use appropriate data structures
- **Icon Management**: Use `DankIcon` for all icons instead of manual Text components
- **Widget System**: Leverage existing widgets (DankSlider, DankToggle, etc.) for consistency
- **NO WRAPPER HELL**: Avoid creating unnecessary wrapper functions - bind directly to underlying APIs for better reactivity and performance
- **Function Discovery**: Use grep/search tools to find existing utility functions before implementing new ones
- **Modern QML Patterns**: Leverage new widgets like DankTextField, DankDropdown, CachingImage
- **Structured Organization**: Follow the established Services/Modules/Widgets/Modals separation
- **Plugin System**: For user extensions, create plugins instead of modifying core modules - see docs/PLUGINS.md
### Common Widget Patterns
1. **Icons**: Always use `DankIcon { name: "icon_name" }` instead of `Text { font.family: Theme.iconFont }`
2. **Sliders**: Use `DankSlider` for consistent styling and behavior
3. **Toggles**: Use `DankToggle` for switches and checkboxes
4. **Tab Bars**: Use `DankTabBar` for tabbed interfaces
5. **Lists**: Use `DankListView` for scrollable lists
6. **Grids**: Use `DankGridView` for grid layouts
7. **Text Fields**: Use `DankTextField` for text input with validation
8. **Dropdowns**: Use `DankDropdown` for selection menus
9. **Popouts**: Use `DankPopout` as base for overlay components
10. **Images**: Use `CachingImage` for optimized image loading
### Essential Utility Functions
Before writing new utility functions, check these existing ones:
**Theme.qml utilities:**
- `getBatteryIcon(level, isCharging, batteryAvailable)` - Battery status icons
- `getPowerProfileIcon(profile)` - Power profile indicators
**Service utilities:**
- `BluetoothService.getDeviceIcon(device)` - Bluetooth device type icons
- `BluetoothService.getSignalIcon(device)` - Signal strength indicators
- `WeatherService.getWeatherIcon(code)` - Weather condition icons
- `AppSearchService.getCategoryIcon(category)` - Application category icons
- `DgopService.getProcessIcon(command)` - Process type icons
- `SettingsData.getWorkspaceNameIcon(workspaceName)` - Workspace icons
**Always search for existing functions using:**
```bash
grep -r "function.*get.*Icon" Services/ Common/
grep -r "function.*" path/to/relevant/directory/
```
+30
View File
@@ -0,0 +1,30 @@
# Contributing
Contributions are welcome and encouraged.
## Formatting
The preferred tool for formatting files is [qmlfmt](https://github.com/jesperhh/qmlfmt) (also available on aur as qmlfmt-git). It actually kinda sucks, but `qmlformat` doesn't work with null safe operators and ternarys and pragma statements and a bunch of other things that are supported.
We need some consistent style, so this at least gives the same formatter that Qt Creator uses.
You can configure it to format on save in vscode by configuring the "custom local formatters" extension then adding this to settings json.
```json
"customLocalFormatters.formatters": [
{
"command": "sh -c \"qmlfmt -t 4 -i 4 -b 250 | sed 's/pragma ComponentBehavior$/pragma ComponentBehavior: Bound/g'\"",
"languages": ["qml"]
}
],
"[qml]": {
"editor.defaultFormatter": "jkillian.custom-local-formatters",
"editor.formatOnSave": true
},
```
Sometimes it just breaks code though. Like turning `"_\""` into `"_""`, so you may not want to do formatOnSave.
## Pull request
Include screenshots/video if applicable in your pull request if applicable, to visualize what your change is affecting.
+25
View File
@@ -0,0 +1,25 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
Singleton {
id: root
readonly property int durShort: 200
readonly property int durMed: 450
readonly property int durLong: 600
readonly property int slidePx: 80
readonly property var emphasized: [0.05, 0.00, 0.133333, 0.06, 0.166667, 0.40, 0.208333, 0.82, 0.25, 1.00, 1.00, 1.00]
readonly property var emphasizedDecel: [0.05, 0.70, 0.10, 1.00, 1.00, 1.00]
readonly property var emphasizedAccel: [0.30, 0.00, 0.80, 0.15, 1.00, 1.00]
readonly property var standard: [0.20, 0.00, 0.00, 1.00, 1.00, 1.00]
readonly property var standardDecel: [0.00, 0.00, 0.00, 1.00, 1.00, 1.00]
readonly property var standardAccel: [0.30, 0.00, 1.00, 1.00, 1.00, 1.00]
}
+127
View File
@@ -0,0 +1,127 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtCore
import QtQuick
import Quickshell
import Quickshell.Io
Singleton {
id: root
property var appUsageRanking: {
}
Component.onCompleted: {
loadSettings()
}
function loadSettings() {
parseSettings(settingsFile.text())
}
function parseSettings(content) {
try {
if (content && content.trim()) {
var settings = JSON.parse(content)
appUsageRanking = settings.appUsageRanking || {}
}
} catch (e) {
}
}
function saveSettings() {
settingsFile.setText(JSON.stringify({
"appUsageRanking": appUsageRanking
}, null, 2))
}
function addAppUsage(app) {
if (!app)
return
var appId = app.id || (app.execString || app.exec || "")
if (!appId)
return
var currentRanking = Object.assign({}, appUsageRanking)
if (currentRanking[appId]) {
currentRanking[appId].usageCount = (currentRanking[appId].usageCount
|| 1) + 1
currentRanking[appId].lastUsed = Date.now()
currentRanking[appId].icon = app.icon || currentRanking[appId].icon
|| "application-x-executable"
currentRanking[appId].name = app.name
|| currentRanking[appId].name || ""
} else {
currentRanking[appId] = {
"name": app.name || "",
"exec": app.execString || app.exec || "",
"icon": app.icon || "application-x-executable",
"comment": app.comment || "",
"usageCount": 1,
"lastUsed": Date.now()
}
}
appUsageRanking = currentRanking
saveSettings()
}
function getRankedApps() {
var apps = []
for (var appId in appUsageRanking) {
var appData = appUsageRanking[appId]
apps.push({
"id": appId,
"name": appData.name,
"exec": appData.exec,
"icon": appData.icon,
"comment": appData.comment,
"usageCount": appData.usageCount,
"lastUsed": appData.lastUsed
})
}
return apps.sort(function (a, b) {
if (a.usageCount !== b.usageCount)
return b.usageCount - a.usageCount
return a.name.localeCompare(b.name)
})
}
function cleanupAppUsageRanking(availableAppIds) {
var currentRanking = Object.assign({}, appUsageRanking)
var hasChanges = false
for (var appId in currentRanking) {
if (availableAppIds.indexOf(appId) === -1) {
delete currentRanking[appId]
hasChanges = true
}
}
if (hasChanges) {
appUsageRanking = currentRanking
saveSettings()
}
}
FileView {
id: settingsFile
path: StandardPaths.writableLocation(
StandardPaths.GenericStateLocation) + "/DankMaterialShell/appusage.json"
blockLoading: true
blockWrites: true
watchChanges: true
onLoaded: {
parseSettings(settingsFile.text())
}
onLoadFailed: error => {}
}
}
+66
View File
@@ -0,0 +1,66 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
Singleton {
id: root
readonly property Rounding rounding: Rounding {}
readonly property Spacing spacing: Spacing {}
readonly property FontSize fontSize: FontSize {}
readonly property Anim anim: Anim {}
component Rounding: QtObject {
readonly property int small: 8
readonly property int normal: 12
readonly property int large: 16
readonly property int extraLarge: 24
readonly property int full: 1000
}
component Spacing: QtObject {
readonly property int small: 4
readonly property int normal: 8
readonly property int large: 12
readonly property int extraLarge: 16
readonly property int huge: 24
}
component FontSize: QtObject {
readonly property int small: 12
readonly property int normal: 14
readonly property int large: 16
readonly property int extraLarge: 20
readonly property int huge: 24
}
component AnimCurves: QtObject {
readonly property list<real> standard: [0.2, 0, 0, 1, 1, 1]
readonly property list<real> standardAccel: [0.3, 0, 1, 1, 1, 1]
readonly property list<real> standardDecel: [0, 0, 0, 1, 1, 1]
readonly property list<real> emphasized: [0.05, 0, 2 / 15, 0.06, 1
/ 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1]
readonly property list<real> emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1]
readonly property list<real> emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1]
readonly property list<real> expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1]
readonly property list<real> expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1]
readonly property list<real> expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1]
}
component AnimDurations: QtObject {
readonly property int quick: 150
readonly property int normal: 300
readonly property int slow: 500
readonly property int extraSlow: 1000
readonly property int expressiveFastSpatial: 350
readonly property int expressiveDefaultSpatial: 500
readonly property int expressiveEffects: 200
}
component Anim: QtObject {
readonly property AnimCurves curves: AnimCurves {}
readonly property AnimDurations durations: AnimDurations {}
}
}
+45
View File
@@ -0,0 +1,45 @@
import Quickshell
pragma Singleton
Singleton {
id: root
// Clear all image cache
function clearImageCache() {
Quickshell.execDetached(["rm", "-rf", Paths.stringify(
Paths.imagecache)])
Paths.mkdir(Paths.imagecache)
}
// Clear cache older than specified minutes
function clearOldCache(ageInMinutes) {
Quickshell.execDetached(
["find", Paths.stringify(
Paths.imagecache), "-name", "*.png", "-mmin", `+${ageInMinutes}`, "-delete"])
}
// Clear cache for specific size
function clearCacheForSize(size) {
Quickshell.execDetached(
["find", Paths.stringify(
Paths.imagecache), "-name", `*@${size}x${size}.png`, "-delete"])
}
// Get cache size in MB
function getCacheSize(callback) {
var process = Qt.createQmlObject(`
import Quickshell.Io
Process {
command: ["du", "-sm", "${Paths.stringify(
Paths.imagecache)}"]
running: true
stdout: StdioCollector {
onStreamFinished: {
var sizeMB = parseInt(text.split("\\t")[0]) || 0
callback(sizeMB)
}
}
}
`, root)
}
}
+62
View File
@@ -0,0 +1,62 @@
import QtQuick
import Quickshell.Io
Item {
id: root
property alias path: socket.path
property alias parser: socket.parser
property bool connected: false
property int reconnectBaseMs: 400
property int reconnectMaxMs: 15000
property int _reconnectAttempt: 0
signal connectionStateChanged()
onConnectedChanged: {
socket.connected = connected
}
Socket {
id: socket
onConnectionStateChanged: {
root.connectionStateChanged()
if (connected) {
root._reconnectAttempt = 0
return
}
if (root.connected) {
root._scheduleReconnect()
}
}
}
Timer {
id: reconnectTimer
interval: 0
repeat: false
onTriggered: {
socket.connected = false
Qt.callLater(() => socket.connected = true)
}
}
function send(data) {
const json = typeof data === "string" ? data : JSON.stringify(data)
const message = json.endsWith("\n") ? json : json + "\n"
socket.write(message)
socket.flush()
}
function _scheduleReconnect() {
const pow = Math.min(_reconnectAttempt, 10)
const base = Math.min(reconnectBaseMs * Math.pow(2, pow), reconnectMaxMs)
const jitter = Math.floor(Math.random() * Math.floor(base / 4))
reconnectTimer.interval = base + jitter
reconnectTimer.restart()
_reconnectAttempt++
}
}
+53
View File
@@ -0,0 +1,53 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
Singleton {
id: root
readonly property var facts: [
"A photon takes 100,000 to 200,000 years bouncing through the Sun's dense core, then races to Earth in just 8 minutes 20 seconds.",
"A teaspoon of neutron star matter would weigh a billion metric tons here on Earth.",
"Right now, 100 trillion solar neutrinos are passing through your body every second.",
"The Sun converts 4 million metric tons of matter into pure energy every second—enough to power Earth for 500,000 years.",
"The universe still glows with leftover heat from the Big Bang—just 2.7 degrees above absolute zero.",
"There's a nebula out there that's actually colder than empty space itself.",
"We've detected black holes crashing together by measuring spacetime stretch by less than 1/10,000th the width of a proton.",
"Fast radio bursts can release more energy in 5 milliseconds than our Sun produces in 3 days.",
"Our galaxy might be crawling with billions of rogue planets drifting alone in the dark.",
"Distant galaxies can move away from us faster than light because space itself is stretching.",
"The edge of what we can see is 46.5 billion light-years away, even though the universe is only 13.8 billion years old.",
"The universe is mostly invisible: 5% regular matter, 27% dark matter, 68% dark energy.",
"A day on Venus lasts longer than its entire year around the Sun.",
"On Mercury, the time between sunrises is 176 Earth days long.",
"In about 4.5 billion years, our galaxy will smash into Andromeda.",
"Most of the gold in your jewelry was forged when neutron stars collided somewhere in space.",
"PSR J1748-2446ad, the fastest spinning star, rotates 716 times per second—its equator moves at 24% the speed of light.",
"Cosmic rays create particles that shouldn't make it to Earth's surface, but time dilation lets them sneak through.",
"Jupiter's magnetic field is so huge that if we could see it, it would look bigger than the Moon in our sky.",
"Interstellar space is so empty it's like a cube 32 kilometers wide containing just a single grain of sand.",
"Voyager 1 is 24 billion kilometers away but won't leave the Sun's gravitational influence for another 30,000 years.",
"Counting to a billion at one number per second would take over 31 years.",
"Space is so vast, even speeding at light-speed, you'd never return past the cosmic horizon.",
"Astronauts on the ISS age about 0.01 seconds less each year than people on Earth.",
"Sagittarius B2, a dust cloud near our galaxy's center, contains ethyl formate—the compound that gives raspberries their flavor and rum its smell.",
"Beyond 16 billion light-years, the cosmic event horizon marks where space expands too fast for light to ever reach us again.",
"Even at light-speed, you'd never catch up to most galaxies—space expands faster.",
"Only around 5% of galaxies are ever reachable—even at light-speed.",
"If the Sun vanished, we'd still orbit it for 8 minutes before drifting away.",
"If a planet 65 million light-years away looked at Earth now, it'd see dinosaurs.",
"Our oldest radio signals will reach the Milky Way's center in 26,000 years.",
"Every atom in your body heavier than hydrogen was forged in the nuclear furnace of a dying star.",
"The Moon moves 3.8 centimeters farther from Earth every year.",
"The universe creates 275 million new stars every single day.",
"Jupiter's Great Red Spot is a storm twice the size of Earth that has been raging for at least 350 years.",
"If you watched someone fall into a black hole, they'd appear frozen at the event horizon forever—time effectively stops from your perspective.",
"The Boötes Supervoid is a cosmic desert 1.8 billion light-years across with 60% fewer galaxies than it should have."
]
function getRandomFact() {
return facts[Math.floor(Math.random() * facts.length)]
}
}
+66
View File
@@ -0,0 +1,66 @@
import QtQuick
import Quickshell
import Quickshell.Io
pragma Singleton
pragma ComponentBehavior: Bound
Singleton {
id: root
property string currentLocale: Qt.locale().name.substring(0, 2)
property var translations: ({})
property bool translationsLoaded: false
FileView {
id: translationLoader
path: root.currentLocale === "en" ? "" : Qt.resolvedUrl(`../translations/${root.currentLocale}.json`)
onLoaded: {
try {
root.translations = JSON.parse(text())
root.translationsLoaded = true
console.log(`I18n: Loaded translations for locale '${root.currentLocale}' (${Object.keys(root.translations).length} contexts)`)
} catch (e) {
console.warn(`I18n: Error parsing translations for locale '${root.currentLocale}':`, e, "- falling back to English")
root.translationsLoaded = false
}
}
onLoadFailed: (error) => {
console.warn(`I18n: Failed to load translations for locale '${root.currentLocale}' (${error}), falling back to English`)
root.translationsLoaded = false
}
}
function tr(term, context) {
if (!translationsLoaded || !translations) {
return term
}
const actualContext = context || term
if (translations[actualContext] && translations[actualContext][term]) {
return translations[actualContext][term]
}
for (const ctx in translations) {
if (translations[ctx][term]) {
return translations[ctx][term]
}
}
return term
}
function trContext(context, term) {
if (!translationsLoaded || !translations) {
return term
}
if (translations[context] && translations[context][term]) {
return translations[context][term]
}
return term
}
}
+16
View File
@@ -0,0 +1,16 @@
pragma Singleton
import Quickshell
import QtQuick
Singleton {
id: modalManager
signal closeAllModalsExcept(var excludedModal)
function openModal(modal) {
if (!modal.allowStacking) {
closeAllModalsExcept(modal)
}
}
}
+61
View File
@@ -0,0 +1,61 @@
pragma Singleton
import Quickshell
import QtCore
Singleton {
id: root
readonly property url home: StandardPaths.standardLocations(
StandardPaths.HomeLocation)[0]
readonly property url pictures: StandardPaths.standardLocations(
StandardPaths.PicturesLocation)[0]
readonly property url data: `${StandardPaths.standardLocations(
StandardPaths.GenericDataLocation)[0]}/DankMaterialShell`
readonly property url state: `${StandardPaths.standardLocations(
StandardPaths.GenericStateLocation)[0]}/DankMaterialShell`
readonly property url cache: `${StandardPaths.standardLocations(
StandardPaths.GenericCacheLocation)[0]}/DankMaterialShell`
readonly property url config: `${StandardPaths.standardLocations(
StandardPaths.GenericConfigLocation)[0]}/DankMaterialShell`
readonly property url imagecache: `${cache}/imagecache`
function stringify(path: url): string {
return path.toString().replace(/%20/g, " ")
}
function expandTilde(path: string): string {
return strip(path.replace("~", stringify(root.home)))
}
function shortenHome(path: string): string {
return path.replace(strip(root.home), "~")
}
function strip(path: url): string {
return stringify(path).replace("file://", "")
}
function mkdir(path: url): void {
Quickshell.execDetached(["mkdir", "-p", strip(path)])
}
function copy(from: url, to: url): void {
Quickshell.execDetached(["cp", strip(from), strip(to)])
}
// ! Spotify and maybe some other apps report the wrong app id in toplevels, hardcode special case
function moddedAppId(appId: string): string {
if (appId === "Spotify")
return "spotify-launcher"
if (appId === "beepertexts")
return "beeper"
if (appId === "home assistant desktop")
return "homeassistant-desktop"
if (appId.includes("com.transmissionbt.transmission"))
return "transmission-gtk"
return appId
}
}
+9
View File
@@ -0,0 +1,9 @@
import QtQuick
import Quickshell
QtObject {
required property Singleton service
Component.onCompleted: service.refCount++
Component.onDestruction: service.refCount--
}
+841
View File
@@ -0,0 +1,841 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtCore
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Services
Singleton {
id: root
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
property bool isLightMode: false
property string wallpaperPath: ""
property string wallpaperLastPath: ""
property string profileLastPath: ""
property bool perMonitorWallpaper: false
property var monitorWallpapers: ({})
property bool perModeWallpaper: false
property string wallpaperPathLight: ""
property string wallpaperPathDark: ""
property var monitorWallpapersLight: ({})
property var monitorWallpapersDark: ({})
property bool doNotDisturb: false
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
property int nightModeEndMinute: 0
property real latitude: 0.0
property real longitude: 0.0
property string nightModeLocationProvider: ""
property var pinnedApps: []
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 var recentColors: []
property bool showThirdPartyPlugins: false
Component.onCompleted: {
if (!isGreeterMode) {
loadSettings()
}
}
function loadSettings() {
if (isGreeterMode) {
parseSettings(greeterSessionFile.text())
} else {
parseSettings(settingsFile.text())
}
}
function parseSettings(content) {
try {
if (content && content.trim()) {
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
wallpaperPathLight = settings.wallpaperPathLight !== undefined ? settings.wallpaperPathLight : ""
wallpaperPathDark = settings.wallpaperPathDark !== undefined ? settings.wallpaperPathDark : ""
monitorWallpapersLight = settings.monitorWallpapersLight !== undefined ? settings.monitorWallpapersLight : {}
monitorWallpapersDark = settings.monitorWallpapersDark !== undefined ? settings.monitorWallpapersDark : {}
doNotDisturb = settings.doNotDisturb !== undefined ? settings.doNotDisturb : false
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false
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
nightModeStartMinute = parseInt(parts[1]) || 0
} else {
nightModeStartHour = settings.nightModeStartHour !== undefined ? settings.nightModeStartHour : 18
nightModeStartMinute = settings.nightModeStartMinute !== undefined ? settings.nightModeStartMinute : 0
}
if (settings.nightModeEndTime !== undefined) {
const parts = settings.nightModeEndTime.split(":")
nightModeEndHour = parseInt(parts[0]) || 6
nightModeEndMinute = parseInt(parts[1]) || 0
} else {
nightModeEndHour = settings.nightModeEndHour !== undefined ? settings.nightModeEndHour : 6
nightModeEndMinute = settings.nightModeEndMinute !== undefined ? settings.nightModeEndMinute : 0
}
latitude = settings.latitude !== undefined ? settings.latitude : 0.0
longitude = settings.longitude !== undefined ? settings.longitude : 0.0
nightModeLocationProvider = settings.nightModeLocationProvider !== undefined ? settings.nightModeLocationProvider : ""
pinnedApps = settings.pinnedApps !== undefined ? settings.pinnedApps : []
selectedGpuIndex = settings.selectedGpuIndex !== undefined ? settings.selectedGpuIndex : 0
nvidiaGpuTempEnabled = settings.nvidiaGpuTempEnabled !== undefined ? settings.nvidiaGpuTempEnabled : false
nonNvidiaGpuTempEnabled = settings.nonNvidiaGpuTempEnabled !== undefined ? settings.nonNvidiaGpuTempEnabled : false
enabledGpuPciIds = settings.enabledGpuPciIds !== undefined ? settings.enabledGpuPciIds : []
wallpaperCyclingEnabled = settings.wallpaperCyclingEnabled !== undefined ? settings.wallpaperCyclingEnabled : false
wallpaperCyclingMode = settings.wallpaperCyclingMode !== undefined ? settings.wallpaperCyclingMode : "interval"
wallpaperCyclingInterval = settings.wallpaperCyclingInterval !== undefined ? settings.wallpaperCyclingInterval : 300
wallpaperCyclingTime = settings.wallpaperCyclingTime !== undefined ? settings.wallpaperCyclingTime : "06:00"
monitorCyclingSettings = settings.monitorCyclingSettings !== undefined ? settings.monitorCyclingSettings : {}
lastBrightnessDevice = settings.lastBrightnessDevice !== undefined ? settings.lastBrightnessDevice : ""
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
recentColors = settings.recentColors !== undefined ? settings.recentColors : []
showThirdPartyPlugins = settings.showThirdPartyPlugins !== undefined ? settings.showThirdPartyPlugins : false
if (!isGreeterMode) {
if (typeof Theme !== "undefined") {
Theme.generateSystemThemesFromCurrentTheme()
}
}
}
} catch (e) {
}
}
function saveSettings() {
if (isGreeterMode) return
settingsFile.setText(JSON.stringify({
"isLightMode": isLightMode,
"wallpaperPath": wallpaperPath,
"wallpaperLastPath": wallpaperLastPath,
"profileLastPath": profileLastPath,
"perMonitorWallpaper": perMonitorWallpaper,
"monitorWallpapers": monitorWallpapers,
"perModeWallpaper": perModeWallpaper,
"wallpaperPathLight": wallpaperPathLight,
"wallpaperPathDark": wallpaperPathDark,
"monitorWallpapersLight": monitorWallpapersLight,
"monitorWallpapersDark": monitorWallpapersDark,
"doNotDisturb": doNotDisturb,
"nightModeEnabled": nightModeEnabled,
"nightModeTemperature": nightModeTemperature,
"nightModeAutoEnabled": nightModeAutoEnabled,
"nightModeAutoMode": nightModeAutoMode,
"nightModeStartHour": nightModeStartHour,
"nightModeStartMinute": nightModeStartMinute,
"nightModeEndHour": nightModeEndHour,
"nightModeEndMinute": nightModeEndMinute,
"latitude": latitude,
"longitude": longitude,
"nightModeLocationProvider": nightModeLocationProvider,
"pinnedApps": pinnedApps,
"selectedGpuIndex": selectedGpuIndex,
"nvidiaGpuTempEnabled": nvidiaGpuTempEnabled,
"nonNvidiaGpuTempEnabled": nonNvidiaGpuTempEnabled,
"enabledGpuPciIds": enabledGpuPciIds,
"wallpaperCyclingEnabled": wallpaperCyclingEnabled,
"wallpaperCyclingMode": wallpaperCyclingMode,
"wallpaperCyclingInterval": wallpaperCyclingInterval,
"wallpaperCyclingTime": wallpaperCyclingTime,
"monitorCyclingSettings": monitorCyclingSettings,
"lastBrightnessDevice": lastBrightnessDevice,
"launchPrefix": launchPrefix,
"wallpaperTransition": wallpaperTransition,
"includedTransitions": includedTransitions,
"acMonitorTimeout": acMonitorTimeout,
"acLockTimeout": acLockTimeout,
"acSuspendTimeout": acSuspendTimeout,
"acHibernateTimeout": acHibernateTimeout,
"batteryMonitorTimeout": batteryMonitorTimeout,
"batteryLockTimeout": batteryLockTimeout,
"batterySuspendTimeout": batterySuspendTimeout,
"batteryHibernateTimeout": batteryHibernateTimeout,
"lockBeforeSuspend": lockBeforeSuspend,
"recentColors": recentColors,
"showThirdPartyPlugins": showThirdPartyPlugins
}, null, 2))
}
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()
}
function setWallpaper(imagePath) {
wallpaperPath = imagePath
if (perModeWallpaper) {
if (isLightMode) {
wallpaperPathLight = imagePath
} else {
wallpaperPathDark = imagePath
}
}
saveSettings()
if (typeof Theme !== "undefined") {
Theme.generateSystemThemesFromCurrentTheme()
}
}
function setWallpaperColor(color) {
wallpaperPath = color
if (perModeWallpaper) {
if (isLightMode) {
wallpaperPathLight = color
} else {
wallpaperPathDark = color
}
}
saveSettings()
if (typeof Theme !== "undefined") {
Theme.generateSystemThemesFromCurrentTheme()
}
}
function clearWallpaper() {
wallpaperPath = ""
saveSettings()
if (typeof Theme !== "undefined") {
if (typeof SettingsData !== "undefined" && SettingsData.theme) {
Theme.switchTheme(SettingsData.theme)
} else {
Theme.switchTheme("blue")
}
}
}
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) {
syncWallpaperForCurrentMode()
}
saveSettings()
if (typeof Theme !== "undefined") {
Theme.generateSystemThemesFromCurrentTheme()
}
}
function setPerModeWallpaper(enabled) {
if (enabled && wallpaperCyclingEnabled) {
setWallpaperCyclingEnabled(false)
}
if (enabled && perMonitorWallpaper) {
var monitorCyclingAny = false
for (var key in monitorCyclingSettings) {
if (monitorCyclingSettings[key].enabled) {
monitorCyclingAny = true
break
}
}
if (monitorCyclingAny) {
var newSettings = Object.assign({}, monitorCyclingSettings)
for (var screenName in newSettings) {
newSettings[screenName].enabled = false
}
monitorCyclingSettings = newSettings
}
}
perModeWallpaper = enabled
if (enabled) {
if (perMonitorWallpaper) {
monitorWallpapersLight = Object.assign({}, monitorWallpapers)
monitorWallpapersDark = Object.assign({}, monitorWallpapers)
} else {
wallpaperPathLight = wallpaperPath
wallpaperPathDark = wallpaperPath
}
} else {
syncWallpaperForCurrentMode()
}
saveSettings()
if (typeof Theme !== "undefined") {
Theme.generateSystemThemesFromCurrentTheme()
}
}
function setMonitorWallpaper(screenName, path) {
var newMonitorWallpapers = Object.assign({}, monitorWallpapers)
if (path && path !== "") {
newMonitorWallpapers[screenName] = path
} else {
delete newMonitorWallpapers[screenName]
}
monitorWallpapers = newMonitorWallpapers
if (perModeWallpaper) {
if (isLightMode) {
var newLight = Object.assign({}, monitorWallpapersLight)
if (path && path !== "") {
newLight[screenName] = path
} else {
delete newLight[screenName]
}
monitorWallpapersLight = newLight
} else {
var newDark = Object.assign({}, monitorWallpapersDark)
if (path && path !== "") {
newDark[screenName] = path
} else {
delete newDark[screenName]
}
monitorWallpapersDark = newDark
}
}
saveSettings()
if (typeof Theme !== "undefined" && typeof Quickshell !== "undefined") {
var screens = Quickshell.screens
if (screens.length > 0 && screenName === screens[0].name) {
Theme.generateSystemThemesFromCurrentTheme()
}
}
}
function getMonitorWallpaper(screenName) {
if (!perMonitorWallpaper) {
return wallpaperPath
}
return monitorWallpapers[screenName] || wallpaperPath
}
function setLastBrightnessDevice(device) {
lastBrightnessDevice = device
saveSettings()
}
function setLaunchPrefix(prefix) {
launchPrefix = prefix
saveSettings()
}
function setWallpaperTransition(transition) {
wallpaperTransition = transition
saveSettings()
}
function setAcMonitorTimeout(timeout) {
acMonitorTimeout = timeout
saveSettings()
}
function setAcLockTimeout(timeout) {
acLockTimeout = timeout
saveSettings()
}
function setAcSuspendTimeout(timeout) {
acSuspendTimeout = timeout
saveSettings()
}
function setBatteryMonitorTimeout(timeout) {
batteryMonitorTimeout = timeout
saveSettings()
}
function setBatteryLockTimeout(timeout) {
batteryLockTimeout = timeout
saveSettings()
}
function setBatterySuspendTimeout(timeout) {
batterySuspendTimeout = timeout
saveSettings()
}
function setAcHibernateTimeout(timeout) {
acHibernateTimeout = timeout
saveSettings()
}
function setBatteryHibernateTimeout(timeout) {
batteryHibernateTimeout = timeout
saveSettings()
}
function setLockBeforeSuspend(enabled) {
lockBeforeSuspend = enabled
saveSettings()
}
function setShowThirdPartyPlugins(enabled) {
showThirdPartyPlugins = enabled
saveSettings()
}
FileView {
id: settingsFile
path: isGreeterMode ? "" : StandardPaths.writableLocation(StandardPaths.GenericStateLocation) + "/DankMaterialShell/session.json"
blockLoading: isGreeterMode
blockWrites: true
watchChanges: !isGreeterMode
onLoaded: {
if (!isGreeterMode) {
parseSettings(settingsFile.text())
hasTriedDefaultSession = false
}
}
onLoadFailed: error => {
if (!isGreeterMode && !hasTriedDefaultSession) {
hasTriedDefaultSession = true
defaultSessionCheckProcess.running = true
}
}
}
FileView {
id: greeterSessionFile
path: {
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms"
return greetCfgDir + "/session.json"
}
preload: isGreeterMode
blockLoading: false
blockWrites: true
watchChanges: false
printErrors: true
onLoaded: {
if (isGreeterMode) {
parseSettings(greeterSessionFile.text())
}
}
}
Process {
id: defaultSessionCheckProcess
command: ["sh", "-c", "CONFIG_DIR=\"" + _stateDir
+ "/DankMaterialShell\"; if [ -f \"$CONFIG_DIR/default-session.json\" ] && [ ! -f \"$CONFIG_DIR/session.json\" ]; then cp --no-preserve=mode \"$CONFIG_DIR/default-session.json\" \"$CONFIG_DIR/session.json\" && echo 'copied'; else echo 'not_found'; fi"]
running: false
onExited: exitCode => {
if (exitCode === 0) {
console.log("Copied default-session.json to session.json")
settingsFile.reload()
}
}
}
IpcHandler {
target: "wallpaper"
function get(): string {
if (root.perMonitorWallpaper) {
return "ERROR: Per-monitor mode enabled. Use getFor(screenName) instead."
}
return root.wallpaperPath || ""
}
function set(path: string): string {
if (root.perMonitorWallpaper) {
return "ERROR: Per-monitor mode enabled. Use setFor(screenName, path) instead."
}
if (!path) {
return "ERROR: No path provided"
}
var absolutePath = path.startsWith("/") ? path : StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/" + path
try {
root.setWallpaper(absolutePath)
return "SUCCESS: Wallpaper set to " + absolutePath
} catch (e) {
return "ERROR: Failed to set wallpaper: " + e.toString()
}
}
function clear(): string {
root.setWallpaper("")
root.setPerMonitorWallpaper(false)
root.monitorWallpapers = {}
root.saveSettings()
return "SUCCESS: All wallpapers cleared"
}
function next(): string {
if (root.perMonitorWallpaper) {
return "ERROR: Per-monitor mode enabled. Use nextFor(screenName) instead."
}
if (!root.wallpaperPath) {
return "ERROR: No wallpaper set"
}
try {
WallpaperCyclingService.cycleNextManually()
return "SUCCESS: Cycling to next wallpaper"
} catch (e) {
return "ERROR: Failed to cycle wallpaper: " + e.toString()
}
}
function prev(): string {
if (root.perMonitorWallpaper) {
return "ERROR: Per-monitor mode enabled. Use prevFor(screenName) instead."
}
if (!root.wallpaperPath) {
return "ERROR: No wallpaper set"
}
try {
WallpaperCyclingService.cyclePrevManually()
return "SUCCESS: Cycling to previous wallpaper"
} catch (e) {
return "ERROR: Failed to cycle wallpaper: " + e.toString()
}
}
function getFor(screenName: string): string {
if (!screenName) {
return "ERROR: No screen name provided"
}
return root.getMonitorWallpaper(screenName) || ""
}
function setFor(screenName: string, path: string): string {
if (!screenName) {
return "ERROR: No screen name provided"
}
if (!path) {
return "ERROR: No path provided"
}
var absolutePath = path.startsWith("/") ? path : StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/" + path
try {
if (!root.perMonitorWallpaper) {
root.setPerMonitorWallpaper(true)
}
root.setMonitorWallpaper(screenName, absolutePath)
return "SUCCESS: Wallpaper set for " + screenName + " to " + absolutePath
} catch (e) {
return "ERROR: Failed to set wallpaper for " + screenName + ": " + e.toString()
}
}
function nextFor(screenName: string): string {
if (!screenName) {
return "ERROR: No screen name provided"
}
var currentWallpaper = root.getMonitorWallpaper(screenName)
if (!currentWallpaper) {
return "ERROR: No wallpaper set for " + screenName
}
try {
WallpaperCyclingService.cycleNextForMonitor(screenName)
return "SUCCESS: Cycling to next wallpaper for " + screenName
} catch (e) {
return "ERROR: Failed to cycle wallpaper for " + screenName + ": " + e.toString()
}
}
function prevFor(screenName: string): string {
if (!screenName) {
return "ERROR: No screen name provided"
}
var currentWallpaper = root.getMonitorWallpaper(screenName)
if (!currentWallpaper) {
return "ERROR: No wallpaper set for " + screenName
}
try {
WallpaperCyclingService.cyclePrevForMonitor(screenName)
return "SUCCESS: Cycling to previous wallpaper for " + screenName
} catch (e) {
return "ERROR: Failed to cycle wallpaper for " + screenName + ": " + e.toString()
}
}
}
}
File diff suppressed because it is too large Load Diff
+545
View File
@@ -0,0 +1,545 @@
// Stock theme definitions for DankMaterialShell
// Separated from Theme.qml to keep that file clean
const CatppuccinMocha = {
surface: "#313244",
surfaceText: "#cdd6f4",
surfaceVariant: "#313244",
surfaceVariantText: "#a6adc8",
background: "#1e1e2e",
backgroundText: "#cdd6f4",
outline: "#6c7086",
surfaceContainer: "#45475a",
surfaceContainerHigh: "#585b70",
surfaceContainerHighest: "#6c7086"
}
const CatppuccinLatte = {
surface: "#e6e9ef",
surfaceText: "#4c4f69",
surfaceVariant: "#e6e9ef",
surfaceVariantText: "#6c6f85",
background: "#eff1f5",
backgroundText: "#4c4f69",
outline: "#9ca0b0",
surfaceContainer: "#dce0e8",
surfaceContainerHigh: "#ccd0da",
surfaceContainerHighest: "#bcc0cc"
}
const CatppuccinVariants = {
"cat-rosewater": {
name: "Rosewater",
dark: { primary: "#f5e0dc", secondary: "#f2cdcd", primaryText: "#1e1e2e", primaryContainer: "#7d5d56", surfaceTint: "#f5e0dc" },
light: { primary: "#dc8a78", secondary: "#dd7878", primaryText: "#ffffff", primaryContainer: "#f6e7e3", surfaceTint: "#dc8a78" }
},
"cat-flamingo": {
name: "Flamingo",
dark: { primary: "#f2cdcd", secondary: "#f5e0dc", primaryText: "#1e1e2e", primaryContainer: "#7a555a", surfaceTint: "#f2cdcd" },
light: { primary: "#dd7878", secondary: "#dc8a78", primaryText: "#ffffff", primaryContainer: "#f6e5e5", surfaceTint: "#dd7878" }
},
"cat-pink": {
name: "Pink",
dark: { primary: "#f5c2e7", secondary: "#cba6f7", primaryText: "#1e1e2e", primaryContainer: "#7a3f69", surfaceTint: "#f5c2e7" },
light: { primary: "#ea76cb", secondary: "#8839ef", primaryText: "#ffffff", primaryContainer: "#f7d7ee", surfaceTint: "#ea76cb" }
},
"cat-mauve": {
name: "Mauve",
dark: { primary: "#cba6f7", secondary: "#b4befe", primaryText: "#1e1e2e", primaryContainer: "#55307f", surfaceTint: "#cba6f7" },
light: { primary: "#8839ef", secondary: "#7287fd", primaryText: "#ffffff", primaryContainer: "#eadcff", surfaceTint: "#8839ef" }
},
"cat-red": {
name: "Red",
dark: { primary: "#f38ba8", secondary: "#eba0ac", primaryText: "#1e1e2e", primaryContainer: "#6f2438", surfaceTint: "#f38ba8" },
light: { primary: "#d20f39", secondary: "#e64553", primaryText: "#ffffff", primaryContainer: "#f6d0d6", surfaceTint: "#d20f39" }
},
"cat-maroon": {
name: "Maroon",
dark: { primary: "#eba0ac", secondary: "#f38ba8", primaryText: "#1e1e2e", primaryContainer: "#6d3641", surfaceTint: "#eba0ac" },
light: { primary: "#e64553", secondary: "#d20f39", primaryText: "#ffffff", primaryContainer: "#f7d8dc", surfaceTint: "#e64553" }
},
"cat-peach": {
name: "Peach",
dark: { primary: "#fab387", secondary: "#f9e2af", primaryText: "#1e1e2e", primaryContainer: "#734226", surfaceTint: "#fab387" },
light: { primary: "#fe640b", secondary: "#df8e1d", primaryText: "#ffffff", primaryContainer: "#ffe4d5", surfaceTint: "#fe640b" }
},
"cat-yellow": {
name: "Yellow",
dark: { primary: "#f9e2af", secondary: "#a6e3a1", primaryText: "#1e1e2e", primaryContainer: "#6e5a2f", surfaceTint: "#f9e2af" },
light: { primary: "#df8e1d", secondary: "#40a02b", primaryText: "#ffffff", primaryContainer: "#fff6d6", surfaceTint: "#df8e1d" }
},
"cat-green": {
name: "Green",
dark: { primary: "#a6e3a1", secondary: "#94e2d5", primaryText: "#1e1e2e", primaryContainer: "#2f5f36", surfaceTint: "#a6e3a1" },
light: { primary: "#40a02b", secondary: "#179299", primaryText: "#ffffff", primaryContainer: "#dff4e0", surfaceTint: "#40a02b" }
},
"cat-teal": {
name: "Teal",
dark: { primary: "#94e2d5", secondary: "#89dceb", primaryText: "#1e1e2e", primaryContainer: "#2e5e59", surfaceTint: "#94e2d5" },
light: { primary: "#179299", secondary: "#04a5e5", primaryText: "#ffffff", primaryContainer: "#daf3f1", surfaceTint: "#179299" }
},
"cat-sky": {
name: "Sky",
dark: { primary: "#89dceb", secondary: "#74c7ec", primaryText: "#1e1e2e", primaryContainer: "#24586a", surfaceTint: "#89dceb" },
light: { primary: "#04a5e5", secondary: "#209fb5", primaryText: "#ffffff", primaryContainer: "#dbf1fb", surfaceTint: "#04a5e5" }
},
"cat-sapphire": {
name: "Sapphire",
dark: { primary: "#74c7ec", secondary: "#89b4fa", primaryText: "#1e1e2e", primaryContainer: "#1f4d6f", surfaceTint: "#74c7ec" },
light: { primary: "#209fb5", secondary: "#1e66f5", primaryText: "#ffffff", primaryContainer: "#def3f8", surfaceTint: "#209fb5" }
},
"cat-blue": {
name: "Blue",
dark: { primary: "#89b4fa", secondary: "#b4befe", primaryText: "#1e1e2e", primaryContainer: "#243f75", surfaceTint: "#89b4fa" },
light: { primary: "#1e66f5", secondary: "#7287fd", primaryText: "#ffffff", primaryContainer: "#e0e9ff", surfaceTint: "#1e66f5" }
},
"cat-lavender": {
name: "Lavender",
dark: { primary: "#b4befe", secondary: "#cba6f7", primaryText: "#1e1e2e", primaryContainer: "#3f4481", surfaceTint: "#b4befe" },
light: { primary: "#7287fd", secondary: "#8839ef", primaryText: "#ffffff", primaryContainer: "#e5e8ff", surfaceTint: "#7287fd" }
}
}
function getCatppuccinTheme(variant, isLight = false) {
const variantData = CatppuccinVariants[variant]
if (!variantData) return null
const baseColors = isLight ? CatppuccinLatte : CatppuccinMocha
const accentColors = isLight ? variantData.light : variantData.dark
return Object.assign({
name: `${variantData.name}${isLight ? ' Light' : ''}`
}, baseColors, accentColors)
}
const StockThemes = {
DARK: {
blue: {
name: "Blue",
primary: "#42a5f5",
primaryText: "#000000",
primaryContainer: "#0d47a1",
secondary: "#8ab4f8",
surface: "#101418",
surfaceText: "#e0e2e8",
surfaceVariant: "#42474e",
surfaceVariantText: "#c2c7cf",
surfaceTint: "#8ab4f8",
background: "#101418",
backgroundText: "#e0e2e8",
outline: "#8c9199",
surfaceContainer: "#1d2024",
surfaceContainerHigh: "#272a2f",
surfaceContainerHighest: "#32353a"
},
purple: {
name: "Purple",
primary: "#D0BCFF",
primaryText: "#381E72",
primaryContainer: "#4F378B",
secondary: "#CCC2DC",
surface: "#141218",
surfaceText: "#e6e0e9",
surfaceVariant: "#49454e",
surfaceVariantText: "#cac4cf",
surfaceTint: "#D0BCFF",
background: "#141218",
backgroundText: "#e6e0e9",
outline: "#948f99",
surfaceContainer: "#211f24",
surfaceContainerHigh: "#2b292f",
surfaceContainerHighest: "#36343a"
},
green: {
name: "Green",
primary: "#4caf50",
primaryText: "#000000",
primaryContainer: "#1b5e20",
secondary: "#81c995",
surface: "#10140f",
surfaceText: "#e0e4db",
surfaceVariant: "#424940",
surfaceVariantText: "#c2c9bd",
surfaceTint: "#81c995",
background: "#10140f",
backgroundText: "#e0e4db",
outline: "#8c9388",
surfaceContainer: "#1d211b",
surfaceContainerHigh: "#272b25",
surfaceContainerHighest: "#323630"
},
orange: {
name: "Orange",
primary: "#ff6d00",
primaryText: "#000000",
primaryContainer: "#3e2723",
secondary: "#ffb74d",
surface: "#1a120e",
surfaceText: "#f0dfd8",
surfaceVariant: "#52443d",
surfaceVariantText: "#d7c2b9",
surfaceTint: "#ffb74d",
background: "#1a120e",
backgroundText: "#f0dfd8",
outline: "#a08d85",
surfaceContainer: "#271e1a",
surfaceContainerHigh: "#322824",
surfaceContainerHighest: "#3d332e"
},
red: {
name: "Red",
primary: "#f44336",
primaryText: "#000000",
primaryContainer: "#4a0e0e",
secondary: "#f28b82",
surface: "#1a1110",
surfaceText: "#f1dedc",
surfaceVariant: "#534341",
surfaceVariantText: "#d8c2be",
surfaceTint: "#f28b82",
background: "#1a1110",
backgroundText: "#f1dedc",
outline: "#a08c89",
surfaceContainer: "#271d1c",
surfaceContainerHigh: "#322826",
surfaceContainerHighest: "#3d3231"
},
cyan: {
name: "Cyan",
primary: "#00bcd4",
primaryText: "#000000",
primaryContainer: "#004d5c",
secondary: "#4dd0e1",
surface: "#0e1416",
surfaceText: "#dee3e5",
surfaceVariant: "#3f484a",
surfaceVariantText: "#bfc8ca",
surfaceTint: "#4dd0e1",
background: "#0e1416",
backgroundText: "#dee3e5",
outline: "#899295",
surfaceContainer: "#1b2122",
surfaceContainerHigh: "#252b2c",
surfaceContainerHighest: "#303637"
},
pink: {
name: "Pink",
primary: "#e91e63",
primaryText: "#000000",
primaryContainer: "#4a0e2f",
secondary: "#f8bbd9",
surface: "#191112",
surfaceText: "#f0dee0",
surfaceVariant: "#524345",
surfaceVariantText: "#d6c2c3",
surfaceTint: "#f8bbd9",
background: "#191112",
backgroundText: "#f0dee0",
outline: "#9f8c8e",
surfaceContainer: "#261d1e",
surfaceContainerHigh: "#312829",
surfaceContainerHighest: "#3c3233"
},
amber: {
name: "Amber",
primary: "#ffc107",
primaryText: "#000000",
primaryContainer: "#4a3c00",
secondary: "#ffd54f",
surface: "#17130b",
surfaceText: "#ebe1d4",
surfaceVariant: "#4d4639",
surfaceVariantText: "#d0c5b4",
surfaceTint: "#ffd54f",
background: "#17130b",
backgroundText: "#ebe1d4",
outline: "#998f80",
surfaceContainer: "#231f17",
surfaceContainerHigh: "#2e2921",
surfaceContainerHighest: "#39342b"
},
coral: {
name: "Coral",
primary: "#ffb4ab",
primaryText: "#000000",
primaryContainer: "#8c1d18",
secondary: "#f9dedc",
surface: "#1a1110",
surfaceText: "#f1dedc",
surfaceVariant: "#534341",
surfaceVariantText: "#d8c2bf",
surfaceTint: "#ffb4ab",
background: "#1a1110",
backgroundText: "#f1dedc",
outline: "#a08c8a",
surfaceContainer: "#271d1c",
surfaceContainerHigh: "#322826",
surfaceContainerHighest: "#3d3231"
},
monochrome: {
name: "Monochrome",
primary: "#ffffff",
primaryText: "#2b303c",
primaryContainer: "#424753",
secondary: "#c4c6d0",
surface: "#2a2a2a",
surfaceText: "#e4e2e3",
surfaceVariant: "#474648",
surfaceVariantText: "#c8c6c7",
surfaceTint: "#c2c6d6",
background: "#131315",
backgroundText: "#e4e2e3",
outline: "#929092",
surfaceContainer: "#353535",
surfaceContainerHigh: "#424242",
surfaceContainerHighest: "#505050",
error: "#ffb4ab",
warning: "#3f4759",
info: "#595e6c",
matugen_type: "scheme-monochrome"
}
},
LIGHT: {
blue: {
name: "Blue Light",
primary: "#1976d2",
primaryText: "#ffffff",
primaryContainer: "#e3f2fd",
secondary: "#42a5f5",
surface: "#f7f9ff",
surfaceText: "#181c20",
surfaceVariant: "#dee3eb",
surfaceVariantText: "#42474e",
surfaceTint: "#1976d2",
background: "#f7f9ff",
backgroundText: "#181c20",
outline: "#72777f",
surfaceContainer: "#eceef4",
surfaceContainerHigh: "#e6e8ee",
surfaceContainerHighest: "#e0e2e8"
},
purple: {
name: "Purple Light",
primary: "#6750A4",
primaryText: "#ffffff",
primaryContainer: "#EADDFF",
secondary: "#625B71",
surface: "#fef7ff",
surfaceText: "#1d1b20",
surfaceVariant: "#e7e0eb",
surfaceVariantText: "#49454e",
surfaceTint: "#6750A4",
background: "#fef7ff",
backgroundText: "#1d1b20",
outline: "#7a757f",
surfaceContainer: "#f2ecf4",
surfaceContainerHigh: "#ece6ee",
surfaceContainerHighest: "#e6e0e9"
},
green: {
name: "Green Light",
primary: "#2e7d32",
primaryText: "#ffffff",
primaryContainer: "#e8f5e8",
secondary: "#4caf50",
surface: "#f7fbf1",
surfaceText: "#191d17",
surfaceVariant: "#dee5d8",
surfaceVariantText: "#424940",
surfaceTint: "#2e7d32",
background: "#f7fbf1",
backgroundText: "#191d17",
outline: "#72796f",
surfaceContainer: "#ecefe6",
surfaceContainerHigh: "#e6e9e0",
surfaceContainerHighest: "#e0e4db"
},
orange: {
name: "Orange Light",
primary: "#e65100",
primaryText: "#ffffff",
primaryContainer: "#ffecb3",
secondary: "#ff9800",
surface: "#fff8f6",
surfaceText: "#221a16",
surfaceVariant: "#f4ded5",
surfaceVariantText: "#52443d",
surfaceTint: "#e65100",
background: "#fff8f6",
backgroundText: "#221a16",
outline: "#85736c",
surfaceContainer: "#fceae3",
surfaceContainerHigh: "#f6e5de",
surfaceContainerHighest: "#f0dfd8"
},
red: {
name: "Red Light",
primary: "#d32f2f",
primaryText: "#ffffff",
primaryContainer: "#ffebee",
secondary: "#f44336",
surface: "#fff8f7",
surfaceText: "#231918",
surfaceVariant: "#f5ddda",
surfaceVariantText: "#534341",
surfaceTint: "#d32f2f",
background: "#fff8f7",
backgroundText: "#231918",
outline: "#857370",
surfaceContainer: "#fceae7",
surfaceContainerHigh: "#f7e4e1",
surfaceContainerHighest: "#f1dedc"
},
cyan: {
name: "Cyan Light",
primary: "#0097a7",
primaryText: "#ffffff",
primaryContainer: "#e0f2f1",
secondary: "#00bcd4",
surface: "#f5fafc",
surfaceText: "#171d1e",
surfaceVariant: "#dbe4e6",
surfaceVariantText: "#3f484a",
surfaceTint: "#0097a7",
background: "#f5fafc",
backgroundText: "#171d1e",
outline: "#6f797b",
surfaceContainer: "#e9eff0",
surfaceContainerHigh: "#e3e9eb",
surfaceContainerHighest: "#dee3e5"
},
pink: {
name: "Pink Light",
primary: "#c2185b",
primaryText: "#ffffff",
primaryContainer: "#fce4ec",
secondary: "#e91e63",
surface: "#fff8f7",
surfaceText: "#22191a",
surfaceVariant: "#f3dddf",
surfaceVariantText: "#524345",
surfaceTint: "#c2185b",
background: "#fff8f7",
backgroundText: "#22191a",
outline: "#847375",
surfaceContainer: "#fbeaeb",
surfaceContainerHigh: "#f5e4e5",
surfaceContainerHighest: "#f0dee0"
},
amber: {
name: "Amber Light",
primary: "#ff8f00",
primaryText: "#000000",
primaryContainer: "#fff8e1",
secondary: "#ffc107",
surface: "#fff8f2",
surfaceText: "#1f1b13",
surfaceVariant: "#ede1cf",
surfaceVariantText: "#4d4639",
surfaceTint: "#ff8f00",
background: "#fff8f2",
backgroundText: "#1f1b13",
outline: "#7f7667",
surfaceContainer: "#f6ecdf",
surfaceContainerHigh: "#f1e7d9",
surfaceContainerHighest: "#ebe1d4"
},
coral: {
name: "Coral Light",
primary: "#8c1d18",
primaryText: "#ffffff",
primaryContainer: "#ffdad6",
secondary: "#ff5449",
surface: "#fff8f7",
surfaceText: "#231918",
surfaceVariant: "#f5ddda",
surfaceVariantText: "#534341",
surfaceTint: "#8c1d18",
background: "#fff8f7",
backgroundText: "#231918",
outline: "#857371",
surfaceContainer: "#fceae7",
surfaceContainerHigh: "#f6e4e2",
surfaceContainerHighest: "#f1dedc"
},
monochrome: {
name: "Monochrome Light",
primary: "#2b303c",
primaryText: "#ffffff",
primaryContainer: "#d6d7dc",
secondary: "#4a4d56",
surface: "#f5f5f6",
surfaceText: "#2a2a2a",
surfaceVariant: "#e0e0e2",
surfaceVariantText: "#424242",
surfaceTint: "#5a5f6e",
background: "#ffffff",
backgroundText: "#1a1a1a",
outline: "#757577",
surfaceContainer: "#e8e8ea",
surfaceContainerHigh: "#dcdcde",
surfaceContainerHighest: "#d0d0d2",
error: "#ba1a1a",
warning: "#f9e79f",
info: "#5d6475",
matugen_type: "scheme-monochrome"
}
}
}
const ThemeCategories = {
GENERIC: {
name: "Generic",
variants: ["blue", "purple", "green", "orange", "red", "cyan", "pink", "amber", "coral", "monochrome"]
},
CATPPUCCIN: {
name: "Catppuccin",
variants: Object.keys(CatppuccinVariants)
}
}
const ThemeNames = {
BLUE: "blue",
PURPLE: "purple",
GREEN: "green",
ORANGE: "orange",
RED: "red",
CYAN: "cyan",
PINK: "pink",
AMBER: "amber",
CORAL: "coral",
MONOCHROME: "monochrome",
DYNAMIC: "dynamic"
}
function isStockTheme(themeName) {
return Object.keys(StockThemes.DARK).includes(themeName)
}
function isCatppuccinVariant(themeName) {
return Object.keys(CatppuccinVariants).includes(themeName)
}
function getAvailableThemes(isLight = false) {
return isLight ? StockThemes.LIGHT : StockThemes.DARK
}
function getThemeByName(themeName, isLight = false) {
if (isCatppuccinVariant(themeName)) {
return getCatppuccinTheme(themeName, isLight)
}
const themes = getAvailableThemes(isLight)
return themes[themeName] || themes.blue
}
function getAllThemeNames() {
return Object.keys(StockThemes.DARK)
}
function getCatppuccinVariantNames() {
return Object.keys(CatppuccinVariants)
}
function getThemeCategories() {
return ThemeCategories
}
+994
View File
@@ -0,0 +1,994 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtCore
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Services.UPower
import qs.Common
import qs.Services
import "StockThemes.js" as StockThemes
Singleton {
id: root
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
property string currentTheme: "blue"
property string currentThemeCategory: "generic"
property bool isLightMode: typeof SessionData !== "undefined" ? SessionData.isLightMode : false
readonly property string dynamic: "dynamic"
readonly property string custom : "custom"
readonly property string homeDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.HomeLocation))
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation))
readonly property string shellDir: Paths.strip(Qt.resolvedUrl(".").toString()).replace("/Common/", "")
readonly property string wallpaperPath: {
if (typeof SessionData === "undefined") return ""
if (SessionData.perMonitorWallpaper) {
var screens = Quickshell.screens
if (screens.length > 0) {
var firstMonitorWallpaper = SessionData.getMonitorWallpaper(screens[0].name)
var wallpaperPath = firstMonitorWallpaper || SessionData.wallpaperPath
if (wallpaperPath && wallpaperPath.startsWith("we:")) {
return stateDir + "/we_screenshots/" + wallpaperPath.substring(3) + ".jpg"
}
return wallpaperPath
}
}
var wallpaperPath = SessionData.wallpaperPath
var screens = Quickshell.screens
if (screens.length > 0 && wallpaperPath && wallpaperPath.startsWith("we:")) {
return stateDir + "/we_screenshots/" + wallpaperPath.substring(3) + ".jpg"
}
return wallpaperPath
}
readonly property string rawWallpaperPath: {
if (typeof SessionData === "undefined") return ""
if (SessionData.perMonitorWallpaper) {
// Use first monitor's wallpaper for dynamic theming
var screens = Quickshell.screens
if (screens.length > 0) {
var firstMonitorWallpaper = SessionData.getMonitorWallpaper(screens[0].name)
return firstMonitorWallpaper || SessionData.wallpaperPath
}
}
return SessionData.wallpaperPath
}
property bool matugenAvailable: false
property bool gtkThemingEnabled: typeof SettingsData !== "undefined" ? SettingsData.gtkAvailable : false
property bool qtThemingEnabled: typeof SettingsData !== "undefined" ? (SettingsData.qt5ctAvailable || SettingsData.qt6ctAvailable) : false
property var workerRunning: false
property var matugenColors: ({})
property int colorUpdateTrigger: 0
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
if (typeof SessionData !== "undefined") {
SessionData.isLightModeChanged.connect(root.onLightModeChanged)
isLightMode = SessionData.isLightMode
}
if (typeof SettingsData !== "undefined" && SettingsData.currentThemeName) {
switchTheme(SettingsData.currentThemeName, false, false)
}
}
function applyGreeterTheme(themeName) {
switchTheme(themeName, false, false)
if (themeName === dynamic && dynamicColorsFileView.path) {
dynamicColorsFileView.reload()
}
}
function getMatugenColor(path, fallback) {
colorUpdateTrigger
const colorMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark"
let cur = matugenColors && matugenColors.colors && matugenColors.colors[colorMode]
for (const part of path.split(".")) {
if (!cur || typeof cur !== "object" || !(part in cur))
return fallback
cur = cur[part]
}
return cur || fallback
}
readonly property var currentThemeData: {
if (currentTheme === "custom") {
return customThemeData || StockThemes.getThemeByName("blue", isLightMode)
} else if (currentTheme === dynamic) {
return {
"primary": getMatugenColor("primary", "#42a5f5"),
"primaryText": getMatugenColor("on_primary", "#ffffff"),
"primaryContainer": getMatugenColor("primary_container", "#1976d2"),
"secondary": getMatugenColor("secondary", "#8ab4f8"),
"surface": getMatugenColor("surface", "#1a1c1e"),
"surfaceText": getMatugenColor("on_background", "#e3e8ef"),
"surfaceVariant": getMatugenColor("surface_variant", "#44464f"),
"surfaceVariantText": getMatugenColor("on_surface_variant", "#c4c7c5"),
"surfaceTint": getMatugenColor("surface_tint", "#8ab4f8"),
"background": getMatugenColor("background", "#1a1c1e"),
"backgroundText": getMatugenColor("on_background", "#e3e8ef"),
"outline": getMatugenColor("outline", "#8e918f"),
"surfaceContainer": getMatugenColor("surface_container", "#1e2023"),
"surfaceContainerHigh": getMatugenColor("surface_container_high", "#292b2f"),
"surfaceContainerHighest": getMatugenColor("surface_container_highest", "#343740"),
"error": "#F2B8B5",
"warning": "#FF9800",
"info": "#2196F3",
"success": "#4CAF50"
}
} else {
return StockThemes.getThemeByName(currentTheme, isLightMode)
}
}
readonly property var availableMatugenSchemes: [
({ "value": "scheme-tonal-spot", "label": "Tonal Spot", "description": "Balanced palette with focused accents (default)." }),
({ "value": "scheme-content", "label": "Content", "description": "Derives colors that closely match the underlying image." }),
({ "value": "scheme-expressive", "label": "Expressive", "description": "Vibrant palette with playful saturation." }),
({ "value": "scheme-fidelity", "label": "Fidelity", "description": "High-fidelity palette that preserves source hues." }),
({ "value": "scheme-fruit-salad", "label": "Fruit Salad", "description": "Colorful mix of bright contrasting accents." }),
({ "value": "scheme-monochrome", "label": "Monochrome", "description": "Minimal palette built around a single hue." }),
({ "value": "scheme-neutral", "label": "Neutral", "description": "Muted palette with subdued, calming tones." }),
({ "value": "scheme-rainbow", "label": "Rainbow", "description": "Diverse palette spanning the full spectrum." })
]
function getMatugenScheme(value) {
const schemes = availableMatugenSchemes
for (let i = 0; i < schemes.length; i++) {
if (schemes[i].value === value)
return schemes[i]
}
return schemes[0]
}
property color primary: currentThemeData.primary
property color primaryText: currentThemeData.primaryText
property color primaryContainer: currentThemeData.primaryContainer
property color secondary: currentThemeData.secondary
property color surface: {
if (typeof SettingsData !== "undefined" && SettingsData.surfaceBase === "s") {
return currentThemeData.background
}
return currentThemeData.surface
}
property color surfaceText: currentThemeData.surfaceText
property color surfaceVariant: currentThemeData.surfaceVariant
property color surfaceVariantText: currentThemeData.surfaceVariantText
property color surfaceTint: currentThemeData.surfaceTint
property color background: currentThemeData.background
property color backgroundText: currentThemeData.backgroundText
property color outline: currentThemeData.outline
property color outlineVariant: currentThemeData.outlineVariant || Qt.rgba(outline.r, outline.g, outline.b, 0.6)
property color surfaceContainer: {
if (typeof SettingsData !== "undefined" && SettingsData.surfaceBase === "s") {
return currentThemeData.surface
}
return currentThemeData.surfaceContainer
}
property color surfaceContainerHigh: {
if (typeof SettingsData !== "undefined" && SettingsData.surfaceBase === "s") {
return currentThemeData.surfaceContainer
}
return currentThemeData.surfaceContainerHigh
}
property color surfaceContainerHighest: {
if (typeof SettingsData !== "undefined" && SettingsData.surfaceBase === "s") {
return currentThemeData.surfaceContainerHigh
}
return currentThemeData.surfaceContainerHighest
}
property color onSurface: surfaceText
property color onSurfaceVariant: surfaceVariantText
property color onPrimary: primaryText
property color onSurface_12: Qt.rgba(onSurface.r, onSurface.g, onSurface.b, 0.12)
property color onSurface_38: Qt.rgba(onSurface.r, onSurface.g, onSurface.b, 0.38)
property color onSurfaceVariant_30: Qt.rgba(onSurfaceVariant.r, onSurfaceVariant.g, onSurfaceVariant.b, 0.30)
property color error: currentThemeData.error || "#F2B8B5"
property color warning: currentThemeData.warning || "#FF9800"
property color info: currentThemeData.info || "#2196F3"
property color tempWarning: "#ff9933"
property color tempDanger: "#ff5555"
property color success: currentThemeData.success || "#4CAF50"
property color primaryHover: Qt.rgba(primary.r, primary.g, primary.b, 0.12)
property color primaryHoverLight: Qt.rgba(primary.r, primary.g, primary.b, 0.08)
property color primaryPressed: Qt.rgba(primary.r, primary.g, primary.b, 0.16)
property color primarySelected: Qt.rgba(primary.r, primary.g, primary.b, 0.3)
property color primaryBackground: Qt.rgba(primary.r, primary.g, primary.b, 0.04)
property color secondaryHover: Qt.rgba(secondary.r, secondary.g, secondary.b, 0.08)
property color surfaceHover: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.08)
property color surfacePressed: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.12)
property color surfaceSelected: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.15)
property color surfaceLight: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.1)
property color surfaceVariantAlpha: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.2)
property color surfaceTextHover: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.08)
property color surfaceTextAlpha: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.3)
property color surfaceTextLight: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.06)
property color surfaceTextMedium: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.7)
property color outlineButton: Qt.rgba(outline.r, outline.g, outline.b, 0.5)
property color outlineLight: Qt.rgba(outline.r, outline.g, outline.b, 0.05)
property color outlineMedium: Qt.rgba(outline.r, outline.g, outline.b, 0.08)
property color outlineStrong: Qt.rgba(outline.r, outline.g, outline.b, 0.12)
property color errorHover: Qt.rgba(error.r, error.g, error.b, 0.12)
property color errorPressed: Qt.rgba(error.r, error.g, error.b, 0.16)
property color shadowMedium: Qt.rgba(0, 0, 0, 0.08)
property color shadowStrong: Qt.rgba(0, 0, 0, 0.3)
readonly property var animationDurations: [
{ shorter: 0, short: 0, medium: 0, long: 0, extraLong: 0 },
{ shorter: 50, short: 75, medium: 150, long: 250, extraLong: 500 },
{ shorter: 100, short: 150, medium: 300, long: 500, extraLong: 1000 },
{ shorter: 150, short: 225, medium: 450, long: 750, extraLong: 1500 },
{ shorter: 200, short: 300, medium: 600, long: 1000, extraLong: 2000 }
]
readonly property int currentAnimationSpeed: typeof SettingsData !== "undefined" ? SettingsData.animationSpeed : SettingsData.AnimationSpeed.Short
readonly property var currentDurations: animationDurations[currentAnimationSpeed] || animationDurations[SettingsData.AnimationSpeed.Short]
property int shorterDuration: currentDurations.shorter
property int shortDuration: currentDurations.short
property int mediumDuration: currentDurations.medium
property int longDuration: currentDurations.long
property int extraLongDuration: currentDurations.extraLong
property int standardEasing: Easing.OutCubic
property int emphasizedEasing: Easing.OutQuart
property real cornerRadius: typeof SettingsData !== "undefined" ? SettingsData.cornerRadius : 12
property real spacingXS: 4
property real spacingS: 8
property real spacingM: 12
property real spacingL: 16
property real spacingXL: 24
property real fontSizeSmall: (typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0) * 12
property real fontSizeMedium: (typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0) * 14
property real fontSizeLarge: (typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0) * 16
property real fontSizeXLarge: (typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0) * 20
property real barHeight: 48
property real iconSize: 24
property real iconSizeSmall: 16
property real iconSizeLarge: 32
property real panelTransparency: 0.85
property real widgetTransparency: typeof SettingsData !== "undefined" && SettingsData.dankBarWidgetTransparency !== undefined ? SettingsData.dankBarWidgetTransparency : 1.0
property real popupTransparency: typeof SettingsData !== "undefined" && SettingsData.popupTransparency !== undefined ? SettingsData.popupTransparency : 1.0
function screenTransition() {
CompositorService.isNiri && NiriService.doScreenTransition()
}
function switchTheme(themeName, savePrefs = true, enableTransition = true) {
if (enableTransition) {
screenTransition()
themeTransitionTimer.themeName = themeName
themeTransitionTimer.savePrefs = savePrefs
themeTransitionTimer.restart()
return
}
if (themeName === dynamic) {
currentTheme = dynamic
currentThemeCategory = dynamic
} else if (themeName === custom) {
currentTheme = custom
currentThemeCategory = custom
if (typeof SettingsData !== "undefined" && SettingsData.customThemeFile) {
loadCustomThemeFromFile(SettingsData.customThemeFile)
}
} else {
currentTheme = themeName
if (StockThemes.isCatppuccinVariant(themeName)) {
currentThemeCategory = "catppuccin"
} else {
currentThemeCategory = "generic"
}
}
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode)
if (savePrefs && typeof SettingsData !== "undefined" && !isGreeterMode)
SettingsData.setTheme(currentTheme)
if (!isGreeterMode) {
generateSystemThemesFromCurrentTheme()
}
}
function setLightMode(light, savePrefs = true, enableTransition = false) {
if (enableTransition) {
screenTransition()
lightModeTransitionTimer.lightMode = light
lightModeTransitionTimer.savePrefs = savePrefs
lightModeTransitionTimer.restart()
return
}
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode)
isLightMode = light
if (savePrefs && typeof SessionData !== "undefined" && !isGreeterMode)
SessionData.setLightMode(isLightMode)
if (!isGreeterMode) {
PortalService.setLightMode(isLightMode)
generateSystemThemesFromCurrentTheme()
}
}
function toggleLightMode(savePrefs = true) {
setLightMode(!isLightMode, savePrefs, true)
}
function forceGenerateSystemThemes() {
if (!matugenAvailable) {
return
}
generateSystemThemesFromCurrentTheme()
}
function getAvailableThemes() {
return StockThemes.getAllThemeNames()
}
function getThemeDisplayName(themeName) {
const themeData = StockThemes.getThemeByName(themeName, isLightMode)
return themeData.name
}
function getThemeColors(themeName) {
if (themeName === "custom" && customThemeData) {
return customThemeData
}
return StockThemes.getThemeByName(themeName, isLightMode)
}
function switchThemeCategory(category, defaultTheme) {
screenTransition()
themeCategoryTransitionTimer.category = category
themeCategoryTransitionTimer.defaultTheme = defaultTheme
themeCategoryTransitionTimer.restart()
}
function getCatppuccinColor(variantName) {
const catColors = {
"cat-rosewater": "#f5e0dc", "cat-flamingo": "#f2cdcd", "cat-pink": "#f5c2e7", "cat-mauve": "#cba6f7",
"cat-red": "#f38ba8", "cat-maroon": "#eba0ac", "cat-peach": "#fab387", "cat-yellow": "#f9e2af",
"cat-green": "#a6e3a1", "cat-teal": "#94e2d5", "cat-sky": "#89dceb", "cat-sapphire": "#74c7ec",
"cat-blue": "#89b4fa", "cat-lavender": "#b4befe"
}
return catColors[variantName] || "#cba6f7"
}
function getCatppuccinVariantName(variantName) {
const catNames = {
"cat-rosewater": "Rosewater", "cat-flamingo": "Flamingo", "cat-pink": "Pink", "cat-mauve": "Mauve",
"cat-red": "Red", "cat-maroon": "Maroon", "cat-peach": "Peach", "cat-yellow": "Yellow",
"cat-green": "Green", "cat-teal": "Teal", "cat-sky": "Sky", "cat-sapphire": "Sapphire",
"cat-blue": "Blue", "cat-lavender": "Lavender"
}
return catNames[variantName] || "Unknown"
}
function loadCustomTheme(themeData) {
if (themeData.dark || themeData.light) {
const colorMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark"
const selectedTheme = themeData[colorMode] || themeData.dark || themeData.light
customThemeData = selectedTheme
} else {
customThemeData = themeData
}
generateSystemThemesFromCurrentTheme()
}
function loadCustomThemeFromFile(filePath) {
customThemeFileView.path = filePath
}
property alias availableThemeNames: root._availableThemeNames
readonly property var _availableThemeNames: StockThemes.getAllThemeNames()
property string currentThemeName: currentTheme
function popupBackground() {
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, popupTransparency)
}
function contentBackground() {
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, popupTransparency)
}
function panelBackground() {
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, panelTransparency)
}
property real notepadTransparency: SettingsData.notepadTransparencyOverride >= 0 ? SettingsData.notepadTransparencyOverride : popupTransparency
property var widgetBaseBackgroundColor: {
const colorMode = typeof SettingsData !== "undefined" ? SettingsData.widgetBackgroundColor : "sch"
switch (colorMode) {
case "s":
return surface
case "sc":
return surfaceContainer
case "sch":
return surfaceContainerHigh
case "sth":
default:
return surfaceTextHover
}
}
property var widgetBaseHoverColor: {
const baseColor = widgetBaseBackgroundColor
const factor = 1.2
return isLightMode ? Qt.darker(baseColor, factor) : Qt.lighter(baseColor, factor)
}
property var widgetBackground: {
const colorMode = typeof SettingsData !== "undefined" ? SettingsData.widgetBackgroundColor : "sch"
switch (colorMode) {
case "s":
return Qt.rgba(surface.r, surface.g, surface.b, widgetTransparency)
case "sc":
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, widgetTransparency)
case "sch":
return Qt.rgba(surfaceContainerHigh.r, surfaceContainerHigh.g, surfaceContainerHigh.b, widgetTransparency)
case "sth":
default:
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, widgetTransparency)
}
}
function getPopupBackgroundAlpha() {
return popupTransparency
}
function getContentBackgroundAlpha() {
return popupTransparency
}
function isColorDark(c) {
return (0.299 * c.r + 0.587 * c.g + 0.114 * c.b) < 0.5
}
function barIconSize(barThickness, offset) {
const defaultOffset = offset !== undefined ? offset : -6
return Math.round((barThickness / 48) * (iconSize + defaultOffset))
}
function barTextSize(barThickness) {
const scale = barThickness / 48
const dankBarScale = (typeof SettingsData !== "undefined" ? SettingsData.dankBarFontScale : 1.0)
if (scale <= 0.75) return fontSizeSmall * 0.9 * dankBarScale
if (scale >= 1.25) return fontSizeMedium * dankBarScale
return fontSizeSmall * dankBarScale
}
function getBatteryIcon(level, isCharging, batteryAvailable) {
if (!batteryAvailable)
return _getBatteryPowerProfileIcon()
if (isCharging) {
if (level >= 90)
return "battery_charging_full"
if (level >= 80)
return "battery_charging_90"
if (level >= 60)
return "battery_charging_80"
if (level >= 50)
return "battery_charging_60"
if (level >= 30)
return "battery_charging_50"
if (level >= 20)
return "battery_charging_30"
return "battery_charging_20"
} else {
if (level >= 95)
return "battery_full"
if (level >= 85)
return "battery_6_bar"
if (level >= 70)
return "battery_5_bar"
if (level >= 55)
return "battery_4_bar"
if (level >= 40)
return "battery_3_bar"
if (level >= 25)
return "battery_2_bar"
if (level >= 10)
return "battery_1_bar"
return "battery_alert"
}
}
function _getBatteryPowerProfileIcon() {
if (typeof PowerProfiles === "undefined")
return "balance"
switch (PowerProfiles.profile) {
case PowerProfile.PowerSaver:
return "energy_savings_leaf"
case PowerProfile.Performance:
return "rocket_launch"
default:
return "balance"
}
}
function getPowerProfileIcon(profile) {
switch (profile) {
case PowerProfile.PowerSaver:
return "battery_saver"
case PowerProfile.Balanced:
return "battery_std"
case PowerProfile.Performance:
return "flash_on"
default:
return "settings"
}
}
function getPowerProfileLabel(profile) {
switch (profile) {
case PowerProfile.PowerSaver:
return "Power Saver"
case PowerProfile.Balanced:
return "Balanced"
case PowerProfile.Performance:
return "Performance"
default:
return profile.charAt(0).toUpperCase() + profile.slice(1)
}
}
function getPowerProfileDescription(profile) {
switch (profile) {
case PowerProfile.PowerSaver:
return "Extend battery life"
case PowerProfile.Balanced:
return "Balance power and performance"
case PowerProfile.Performance:
return "Prioritize performance"
default:
return "Custom power profile"
}
}
function onLightModeChanged() {
if (matugenColors && Object.keys(matugenColors).length > 0) {
colorUpdateTrigger++
}
if (currentTheme === "custom" && customThemeFileView.path) {
customThemeFileView.reload()
}
}
function setDesiredTheme(kind, value, isLight, iconTheme, matugenType) {
if (!matugenAvailable) {
console.warn("matugen not available or disabled - cannot set system theme")
return
}
if (typeof NiriService !== "undefined" && CompositorService.isNiri) {
NiriService.suppressNextToast()
}
const desired = {
"kind": kind,
"value": value,
"mode": isLight ? "light" : "dark",
"iconTheme": iconTheme || "System Default",
"matugenType": matugenType || "scheme-tonal-spot",
"surfaceBase": (typeof SettingsData !== "undefined" && SettingsData.surfaceBase) ? SettingsData.surfaceBase : "sc"
}
const json = JSON.stringify(desired)
const desiredPath = stateDir + "/matugen.desired.json"
Quickshell.execDetached(["sh", "-c", `mkdir -p '${stateDir}' && cat > '${desiredPath}' << 'EOF'\n${json}\nEOF`])
workerRunning = true
if (rawWallpaperPath.startsWith("we:")) {
console.log("calling matugen worker")
systemThemeGenerator.command = [
"sh", "-c",
`sleep 1 && ${shellDir}/scripts/matugen-worker.sh '${stateDir}' '${shellDir}' '${configDir}' --run`
]
} else {
systemThemeGenerator.command = [shellDir + "/scripts/matugen-worker.sh", stateDir, shellDir, configDir, "--run"]
}
systemThemeGenerator.running = true
}
function generateSystemThemesFromCurrentTheme() {
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) {
return
}
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) {
console.warn("Custom theme data not available for system theme generation")
return
}
primaryColor = customThemeData.primary
matugenType = customThemeData.matugen_type
} else {
primaryColor = currentThemeData.primary
matugenType = currentThemeData.matugen_type
}
if (!primaryColor) {
console.warn("No primary color available for theme:", currentTheme)
return
}
setDesiredTheme("hex", primaryColor, isLight, iconTheme, matugenType)
}
}
function applyGtkColors() {
if (!matugenAvailable) {
if (typeof ToastService !== "undefined") {
ToastService.showError("matugen not available or disabled - cannot apply GTK colors")
}
return
}
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "true" : "false"
gtkApplier.command = [shellDir + "/scripts/gtk.sh", configDir, isLight, shellDir]
gtkApplier.running = true
}
function applyQtColors() {
if (!matugenAvailable) {
if (typeof ToastService !== "undefined") {
ToastService.showError("matugen not available or disabled - cannot apply Qt colors")
}
return
}
qtApplier.command = [shellDir + "/scripts/qt.sh", configDir]
qtApplier.running = true
}
function withAlpha(c, a) { return Qt.rgba(c.r, c.g, c.b, a); }
function snap(value, dpr) {
return Math.round(value * dpr) / dpr
}
function invertHex(hex) {
hex = hex.replace('#', '');
if (!/^[0-9A-Fa-f]{6}$/.test(hex)) {
return hex;
}
const r = parseInt(hex.substr(0, 2), 16);
const g = parseInt(hex.substr(2, 2), 16);
const b = parseInt(hex.substr(4, 2), 16);
const invR = (255 - r).toString(16).padStart(2, '0');
const invG = (255 - g).toString(16).padStart(2, '0');
const invB = (255 - b).toString(16).padStart(2, '0');
return `#${invR}${invG}${invB}`;
}
property string baseLogoColor: {
if (typeof SettingsData === "undefined") return ""
const colorOverride = SettingsData.launcherLogoColorOverride
if (!colorOverride || colorOverride === "") return ""
if (colorOverride === "primary") return primary
if (colorOverride === "surface") return surfaceText
return colorOverride
}
property string effectiveLogoColor: {
if (typeof SettingsData === "undefined") return ""
const colorOverride = SettingsData.launcherLogoColorOverride
if (!colorOverride || colorOverride === "") return ""
if (colorOverride === "primary") return primary
if (colorOverride === "surface") return surfaceText
if (!SettingsData.launcherLogoColorInvertOnMode) {
return colorOverride
}
if (isLightMode) {
return invertHex(colorOverride)
}
return colorOverride
}
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
onExited: exitCode => {
workerRunning = false
if (exitCode !== 0 && exitCode !== 2) {
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)
}
}
}
}
FileView {
id: customThemeFileView
watchChanges: currentTheme === "custom"
function parseAndLoadTheme() {
try {
var themeData = JSON.parse(customThemeFileView.text())
loadCustomTheme(themeData)
} catch (e) {
ToastService.showError("Invalid JSON format: " + e.message)
}
}
onLoaded: {
parseAndLoadTheme()
}
onFileChanged: {
customThemeFileView.reload()
}
onLoadFailed: function (error) {
if (typeof ToastService !== "undefined") {
ToastService.showError("Failed to read theme file: " + error)
}
}
}
FileView {
id: dynamicColorsFileView
path: {
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms"
const colorsPath = SessionData.isGreeterMode
? greetCfgDir + "/colors.json"
: stateDir + "/dms-colors.json"
return colorsPath
}
watchChanges: currentTheme === dynamic && !SessionData.isGreeterMode
function parseAndLoadColors() {
try {
const colorsText = dynamicColorsFileView.text()
if (colorsText) {
root.matugenColors = JSON.parse(colorsText)
root.colorUpdateTrigger++
if (typeof ToastService !== "undefined") {
ToastService.clearWallpaperError()
}
}
} catch (e) {
console.error("Theme: Failed to parse dynamic colors:", e)
if (typeof ToastService !== "undefined") {
ToastService.wallpaperErrorStatus = "error"
ToastService.showError("Dynamic colors parse error: " + e.message)
}
}
}
onLoaded: {
if (currentTheme === dynamic) {
parseAndLoadColors()
}
}
onFileChanged: {
if (currentTheme === dynamic) {
dynamicColorsFileView.reload()
}
}
onLoadFailed: function (error) {
if (currentTheme === dynamic && typeof ToastService !== "undefined") {
ToastService.showError("Failed to read dynamic colors: " + error)
}
}
}
IpcHandler {
target: "theme"
function toggle(): string {
root.toggleLightMode()
return root.isLightMode ? "dark" : "light"
}
function light(): string {
root.setLightMode(true, true, true)
return "light"
}
function dark(): string {
root.setLightMode(false, true, true)
return "dark"
}
function getMode(): string {
return root.isLightMode ? "light" : "dark"
}
}
// These timers are for screen transitions, since sometimes QML still beats the niri call
Timer {
id: themeTransitionTimer
interval: 50
repeat: false
property string themeName: ""
property bool savePrefs: true
onTriggered: root.switchTheme(themeName, savePrefs, false)
}
Timer {
id: lightModeTransitionTimer
interval: 100
repeat: false
property bool lightMode: false
property bool savePrefs: true
onTriggered: root.setLightMode(lightMode, savePrefs, false)
}
Timer {
id: themeCategoryTransitionTimer
interval: 50
repeat: false
property string category: ""
property string defaultTheme: ""
onTriggered: {
root.currentThemeCategory = category
root.switchTheme(defaultTheme, true, false)
}
}
}
+1307
View File
File diff suppressed because it is too large Load Diff
+106
View File
@@ -0,0 +1,106 @@
.pragma library
// This exists only beacause I haven't been able to get linkColor to work with MarkdownText
// May not be necessary if that's possible tbh.
function markdownToHtml(text) {
if (!text) return "";
// Store code blocks and inline code to protect them from further processing
const codeBlocks = [];
const inlineCode = [];
let blockIndex = 0;
let inlineIndex = 0;
// First, extract and replace code blocks with placeholders
let html = text.replace(/```([\s\S]*?)```/g, (match, code) => {
// Trim leading and trailing blank lines only
const trimmedCode = code.replace(/^\n+|\n+$/g, '');
// Escape HTML entities in code
const escapedCode = trimmedCode.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
codeBlocks.push(`<pre><code>${escapedCode}</code></pre>`);
return `\x00CODEBLOCK${blockIndex++}\x00`;
});
// Extract and replace inline code
html = html.replace(/`([^`]+)`/g, (match, code) => {
// Escape HTML entities in code
const escapedCode = code.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
inlineCode.push(`<code>${escapedCode}</code>`);
return `\x00INLINECODE${inlineIndex++}\x00`;
});
// Now process everything else
// Escape HTML entities (but not in code blocks)
html = html.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
// Headers
html = html.replace(/^### (.*?)$/gm, '<h3>$1</h3>');
html = html.replace(/^## (.*?)$/gm, '<h2>$1</h2>');
html = html.replace(/^# (.*?)$/gm, '<h1>$1</h1>');
// Bold and italic (order matters!)
html = html.replace(/\*\*\*(.*?)\*\*\*/g, '<b><i>$1</i></b>');
html = html.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>');
html = html.replace(/\*(.*?)\*/g, '<i>$1</i>');
html = html.replace(/___(.*?)___/g, '<b><i>$1</i></b>');
html = html.replace(/__(.*?)__/g, '<b>$1</b>');
html = html.replace(/_(.*?)_/g, '<i>$1</i>');
// Links
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
// Lists
html = html.replace(/^\* (.*?)$/gm, '<li>$1</li>');
html = html.replace(/^- (.*?)$/gm, '<li>$1</li>');
html = html.replace(/^\d+\. (.*?)$/gm, '<li>$1</li>');
// Wrap consecutive list items in ul/ol tags
html = html.replace(/(<li>[\s\S]*?<\/li>\s*)+/g, function(match) {
return '<ul>' + match + '</ul>';
});
// Detect plain URLs and wrap them in anchor tags (but not inside existing <a> or markdown links)
html = html.replace(/(^|[^"'>])((https?|file):\/\/[^\s<]+)/g, '$1<a href="$2">$2</a>');
// Restore code blocks and inline code BEFORE line break processing
html = html.replace(/\x00CODEBLOCK(\d+)\x00/g, (match, index) => {
return codeBlocks[parseInt(index)];
});
html = html.replace(/\x00INLINECODE(\d+)\x00/g, (match, index) => {
return inlineCode[parseInt(index)];
});
// Line breaks (after code blocks are restored)
html = html.replace(/\n\n/g, '</p><p>');
html = html.replace(/\n/g, '<br/>');
// Wrap in paragraph tags if not already wrapped
if (!html.startsWith('<')) {
html = '<p>' + html + '</p>';
}
// Clean up the final HTML
// Remove <br/> tags immediately before block elements
html = html.replace(/<br\/>\s*<pre>/g, '<pre>');
html = html.replace(/<br\/>\s*<ul>/g, '<ul>');
html = html.replace(/<br\/>\s*<h[1-6]>/g, '<h$1>');
// Remove empty paragraphs
html = html.replace(/<p>\s*<\/p>/g, '');
html = html.replace(/<p>\s*<br\/>\s*<\/p>/g, '');
// Remove excessive line breaks
html = html.replace(/(<br\/>){3,}/g, '<br/><br/>'); // Max 2 consecutive line breaks
html = html.replace(/(<\/p>)\s*(<p>)/g, '$1$2'); // Remove whitespace between paragraphs
// Remove leading/trailing whitespace
html = html.trim();
return html;
}
+29
View File
@@ -0,0 +1,29 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import Quickshell.Services.Greetd
import qs.Common
import qs.Modules.Greetd
Item {
id: root
WlSessionLock {
id: sessionLock
locked: false
Component.onCompleted: {
Qt.callLater(() => { locked = true })
}
onLockedChanged: {
if (!locked) {
console.log("Greetd session unlocked, exiting")
}
}
GreeterSurface {
lock: sessionLock
}
}
}
+524
View File
@@ -0,0 +1,524 @@
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Modals
import qs.Modals.Clipboard
import qs.Modals.Common
import qs.Modals.Settings
import qs.Modals.Spotlight
import qs.Modules
import qs.Modules.AppDrawer
import qs.Modules.DankDash
import qs.Modules.ControlCenter
import qs.Modules.Dock
import qs.Modules.Lock
import qs.Modules.Notepad
import qs.Modules.Notifications.Center
import qs.Widgets
import qs.Modules.Notifications.Popup
import qs.Modules.OSD
import qs.Modules.ProcessList
import qs.Modules.Settings
import qs.Modules.DankBar
import qs.Modules.DankBar.Popouts
import qs.Modules.Plugins
import qs.Services
Item {
id: root
Instantiator {
id: daemonPluginInstantiator
asynchronous: true
model: Object.keys(PluginService.pluginDaemonComponents)
delegate: Loader {
id: daemonLoader
property string pluginId: modelData
sourceComponent: PluginService.pluginDaemonComponents[pluginId]
onLoaded: {
if (item) {
item.pluginService = PluginService
if (item.popoutService !== undefined) {
item.popoutService = PopoutService
}
item.pluginId = pluginId
console.log("Daemon plugin loaded:", pluginId)
}
}
}
}
WallpaperBackground {}
LazyLoader {
id: lockLoader
active: false
Lock {
id: lock
anchors.fill: parent
Component.onCompleted: {
IdleService.lockComponent = lock
}
}
}
Timer {
id: lockInitTimer
interval: 100
running: true
repeat: false
onTriggered: lockLoader.active = true
}
Loader {
id: dankBarLoader
asynchronous: false
property var currentPosition: SettingsData.dankBarPosition
property bool initialized: false
sourceComponent: DankBar {
onColorPickerRequested: colorPickerModal.show()
}
Component.onCompleted: {
initialized = true
}
onCurrentPositionChanged: {
if (!initialized) return
const component = sourceComponent
sourceComponent = null
sourceComponent = component
}
}
Loader {
id: dockLoader
active: true
asynchronous: false
property var currentPosition: SettingsData.dockPosition
property bool initialized: false
sourceComponent: Dock {
contextMenu: dockContextMenuLoader.item ? dockContextMenuLoader.item : null
}
onLoaded: {
if (item) {
dockContextMenuLoader.active = true
}
}
Component.onCompleted: {
initialized = true
}
onCurrentPositionChanged: {
if (!initialized) return
console.log("DEBUG: Dock position changed to:", currentPosition, "- recreating dock")
const comp = sourceComponent
sourceComponent = null
sourceComponent = comp
}
}
Loader {
id: dankDashPopoutLoader
active: false
asynchronous: true
sourceComponent: Component {
DankDashPopout {
id: dankDashPopout
Component.onCompleted: {
PopoutService.dankDashPopout = dankDashPopout
}
}
}
}
LazyLoader {
id: dockContextMenuLoader
active: false
DockContextMenu {
id: dockContextMenu
}
}
LazyLoader {
id: notificationCenterLoader
active: false
NotificationCenterPopout {
id: notificationCenter
Component.onCompleted: {
PopoutService.notificationCenterPopout = notificationCenter
}
}
}
Variants {
model: SettingsData.getFilteredScreens("notifications")
delegate: NotificationPopupManager {
modelData: item
}
}
LazyLoader {
id: controlCenterLoader
active: false
property var modalRef: colorPickerModal
property LazyLoader powerModalLoaderRef: powerMenuModalLoader
ControlCenterPopout {
id: controlCenterPopout
colorPickerModal: controlCenterLoader.modalRef
powerMenuModalLoader: controlCenterLoader.powerModalLoaderRef
onLockRequested: {
lockLoader.item.activate()
}
Component.onCompleted: {
PopoutService.controlCenterPopout = controlCenterPopout
}
}
}
LazyLoader {
id: wifiPasswordModalLoader
active: false
WifiPasswordModal {
id: wifiPasswordModal
Component.onCompleted: {
PopoutService.wifiPasswordModal = wifiPasswordModal
}
}
}
LazyLoader {
id: networkInfoModalLoader
active: false
NetworkInfoModal {
id: networkInfoModal
Component.onCompleted: {
PopoutService.networkInfoModal = networkInfoModal
}
}
}
LazyLoader {
id: batteryPopoutLoader
active: false
BatteryPopout {
id: batteryPopout
Component.onCompleted: {
PopoutService.batteryPopout = batteryPopout
}
}
}
LazyLoader {
id: vpnPopoutLoader
active: false
VpnPopout {
id: vpnPopout
Component.onCompleted: {
PopoutService.vpnPopout = vpnPopout
}
}
}
LazyLoader {
id: powerMenuLoader
active: false
PowerMenu {
id: powerMenu
onPowerActionRequested: (action, title, message) => {
powerConfirmModalLoader.active = true
if (powerConfirmModalLoader.item) {
powerConfirmModalLoader.item.confirmButtonColor = action === "poweroff" ? Theme.error : action === "reboot" ? Theme.warning : Theme.primary
powerConfirmModalLoader.item.show(title, message, function () {
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
}
}, function () {})
}
}
}
}
LazyLoader {
id: powerConfirmModalLoader
active: false
ConfirmModal {
id: powerConfirmModal
}
}
LazyLoader {
id: processListPopoutLoader
active: false
ProcessListPopout {
id: processListPopout
Component.onCompleted: {
PopoutService.processListPopout = processListPopout
}
}
}
SettingsModal {
id: settingsModal
Component.onCompleted: {
PopoutService.settingsModal = settingsModal
}
}
LazyLoader {
id: appDrawerLoader
active: false
AppDrawerPopout {
id: appDrawerPopout
Component.onCompleted: {
PopoutService.appDrawerPopout = appDrawerPopout
}
}
}
SpotlightModal {
id: spotlightModal
Component.onCompleted: {
PopoutService.spotlightModal = spotlightModal
}
}
ClipboardHistoryModal {
id: clipboardHistoryModalPopup
Component.onCompleted: {
PopoutService.clipboardHistoryModal = clipboardHistoryModalPopup
}
}
NotificationModal {
id: notificationModal
Component.onCompleted: {
PopoutService.notificationModal = notificationModal
}
}
DankColorPickerModal {
id: colorPickerModal
Component.onCompleted: {
PopoutService.colorPickerModal = colorPickerModal
}
}
LazyLoader {
id: processListModalLoader
active: false
ProcessListModal {
id: processListModal
Component.onCompleted: {
PopoutService.processListModal = processListModal
}
}
}
LazyLoader {
id: systemUpdateLoader
active: false
SystemUpdatePopout {
id: systemUpdatePopout
Component.onCompleted: {
PopoutService.systemUpdatePopout = systemUpdatePopout
}
}
}
Variants {
id: notepadSlideoutVariants
model: SettingsData.getFilteredScreens("notepad")
delegate: DankSlideout {
id: notepadSlideout
modelData: item
title: I18n.tr("Notepad")
slideoutWidth: 480
expandable: true
expandedWidthValue: 960
customTransparency: SettingsData.notepadTransparencyOverride
content: Component {
Notepad {
onHideRequested: {
notepadSlideout.hide()
}
}
}
function toggle() {
if (isVisible) {
hide()
} else {
show()
}
}
}
}
LazyLoader {
id: powerMenuModalLoader
active: false
PowerMenuModal {
id: powerMenuModal
onPowerActionRequested: (action, title, message) => {
powerConfirmModalLoader.active = true
if (powerConfirmModalLoader.item) {
powerConfirmModalLoader.item.confirmButtonColor = action === "poweroff" ? Theme.error : action === "reboot" ? Theme.warning : Theme.primary
powerConfirmModalLoader.item.show(title, message, function () {
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
}
}, function () {})
}
}
Component.onCompleted: {
PopoutService.powerMenuModal = powerMenuModal
}
}
}
DMSShellIPC {
powerMenuModalLoader: powerMenuModalLoader
processListModalLoader: processListModalLoader
controlCenterLoader: controlCenterLoader
dankDashPopoutLoader: dankDashPopoutLoader
notepadSlideoutVariants: notepadSlideoutVariants
}
Variants {
model: SettingsData.getFilteredScreens("toast")
delegate: Toast {
modelData: item
visible: ToastService.toastVisible
}
}
Variants {
model: SettingsData.getFilteredScreens("osd")
delegate: VolumeOSD {
modelData: item
}
}
Variants {
model: SettingsData.getFilteredScreens("osd")
delegate: MicMuteOSD {
modelData: item
}
}
Variants {
model: SettingsData.getFilteredScreens("osd")
delegate: BrightnessOSD {
modelData: item
}
}
Variants {
model: SettingsData.getFilteredScreens("osd")
delegate: IdleInhibitorOSD {
modelData: item
}
}
}
+265
View File
@@ -0,0 +1,265 @@
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Hyprland
import qs.Common
import qs.Services
Item {
id: root
required property var powerMenuModalLoader
required property var processListModalLoader
required property var controlCenterLoader
required property var dankDashPopoutLoader
required property var notepadSlideoutVariants
IpcHandler {
function open() {
root.powerMenuModalLoader.active = true
if (root.powerMenuModalLoader.item)
root.powerMenuModalLoader.item.openCentered()
return "POWERMENU_OPEN_SUCCESS"
}
function close() {
if (root.powerMenuModalLoader.item)
root.powerMenuModalLoader.item.close()
return "POWERMENU_CLOSE_SUCCESS"
}
function toggle() {
root.powerMenuModalLoader.active = true
if (root.powerMenuModalLoader.item) {
if (root.powerMenuModalLoader.item.shouldBeVisible) {
root.powerMenuModalLoader.item.close()
} else {
root.powerMenuModalLoader.item.openCentered()
}
}
return "POWERMENU_TOGGLE_SUCCESS"
}
target: "powermenu"
}
IpcHandler {
function open(): string {
root.processListModalLoader.active = true
if (root.processListModalLoader.item)
root.processListModalLoader.item.show()
return "PROCESSLIST_OPEN_SUCCESS"
}
function close(): string {
if (root.processListModalLoader.item)
root.processListModalLoader.item.hide()
return "PROCESSLIST_CLOSE_SUCCESS"
}
function toggle(): string {
root.processListModalLoader.active = true
if (root.processListModalLoader.item)
root.processListModalLoader.item.toggle()
return "PROCESSLIST_TOGGLE_SUCCESS"
}
target: "processlist"
}
IpcHandler {
function open(): string {
root.controlCenterLoader.active = true
if (root.controlCenterLoader.item) {
root.controlCenterLoader.item.open()
return "CONTROL_CENTER_OPEN_SUCCESS"
}
return "CONTROL_CENTER_OPEN_FAILED"
}
function close(): string {
if (root.controlCenterLoader.item) {
root.controlCenterLoader.item.close()
return "CONTROL_CENTER_CLOSE_SUCCESS"
}
return "CONTROL_CENTER_CLOSE_FAILED"
}
function toggle(): string {
root.controlCenterLoader.active = true
if (root.controlCenterLoader.item) {
root.controlCenterLoader.item.toggle()
return "CONTROL_CENTER_TOGGLE_SUCCESS"
}
return "CONTROL_CENTER_TOGGLE_FAILED"
}
target: "control-center"
}
IpcHandler {
function open(tab: string): string {
root.dankDashPopoutLoader.active = true
if (root.dankDashPopoutLoader.item) {
switch (tab.toLowerCase()) {
case "media":
root.dankDashPopoutLoader.item.currentTabIndex = 1
break
case "weather":
root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 2 : 0
break
default:
root.dankDashPopoutLoader.item.currentTabIndex = 0
break
}
root.dankDashPopoutLoader.item.setTriggerPosition(Screen.width / 2, Theme.barHeight + Theme.spacingS, 100, "center", Screen)
root.dankDashPopoutLoader.item.dashVisible = true
return "DASH_OPEN_SUCCESS"
}
return "DASH_OPEN_FAILED"
}
function close(): string {
if (root.dankDashPopoutLoader.item) {
root.dankDashPopoutLoader.item.dashVisible = false
return "DASH_CLOSE_SUCCESS"
}
return "DASH_CLOSE_FAILED"
}
function toggle(tab: string): string {
root.dankDashPopoutLoader.active = true
if (root.dankDashPopoutLoader.item) {
if (root.dankDashPopoutLoader.item.dashVisible) {
root.dankDashPopoutLoader.item.dashVisible = false
} else {
switch (tab.toLowerCase()) {
case "media":
root.dankDashPopoutLoader.item.currentTabIndex = 1
break
case "weather":
root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 2 : 0
break
default:
root.dankDashPopoutLoader.item.currentTabIndex = 0
break
}
root.dankDashPopoutLoader.item.setTriggerPosition(Screen.width / 2, Theme.barHeight + Theme.spacingS, 100, "center", Screen)
root.dankDashPopoutLoader.item.dashVisible = true
}
return "DASH_TOGGLE_SUCCESS"
}
return "DASH_TOGGLE_FAILED"
}
target: "dash"
}
IpcHandler {
function getFocusedScreenName() {
if (CompositorService.isHyprland && Hyprland.focusedWorkspace && Hyprland.focusedWorkspace.monitor) {
return Hyprland.focusedWorkspace.monitor.name
}
if (CompositorService.isNiri && NiriService.currentOutput) {
return NiriService.currentOutput
}
return ""
}
function getActiveNotepadInstance() {
if (root.notepadSlideoutVariants.instances.length === 0) {
return null
}
if (root.notepadSlideoutVariants.instances.length === 1) {
return root.notepadSlideoutVariants.instances[0]
}
var focusedScreen = getFocusedScreenName()
if (focusedScreen && root.notepadSlideoutVariants.instances.length > 0) {
for (var i = 0; i < root.notepadSlideoutVariants.instances.length; i++) {
var slideout = root.notepadSlideoutVariants.instances[i]
if (slideout.modelData && slideout.modelData.name === focusedScreen) {
return slideout
}
}
}
for (var i = 0; i < root.notepadSlideoutVariants.instances.length; i++) {
var slideout = root.notepadSlideoutVariants.instances[i]
if (slideout.isVisible) {
return slideout
}
}
return root.notepadSlideoutVariants.instances[0]
}
function open(): string {
var instance = getActiveNotepadInstance()
if (instance) {
instance.show()
return "NOTEPAD_OPEN_SUCCESS"
}
return "NOTEPAD_OPEN_FAILED"
}
function close(): string {
var instance = getActiveNotepadInstance()
if (instance) {
instance.hide()
return "NOTEPAD_CLOSE_SUCCESS"
}
return "NOTEPAD_CLOSE_FAILED"
}
function toggle(): string {
var instance = getActiveNotepadInstance()
if (instance) {
instance.toggle()
return "NOTEPAD_TOGGLE_SUCCESS"
}
return "NOTEPAD_TOGGLE_FAILED"
}
target: "notepad"
}
IpcHandler {
function toggle(): string {
SessionService.toggleIdleInhibit()
return SessionService.idleInhibited ? "Idle inhibit enabled" : "Idle inhibit disabled"
}
function enable(): string {
SessionService.enableIdleInhibit()
return "Idle inhibit enabled"
}
function disable(): string {
SessionService.disableIdleInhibit()
return "Idle inhibit disabled"
}
function status(): string {
return SessionService.idleInhibited ? "Idle inhibit is enabled" : "Idle inhibit is disabled"
}
function reason(newReason: string): string {
if (!newReason) {
return `Current reason: ${SessionService.inhibitReason}`
}
SessionService.setInhibitReason(newReason)
return `Inhibit reason set to: ${newReason}`
}
target: "inhibit"
}
}
+674
View File
@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
+19
View File
@@ -0,0 +1,19 @@
pragma Singleton
import QtQuick
import Quickshell
Singleton {
id: root
readonly property int previewLength: 100
readonly property int longTextThreshold: 200
readonly property int modalWidth: 650
readonly property int modalHeight: 550
readonly property int itemHeight: 72
readonly property int thumbnailSize: 48
readonly property int retryInterval: 50
readonly property int viewportBuffer: 100
readonly property int extendedBuffer: 200
readonly property int keyboardHintsHeight: 80
readonly property int headerHeight: 40
}
+165
View File
@@ -0,0 +1,165 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modals.Clipboard
Item {
id: clipboardContent
required property var modal
required property var filteredModel
required property var clearConfirmDialog
property alias searchField: searchField
property alias clipboardListView: clipboardListView
anchors.fill: parent
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
focus: false
// Header
ClipboardHeader {
id: header
width: parent.width
totalCount: modal.totalCount
showKeyboardHints: modal.showKeyboardHints
onKeyboardHintsToggled: modal.showKeyboardHints = !modal.showKeyboardHints
onClearAllClicked: {
clearConfirmDialog.show(I18n.tr("Clear All History?"), I18n.tr("This will permanently delete all clipboard history."), function () {
modal.clearAll()
modal.hide()
}, function () {})
}
onCloseClicked: modal.hide()
}
// Search Field
DankTextField {
id: searchField
width: parent.width
placeholderText: ""
leftIconName: "search"
showClearButton: true
focus: true
keyForwardTargets: [modal.modalFocusScope]
onTextChanged: {
modal.searchText = text
modal.updateFilteredModel()
}
Keys.onEscapePressed: function (event) {
modal.hide()
event.accepted = true
}
Component.onCompleted: {
Qt.callLater(function () {
forceActiveFocus()
})
}
Connections {
target: modal
function onOpened() {
Qt.callLater(function () {
searchField.forceActiveFocus()
})
}
}
}
// List Container
Rectangle {
width: parent.width
height: parent.height - ClipboardConstants.headerHeight - 70
radius: Theme.cornerRadius
color: "transparent"
clip: true
DankListView {
id: clipboardListView
anchors.fill: parent
model: filteredModel
currentIndex: clipboardContent.modal ? clipboardContent.modal.selectedIndex : 0
spacing: Theme.spacingXS
interactive: true
flickDeceleration: 1500
maximumFlickVelocity: 2000
boundsBehavior: Flickable.DragAndOvershootBounds
boundsMovement: Flickable.FollowBoundsBehavior
pressDelay: 0
flickableDirection: Flickable.VerticalFlick
function ensureVisible(index) {
if (index < 0 || index >= count) {
return
}
const itemHeight = ClipboardConstants.itemHeight + spacing
const itemY = index * itemHeight
const itemBottom = itemY + itemHeight
if (itemY < contentY) {
contentY = itemY
} else if (itemBottom > contentY + height) {
contentY = itemBottom - height
}
}
onCurrentIndexChanged: {
if (clipboardContent.modal && clipboardContent.modal.keyboardNavigationActive && currentIndex >= 0) {
ensureVisible(currentIndex)
}
}
StyledText {
text: I18n.tr("No clipboard entries found")
anchors.centerIn: parent
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
visible: filteredModel.count === 0
}
delegate: ClipboardEntry {
required property int index
required property var model
width: clipboardListView.width
height: ClipboardConstants.itemHeight
entryData: model.entry
entryIndex: index + 1
itemIndex: index
isSelected: clipboardContent.modal && clipboardContent.modal.keyboardNavigationActive && index === clipboardContent.modal.selectedIndex
modal: clipboardContent.modal
listView: clipboardListView
onCopyRequested: clipboardContent.modal.copyEntry(model.entry)
onDeleteRequested: clipboardContent.modal.deleteEntry(model.entry)
}
}
}
// Spacer for keyboard hints
Item {
width: parent.width
height: modal.showKeyboardHints ? ClipboardConstants.keyboardHintsHeight + Theme.spacingL : 0
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
// Keyboard Hints Overlay
ClipboardKeyboardHints {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacingL
visible: modal.showKeyboardHints
}
}
+130
View File
@@ -0,0 +1,130 @@
import QtQuick
import QtQuick.Effects
import Quickshell.Io
import qs.Common
import qs.Widgets
import qs.Modals.Clipboard
Rectangle {
id: entry
required property string entryData
required property int entryIndex
required property int itemIndex
required property bool isSelected
required property var modal
required property var listView
signal copyRequested
signal deleteRequested
readonly property string entryType: modal ? modal.getEntryType(entryData) : "text"
readonly property string entryPreview: modal ? modal.getEntryPreview(entryData) : entryData
radius: Theme.cornerRadius
color: {
if (isSelected) {
return Theme.primaryPressed
}
return mouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceContainerHigh
}
Row {
anchors.fill: parent
anchors.margins: Theme.spacingM
anchors.rightMargin: Theme.spacingS
spacing: Theme.spacingL
// Index indicator
Rectangle {
width: 24
height: 24
radius: 12
color: Theme.primarySelected
anchors.verticalCenter: parent.verticalCenter
StyledText {
anchors.centerIn: parent
text: entryIndex.toString()
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.primary
}
}
// Content area
Row {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 68
spacing: Theme.spacingM
// Thumbnail/Icon
ClipboardThumbnail {
width: entryType === "image" ? ClipboardConstants.thumbnailSize : Theme.iconSize
height: entryType === "image" ? ClipboardConstants.thumbnailSize : Theme.iconSize
anchors.verticalCenter: parent.verticalCenter
entryData: entry.entryData
entryType: entry.entryType
modal: entry.modal
listView: entry.listView
itemIndex: entry.itemIndex
}
// Text content
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - (entryType === "image" ? ClipboardConstants.thumbnailSize : Theme.iconSize) - Theme.spacingM
spacing: Theme.spacingXS
StyledText {
text: {
switch (entryType) {
case "image":
return I18n.tr("Image") + " • " + entryPreview
case "long_text":
return I18n.tr("Long Text")
default:
return I18n.tr("Text")
}
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
font.weight: Font.Medium
width: parent.width
elide: Text.ElideRight
}
StyledText {
text: entryPreview
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
width: parent.width
wrapMode: Text.WordWrap
maximumLineCount: entryType === "long_text" ? 3 : 1
elide: Text.ElideRight
}
}
}
}
// Delete button
DankActionButton {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
iconName: "close"
iconSize: Theme.iconSize - 6
iconColor: Theme.surfaceText
onClicked: deleteRequested()
}
// Click area
MouseArea {
id: mouseArea
anchors.fill: parent
anchors.rightMargin: 40
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: copyRequested()
}
}
+65
View File
@@ -0,0 +1,65 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modals.Clipboard
Item {
id: header
property int totalCount: 0
property bool showKeyboardHints: false
signal keyboardHintsToggled
signal clearAllClicked
signal closeClicked
height: ClipboardConstants.headerHeight
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "content_paste"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Clipboard History") + ` (${totalCount})`
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankActionButton {
iconName: "info"
iconSize: Theme.iconSize - 4
iconColor: showKeyboardHints ? Theme.primary : Theme.surfaceText
onClicked: keyboardHintsToggled()
}
DankActionButton {
iconName: "delete_sweep"
iconSize: Theme.iconSize
iconColor: Theme.surfaceText
onClicked: clearAllClicked()
}
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: closeClicked()
}
}
}
+216
View File
@@ -0,0 +1,216 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Modals.Common
import qs.Services
import qs.Widgets
DankModal {
id: clipboardHistoryModal
property int totalCount: 0
property var clipboardEntries: []
property string searchText: ""
property int selectedIndex: 0
property bool keyboardNavigationActive: false
property bool showKeyboardHints: false
property Component clipboardContent
property int activeImageLoads: 0
readonly property int maxConcurrentLoads: 3
function updateFilteredModel() {
filteredClipboardModel.clear()
for (var i = 0; i < clipboardModel.count; i++) {
const entry = clipboardModel.get(i).entry
if (searchText.trim().length === 0) {
filteredClipboardModel.append({
"entry": entry
})
} else {
const content = getEntryPreview(entry).toLowerCase()
if (content.includes(searchText.toLowerCase())) {
filteredClipboardModel.append({
"entry": entry
})
}
}
}
clipboardHistoryModal.totalCount = filteredClipboardModel.count
if (filteredClipboardModel.count === 0) {
keyboardNavigationActive = false
selectedIndex = 0
} else if (selectedIndex >= filteredClipboardModel.count) {
selectedIndex = filteredClipboardModel.count - 1
}
}
function toggle() {
if (shouldBeVisible) {
hide()
} else {
show()
}
}
function show() {
open()
clipboardHistoryModal.searchText = ""
clipboardHistoryModal.activeImageLoads = 0
refreshClipboard()
keyboardController.reset()
Qt.callLater(function () {
if (contentLoader.item && contentLoader.item.searchField) {
contentLoader.item.searchField.text = ""
contentLoader.item.searchField.forceActiveFocus()
}
})
}
function hide() {
close()
clipboardHistoryModal.searchText = ""
clipboardHistoryModal.activeImageLoads = 0
updateFilteredModel()
keyboardController.reset()
cleanupTempFiles()
}
function cleanupTempFiles() {
Quickshell.execDetached(["sh", "-c", "rm -f /tmp/clipboard_*.png"])
}
function refreshClipboard() {
clipboardProcesses.refresh()
}
function copyEntry(entry) {
const entryId = entry.split('\t')[0]
Quickshell.execDetached(["sh", "-c", `cliphist decode ${entryId} | wl-copy`])
ToastService.showInfo(I18n.tr("Copied to clipboard"))
hide()
}
function deleteEntry(entry) {
clipboardProcesses.deleteEntry(entry)
}
function clearAll() {
clipboardProcesses.clearAll()
}
function getEntryPreview(entry) {
let content = entry.replace(/^\s*\d+\s+/, "")
if (content.includes("image/") || content.includes("binary data") || /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(content)) {
const dimensionMatch = content.match(/(\d+)x(\d+)/)
if (dimensionMatch) {
return `Image ${dimensionMatch[1]}×${dimensionMatch[2]}`
}
const typeMatch = content.match(/\b(png|jpg|jpeg|gif|bmp|webp)\b/i)
if (typeMatch) {
return `Image (${typeMatch[1].toUpperCase()})`
}
return "Image"
}
if (content.length > ClipboardConstants.previewLength) {
return content.substring(0, ClipboardConstants.previewLength) + "..."
}
return content
}
function getEntryType(entry) {
if (entry.includes("image/") || entry.includes("binary data") || /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(entry) || /\b(png|jpg|jpeg|gif|bmp|webp)\b/i.test(entry)) {
return "image"
}
if (entry.length > ClipboardConstants.longTextThreshold) {
return "long_text"
}
return "text"
}
visible: false
width: ClipboardConstants.modalWidth
height: ClipboardConstants.modalHeight
backgroundColor: Theme.popupBackground()
cornerRadius: Theme.cornerRadius
borderColor: Theme.outlineMedium
borderWidth: 1
enableShadow: true
onBackgroundClicked: hide()
modalFocusScope.Keys.onPressed: function (event) {
keyboardController.handleKey(event)
}
content: clipboardContent
ClipboardKeyboardController {
id: keyboardController
modal: clipboardHistoryModal
}
ConfirmModal {
id: clearConfirmDialog
confirmButtonText: I18n.tr("Clear All")
confirmButtonColor: Theme.primary
onVisibleChanged: {
if (visible) {
clipboardHistoryModal.shouldHaveFocus = false
} else if (clipboardHistoryModal.shouldBeVisible) {
clipboardHistoryModal.shouldHaveFocus = true
clipboardHistoryModal.modalFocusScope.forceActiveFocus()
if (clipboardHistoryModal.contentLoader.item && clipboardHistoryModal.contentLoader.item.searchField) {
clipboardHistoryModal.contentLoader.item.searchField.forceActiveFocus()
}
}
}
}
property alias filteredClipboardModel: filteredClipboardModel
property alias clipboardModel: clipboardModel
property var confirmDialog: clearConfirmDialog
ListModel {
id: clipboardModel
}
ListModel {
id: filteredClipboardModel
}
ClipboardProcesses {
id: clipboardProcesses
modal: clipboardHistoryModal
clipboardModel: clipboardModel
filteredClipboardModel: filteredClipboardModel
}
IpcHandler {
function open(): string {
clipboardHistoryModal.show()
return "CLIPBOARD_OPEN_SUCCESS"
}
function close(): string {
clipboardHistoryModal.hide()
return "CLIPBOARD_CLOSE_SUCCESS"
}
function toggle(): string {
clipboardHistoryModal.toggle()
return "CLIPBOARD_TOGGLE_SUCCESS"
}
target: "clipboard"
}
clipboardContent: Component {
ClipboardContent {
modal: clipboardHistoryModal
filteredModel: filteredClipboardModel
clearConfirmDialog: clipboardHistoryModal.confirmDialog
}
}
}
@@ -0,0 +1,131 @@
import QtQuick
import qs.Common
QtObject {
id: keyboardController
required property var modal
function reset() {
modal.selectedIndex = 0
modal.keyboardNavigationActive = false
modal.showKeyboardHints = false
}
function selectNext() {
if (!modal.filteredClipboardModel || modal.filteredClipboardModel.count === 0) {
return
}
modal.keyboardNavigationActive = true
modal.selectedIndex = Math.min(modal.selectedIndex + 1, modal.filteredClipboardModel.count - 1)
}
function selectPrevious() {
if (!modal.filteredClipboardModel || modal.filteredClipboardModel.count === 0) {
return
}
modal.keyboardNavigationActive = true
modal.selectedIndex = Math.max(modal.selectedIndex - 1, 0)
}
function copySelected() {
if (!modal.filteredClipboardModel || modal.filteredClipboardModel.count === 0 || modal.selectedIndex < 0 || modal.selectedIndex >= modal.filteredClipboardModel.count) {
return
}
const selectedEntry = modal.filteredClipboardModel.get(modal.selectedIndex).entry
modal.copyEntry(selectedEntry)
}
function deleteSelected() {
if (!modal.filteredClipboardModel || modal.filteredClipboardModel.count === 0 || modal.selectedIndex < 0 || modal.selectedIndex >= modal.filteredClipboardModel.count) {
return
}
const selectedEntry = modal.filteredClipboardModel.get(modal.selectedIndex).entry
modal.deleteEntry(selectedEntry)
}
function handleKey(event) {
if (event.key === Qt.Key_Escape) {
if (modal.keyboardNavigationActive) {
modal.keyboardNavigationActive = false
event.accepted = true
} else {
modal.hide()
event.accepted = true
}
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_Tab) {
if (!modal.keyboardNavigationActive) {
modal.keyboardNavigationActive = true
modal.selectedIndex = 0
event.accepted = true
} else {
selectNext()
event.accepted = true
}
} else if (event.key === Qt.Key_Up || event.key === Qt.Key_Backtab) {
if (!modal.keyboardNavigationActive) {
modal.keyboardNavigationActive = true
modal.selectedIndex = 0
event.accepted = true
} else if (modal.selectedIndex === 0) {
modal.keyboardNavigationActive = false
event.accepted = true
} else {
selectPrevious()
event.accepted = true
}
} else if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) {
if (!modal.keyboardNavigationActive) {
modal.keyboardNavigationActive = true
modal.selectedIndex = 0
} else {
selectNext()
}
event.accepted = true
} else if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) {
if (!modal.keyboardNavigationActive) {
modal.keyboardNavigationActive = true
modal.selectedIndex = 0
} else if (modal.selectedIndex === 0) {
modal.keyboardNavigationActive = false
} else {
selectPrevious()
}
event.accepted = true
} else if (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) {
if (!modal.keyboardNavigationActive) {
modal.keyboardNavigationActive = true
modal.selectedIndex = 0
} else {
selectNext()
}
event.accepted = true
} else if (event.key === Qt.Key_K && event.modifiers & Qt.ControlModifier) {
if (!modal.keyboardNavigationActive) {
modal.keyboardNavigationActive = true
modal.selectedIndex = 0
} else if (modal.selectedIndex === 0) {
modal.keyboardNavigationActive = false
} else {
selectPrevious()
}
event.accepted = true
} else if (event.key === Qt.Key_Delete && (event.modifiers & Qt.ShiftModifier)) {
modal.clearAll()
modal.hide()
event.accepted = true
} else if (modal.keyboardNavigationActive) {
if ((event.key === Qt.Key_C && (event.modifiers & Qt.ControlModifier)) || event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
copySelected()
event.accepted = true
} else if (event.key === Qt.Key_Delete) {
deleteSelected()
event.accepted = true
}
}
if (event.key === Qt.Key_F10) {
modal.showKeyboardHints = !modal.showKeyboardHints
event.accepted = true
}
}
}
@@ -0,0 +1,44 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modals.Clipboard
Rectangle {
id: keyboardHints
readonly property string hintsText: I18n.tr("Shift+Del: Clear All • Esc: Close")
height: ClipboardConstants.keyboardHintsHeight
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95)
border.color: Theme.primary
border.width: 2
opacity: visible ? 1 : 0
z: 100
Column {
anchors.centerIn: parent
spacing: 2
StyledText {
text: "↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • F10: Help"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: keyboardHints.hintsText
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
+94
View File
@@ -0,0 +1,94 @@
import QtQuick
import Quickshell.Io
QtObject {
id: clipboardProcesses
required property var modal
required property var clipboardModel
required property var filteredClipboardModel
// Load clipboard entries
property var loadProcess: Process {
id: loadProcess
command: ["cliphist", "list"]
running: false
stdout: StdioCollector {
onStreamFinished: {
clipboardModel.clear()
const lines = text.trim().split('\n')
for (const line of lines) {
if (line.trim().length > 0) {
clipboardModel.append({
"entry": line
})
}
}
modal.updateFilteredModel()
}
}
}
// Delete single entry
property var deleteProcess: Process {
id: deleteProcess
property string deletedEntry: ""
running: false
onExited: exitCode => {
if (exitCode === 0) {
for (var i = 0; i < clipboardModel.count; i++) {
if (clipboardModel.get(i).entry === deleteProcess.deletedEntry) {
clipboardModel.remove(i)
break
}
}
for (var j = 0; j < filteredClipboardModel.count; j++) {
if (filteredClipboardModel.get(j).entry === deleteProcess.deletedEntry) {
filteredClipboardModel.remove(j)
break
}
}
modal.totalCount = filteredClipboardModel.count
if (filteredClipboardModel.count === 0) {
modal.keyboardNavigationActive = false
modal.selectedIndex = 0
} else if (modal.selectedIndex >= filteredClipboardModel.count) {
modal.selectedIndex = filteredClipboardModel.count - 1
}
} else {
console.warn("Failed to delete clipboard entry")
}
}
}
// Clear all entries
property var clearProcess: Process {
id: clearProcess
command: ["cliphist", "wipe"]
running: false
onExited: exitCode => {
if (exitCode === 0) {
clipboardModel.clear()
filteredClipboardModel.clear()
modal.totalCount = 0
}
}
}
function refresh() {
loadProcess.running = true
}
function deleteEntry(entry) {
deleteProcess.deletedEntry = entry
deleteProcess.command = ["sh", "-c", `echo '${entry.replace(/'/g, "'\\''")}' | cliphist delete`]
deleteProcess.running = true
}
function clearAll() {
clearProcess.running = true
}
}
+174
View File
@@ -0,0 +1,174 @@
import QtQuick
import QtQuick.Effects
import Quickshell.Io
import qs.Common
import qs.Widgets
import qs.Modals.Clipboard
Item {
id: thumbnail
required property string entryData
required property string entryType
required property var modal
required property var listView
required property int itemIndex
Image {
id: thumbnailImage
property string entryId: entryData.split('\t')[0]
property bool isVisible: false
property string cachedImageData: ""
property bool loadQueued: false
anchors.fill: parent
source: ""
fillMode: Image.PreserveAspectCrop
smooth: true
cache: false
visible: false
asynchronous: true
sourceSize.width: 128
sourceSize.height: 128
onCachedImageDataChanged: {
if (cachedImageData) {
source = ""
source = `data:image/png;base64,${cachedImageData}`
}
}
function tryLoadImage() {
if (!loadQueued && entryType === "image" && !cachedImageData) {
loadQueued = true
if (modal.activeImageLoads < modal.maxConcurrentLoads) {
modal.activeImageLoads++
imageLoader.running = true
} else {
retryTimer.restart()
}
}
}
Timer {
id: retryTimer
interval: ClipboardConstants.retryInterval
onTriggered: {
if (thumbnailImage.loadQueued && !imageLoader.running) {
if (modal.activeImageLoads < modal.maxConcurrentLoads) {
modal.activeImageLoads++
imageLoader.running = true
} else {
retryTimer.restart()
}
}
}
}
Component.onCompleted: {
if (entryType !== "image") {
return
}
// Check if item is visible on screen initially
const itemY = itemIndex * (ClipboardConstants.itemHeight + listView.spacing)
const viewTop = listView.contentY
const viewBottom = viewTop + listView.height
isVisible = (itemY + ClipboardConstants.itemHeight >= viewTop && itemY <= viewBottom)
if (isVisible) {
tryLoadImage()
}
}
Connections {
target: listView
function onContentYChanged() {
if (entryType !== "image") {
return
}
const itemY = itemIndex * (ClipboardConstants.itemHeight + listView.spacing)
const viewTop = listView.contentY - ClipboardConstants.viewportBuffer
const viewBottom = viewTop + listView.height + ClipboardConstants.extendedBuffer
const nowVisible = (itemY + ClipboardConstants.itemHeight >= viewTop && itemY <= viewBottom)
if (nowVisible && !thumbnailImage.isVisible) {
thumbnailImage.isVisible = true
thumbnailImage.tryLoadImage()
}
}
}
Process {
id: imageLoader
running: false
command: ["sh", "-c", `cliphist decode ${thumbnailImage.entryId} | base64 -w 0`]
stdout: StdioCollector {
onStreamFinished: {
const imageData = text.trim()
if (imageData && imageData.length > 0) {
thumbnailImage.cachedImageData = imageData
}
}
}
onExited: exitCode => {
thumbnailImage.loadQueued = false
if (modal.activeImageLoads > 0) {
modal.activeImageLoads--
}
if (exitCode !== 0) {
console.warn("Failed to load clipboard image:", thumbnailImage.entryId)
}
}
}
}
// Rounded mask effect for images
MultiEffect {
anchors.fill: parent
anchors.margins: 2
source: thumbnailImage
maskEnabled: true
maskSource: clipboardCircularMask
visible: entryType === "image" && thumbnailImage.status === Image.Ready && thumbnailImage.source != ""
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
Item {
id: clipboardCircularMask
width: ClipboardConstants.thumbnailSize - 4
height: ClipboardConstants.thumbnailSize - 4
layer.enabled: true
layer.smooth: true
visible: false
Rectangle {
anchors.fill: parent
radius: width / 2
color: "black"
antialiasing: true
}
}
// Fallback icon
DankIcon {
visible: !(entryType === "image" && thumbnailImage.status === Image.Ready && thumbnailImage.source != "")
name: {
if (entryType === "image") {
return "image"
}
if (entryType === "long_text") {
return "subject"
}
return "content_copy"
}
size: Theme.iconSize
color: Theme.primary
anchors.centerIn: parent
}
}
+269
View File
@@ -0,0 +1,269 @@
import QtQuick
import qs.Common
import qs.Modals.Common
import qs.Widgets
DankModal {
id: root
property string confirmTitle: ""
property string confirmMessage: ""
property string confirmButtonText: "Confirm"
property string cancelButtonText: "Cancel"
property color confirmButtonColor: Theme.primary
property var onConfirm: function () {}
property var onCancel: function () {}
property int selectedButton: -1
property bool keyboardNavigation: false
function show(title, message, onConfirmCallback, onCancelCallback) {
confirmTitle = title || ""
confirmMessage = message || ""
confirmButtonText = "Confirm"
cancelButtonText = "Cancel"
confirmButtonColor = Theme.primary
onConfirm = onConfirmCallback || (() => {})
onCancel = onCancelCallback || (() => {})
selectedButton = -1
keyboardNavigation = false
open()
}
function showWithOptions(options) {
confirmTitle = options.title || ""
confirmMessage = options.message || ""
confirmButtonText = options.confirmText || "Confirm"
cancelButtonText = options.cancelText || "Cancel"
confirmButtonColor = options.confirmColor || Theme.primary
onConfirm = options.onConfirm || (() => {})
onCancel = options.onCancel || (() => {})
selectedButton = -1
keyboardNavigation = false
open()
}
function selectButton() {
close()
if (selectedButton === 0) {
if (onCancel) {
onCancel()
}
} else {
if (onConfirm) {
onConfirm()
}
}
}
shouldBeVisible: false
allowStacking: true
width: 350
height: 160
enableShadow: true
shouldHaveFocus: true
onBackgroundClicked: {
close()
if (onCancel) {
onCancel()
}
}
onOpened: {
modalFocusScope.forceActiveFocus()
modalFocusScope.focus = true
shouldHaveFocus = true
}
modalFocusScope.Keys.onPressed: function (event) {
switch (event.key) {
case Qt.Key_Escape:
close()
if (onCancel) {
onCancel()
}
event.accepted = true
break
case Qt.Key_Left:
case Qt.Key_Up:
keyboardNavigation = true
selectedButton = 0
event.accepted = true
break
case Qt.Key_Right:
case Qt.Key_Down:
keyboardNavigation = true
selectedButton = 1
event.accepted = true
break
case Qt.Key_N:
if (event.modifiers & Qt.ControlModifier) {
keyboardNavigation = true
selectedButton = (selectedButton + 1) % 2
event.accepted = true
}
break
case Qt.Key_P:
if (event.modifiers & Qt.ControlModifier) {
keyboardNavigation = true
selectedButton = selectedButton === -1 ? 1 : (selectedButton - 1 + 2) % 2
event.accepted = true
}
break
case Qt.Key_J:
if (event.modifiers & Qt.ControlModifier) {
keyboardNavigation = true
selectedButton = 1
event.accepted = true
}
break
case Qt.Key_K:
if (event.modifiers & Qt.ControlModifier) {
keyboardNavigation = true
selectedButton = 0
event.accepted = true
}
break
case Qt.Key_H:
if (event.modifiers & Qt.ControlModifier) {
keyboardNavigation = true
selectedButton = 0
event.accepted = true
}
break
case Qt.Key_L:
if (event.modifiers & Qt.ControlModifier) {
keyboardNavigation = true
selectedButton = 1
event.accepted = true
}
break
case Qt.Key_Tab:
keyboardNavigation = true
selectedButton = selectedButton === -1 ? 0 : (selectedButton + 1) % 2
event.accepted = true
break
case Qt.Key_Return:
case Qt.Key_Enter:
if (selectedButton !== -1) {
selectButton()
} else {
selectedButton = 1
selectButton()
}
event.accepted = true
break
}
}
content: Component {
Item {
anchors.fill: parent
Column {
anchors.centerIn: parent
width: parent.width - Theme.spacingM * 2
spacing: Theme.spacingM
StyledText {
text: confirmTitle
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
width: parent.width
horizontalAlignment: Text.AlignHCenter
}
StyledText {
text: confirmMessage
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
width: parent.width
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
}
Item {
height: Theme.spacingS
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
Rectangle {
width: 120
height: 40
radius: Theme.cornerRadius
color: {
if (keyboardNavigation && selectedButton === 0) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
} else if (cancelButton.containsMouse) {
return Theme.surfacePressed
} else {
return Theme.surfaceVariantAlpha
}
}
border.color: (keyboardNavigation && selectedButton === 0) ? Theme.primary : "transparent"
border.width: (keyboardNavigation && selectedButton === 0) ? 1 : 0
StyledText {
text: cancelButtonText
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: cancelButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedButton = 0
selectButton()
}
}
}
Rectangle {
width: 120
height: 40
radius: Theme.cornerRadius
color: {
const baseColor = confirmButtonColor
if (keyboardNavigation && selectedButton === 1) {
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 1)
} else if (confirmButton.containsMouse) {
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 0.9)
} else {
return baseColor
}
}
border.color: (keyboardNavigation && selectedButton === 1) ? "white" : "transparent"
border.width: (keyboardNavigation && selectedButton === 1) ? 1 : 0
StyledText {
text: confirmButtonText
font.pixelSize: Theme.fontSizeMedium
color: Theme.primaryText
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: confirmButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedButton = 1
selectButton()
}
}
}
}
}
}
}
}
+245
View File
@@ -0,0 +1,245 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Common
PanelWindow {
id: root
WlrLayershell.namespace: "quickshell:modal"
property alias content: contentLoader.sourceComponent
property alias contentLoader: contentLoader
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)
}
property bool showBackground: true
property real backgroundOpacity: 0.5
property string positioning: "center"
property point customPosition: Qt.point(0, 0)
property bool closeOnEscapeKey: true
property bool closeOnBackgroundClick: true
property string animationType: "scale"
property int animationDuration: Theme.shortDuration
property var animationEasing: Theme.emphasizedEasing
property color backgroundColor: Theme.surfaceContainer
property color borderColor: Theme.outlineMedium
property real borderWidth: 1
property real cornerRadius: Theme.cornerRadius
property bool enableShadow: false
property alias modalFocusScope: focusScope
property bool shouldBeVisible: false
property bool shouldHaveFocus: shouldBeVisible
property bool allowFocusOverride: false
property bool allowStacking: false
property bool keepContentLoaded: false
signal opened
signal dialogClosed
signal backgroundClicked
function open() {
ModalManager.openModal(root)
closeTimer.stop()
shouldBeVisible = true
visible = true
focusScope.forceActiveFocus()
}
function close() {
shouldBeVisible = false
closeTimer.restart()
}
function toggle() {
if (shouldBeVisible) {
close()
} else {
open()
}
}
visible: shouldBeVisible
color: "transparent"
WlrLayershell.layer: WlrLayershell.Top // if set to overlay -> virtual keyboards can be stuck under modal
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: shouldHaveFocus ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
onVisibleChanged: {
if (root.visible) {
opened()
} else {
if (Qt.inputMethod) {
Qt.inputMethod.hide()
Qt.inputMethod.reset()
}
dialogClosed()
}
}
Connections {
function onCloseAllModalsExcept(excludedModal) {
if (excludedModal !== root && !allowStacking && shouldBeVisible) {
close()
}
}
target: ModalManager
}
Timer {
id: closeTimer
interval: animationDuration + 100
onTriggered: {
visible = false
}
}
anchors {
top: true
left: true
right: true
bottom: true
}
Rectangle {
id: background
anchors.fill: parent
color: "black"
opacity: root.showBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
visible: root.showBackground
MouseArea {
anchors.fill: parent
enabled: root.closeOnBackgroundClick
onClicked: mouse => {
const localPos = mapToItem(contentContainer, mouse.x, mouse.y)
if (localPos.x < 0 || localPos.x > contentContainer.width || localPos.y < 0 || localPos.y > contentContainer.height) {
root.backgroundClicked()
}
}
}
Behavior on opacity {
NumberAnimation {
duration: root.animationDuration
easing.type: root.animationEasing
}
}
}
Rectangle {
id: contentContainer
width: px(root.width)
height: px(root.height)
anchors.centerIn: undefined
x: {
if (positioning === "center") {
return snap((root.screenWidth - width) / 2)
} else if (positioning === "top-right") {
return px(Math.max(Theme.spacingL, root.screenWidth - width - Theme.spacingL))
} else if (positioning === "custom") {
return snap(root.customPosition.x)
}
return 0
}
y: {
if (positioning === "center") {
return snap((root.screenHeight - height) / 2)
} else if (positioning === "top-right") {
return px(Theme.barHeight + Theme.spacingXS)
} else if (positioning === "custom") {
return snap(root.customPosition.y)
}
return 0
}
color: root.backgroundColor
radius: root.cornerRadius
border.color: root.borderColor
border.width: root.borderWidth
clip: false
opacity: root.shouldBeVisible ? 1 : 0
transform: root.animationType === "slide" ? slideTransform : null
Translate {
id: slideTransform
readonly property real rawX: root.shouldBeVisible ? 0 : 15
readonly property real rawY: root.shouldBeVisible ? 0 : -30
x: snap(rawX)
y: snap(rawY)
}
Behavior on opacity {
NumberAnimation {
duration: animationDuration
easing.type: animationEasing
}
}
FocusScope {
anchors.fill: parent
focus: root.shouldBeVisible
clip: false
Loader {
id: contentLoader
anchors.fill: parent
active: root.keepContentLoaded || root.shouldBeVisible || root.visible
asynchronous: false
focus: true
clip: false
onLoaded: {
if (item) {
Qt.callLater(() => item.forceActiveFocus())
}
}
}
}
}
FocusScope {
id: focusScope
objectName: "modalFocusScope"
anchors.fill: parent
visible: root.shouldBeVisible || root.visible
focus: root.shouldBeVisible
Keys.onEscapePressed: event => {
if (root.closeOnEscapeKey && shouldHaveFocus) {
root.close()
event.accepted = true
}
}
onVisibleChanged: {
if (visible && shouldHaveFocus) {
Qt.callLater(() => focusScope.forceActiveFocus())
}
}
Connections {
function onShouldHaveFocusChanged() {
if (shouldHaveFocus && shouldBeVisible) {
Qt.callLater(() => focusScope.forceActiveFocus())
}
}
target: root
}
}
}
+580
View File
@@ -0,0 +1,580 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import qs.Common
import qs.Services
import qs.Widgets
PanelWindow {
id: root
property string pickerTitle: "Choose Color"
property color selectedColor: Theme.primary
property bool shouldBeVisible: false
property var onColorSelectedCallback: null
signal colorSelected(color selectedColor)
property color currentColor: Theme.primary
property real hue: 0
property real saturation: 1
property real value: 1
property real alpha: 1
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
}
function show() {
open()
}
function hide() {
close()
}
onColorSelected: (color) => {
if (onColorSelectedCallback) {
onColorSelectedCallback(color)
}
}
function copyColorToClipboard(colorValue) {
Quickshell.execDetached(["sh", "-c", `echo "${colorValue}" | wl-copy`])
ToastService.showInfo(`Color ${colorValue} copied`)
SessionData.addRecentColor(currentColor)
}
function updateFromColor(color) {
hue = color.hsvHue
saturation = color.hsvSaturation
value = color.hsvValue
alpha = color.a
gradientX = saturation
gradientY = 1 - value
}
function updateColor() {
currentColor = Qt.hsva(hue, saturation, value, alpha)
}
function updateColorFromGradient(x, y) {
saturation = Math.max(0, Math.min(1, x))
value = Math.max(0, Math.min(1, 1 - y))
updateColor()
}
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()
}
}
}
onExited: (exitCode, exitStatus) => {
if (exitCode !== 0) {
console.warn("hyprpicker exited with code:", exitCode)
}
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"
]
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
}
FocusScope {
id: colorContent
anchors.fill: parent
focus: root.shouldBeVisible
Keys.onEscapePressed: event => {
root.close()
event.accepted = true
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingS
Column {
width: parent.width - 90
spacing: Theme.spacingXS
StyledText {
text: root.pickerTitle
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: I18n.tr("Select a color from the palette or use custom sliders")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium
}
}
DankActionButton {
iconName: "colorize"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: () => {
pickColorFromScreen()
}
}
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: () => {
root.close()
}
}
}
Row {
width: parent.width
spacing: Theme.spacingM
Rectangle {
id: gradientPicker
width: parent.width - 70
height: 280
radius: Theme.cornerRadius
border.color: Theme.outlineStrong
border.width: 1
clip: true
Rectangle {
anchors.fill: parent
color: Qt.hsva(root.hue, 1, 1, 1)
Rectangle {
anchors.fill: parent
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop { position: 0.0; color: "#ffffff" }
GradientStop { position: 1.0; color: "transparent" }
}
}
Rectangle {
anchors.fill: parent
gradient: Gradient {
orientation: Gradient.Vertical
GradientStop { position: 0.0; color: "transparent" }
GradientStop { position: 1.0; color: "#000000" }
}
}
}
Rectangle {
id: pickerCircle
width: 16
height: 16
radius: 8
border.color: "white"
border.width: 2
color: "transparent"
x: root.gradientX * parent.width - width / 2
y: root.gradientY * parent.height - height / 2
Rectangle {
anchors.centerIn: parent
width: parent.width - 4
height: parent.height - 4
radius: width / 2
border.color: "black"
border.width: 1
color: "transparent"
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.CrossCursor
onPressed: mouse => {
const x = Math.max(0, Math.min(1, mouse.x / width))
const y = Math.max(0, Math.min(1, mouse.y / height))
root.gradientX = x
root.gradientY = y
root.updateColorFromGradient(x, y)
}
onPositionChanged: mouse => {
if (pressed) {
const x = Math.max(0, Math.min(1, mouse.x / width))
const y = Math.max(0, Math.min(1, mouse.y / height))
root.gradientX = x
root.gradientY = y
root.updateColorFromGradient(x, y)
}
}
}
}
Rectangle {
id: hueSlider
width: 50
height: 280
radius: Theme.cornerRadius
border.color: Theme.outlineStrong
border.width: 1
gradient: Gradient {
orientation: Gradient.Vertical
GradientStop { position: 0.00; color: "#ff0000" }
GradientStop { position: 0.17; color: "#ffff00" }
GradientStop { position: 0.33; color: "#00ff00" }
GradientStop { position: 0.50; color: "#00ffff" }
GradientStop { position: 0.67; color: "#0000ff" }
GradientStop { position: 0.83; color: "#ff00ff" }
GradientStop { position: 1.00; color: "#ff0000" }
}
Rectangle {
id: hueIndicator
width: parent.width
height: 4
color: "white"
border.color: "black"
border.width: 1
y: root.hue * parent.height - height / 2
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.SizeVerCursor
onPressed: mouse => {
const h = Math.max(0, Math.min(1, mouse.y / height))
root.hue = h
root.updateColor()
}
onPositionChanged: mouse => {
if (pressed) {
const h = Math.max(0, Math.min(1, mouse.y / height))
root.hue = h
root.updateColor()
}
}
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: I18n.tr("Material Colors")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
GridView {
width: parent.width
height: 140
cellWidth: 38
cellHeight: 38
clip: true
interactive: false
model: root.standardColors
delegate: Rectangle {
width: 36
height: 36
color: modelData
radius: 4
border.color: Theme.outlineStrong
border.width: 1
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: () => {
root.currentColor = modelData
root.updateFromColor(root.currentColor)
}
}
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
Row {
width: parent.width
spacing: Theme.spacingS
Column {
width: 210
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Recent Colors")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
Row {
width: parent.width
spacing: Theme.spacingXS
Repeater {
model: 5
Rectangle {
width: 36
height: 36
radius: 4
border.color: Theme.outlineStrong
border.width: 1
color: {
if (index < SessionData.recentColors.length) {
return SessionData.recentColors[index]
}
return Theme.surfaceContainerHigh
}
opacity: index < SessionData.recentColors.length ? 1.0 : 0.3
MouseArea {
anchors.fill: parent
cursorShape: index < SessionData.recentColors.length ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: index < SessionData.recentColors.length
onClicked: () => {
if (index < SessionData.recentColors.length) {
root.currentColor = SessionData.recentColors[index]
root.updateFromColor(root.currentColor)
}
}
}
}
}
}
}
Column {
width: parent.width - 330
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Opacity")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
DankSlider {
width: parent.width
value: Math.round(root.alpha * 100)
minimum: 0
maximum: 100
showValue: false
onSliderValueChanged: (newValue) => {
root.alpha = newValue / 100
root.updateColor()
}
}
}
Rectangle {
width: 100
height: 50
radius: Theme.cornerRadius
color: root.currentColor
border.color: Theme.outlineStrong
border.width: 2
anchors.verticalCenter: parent.verticalCenter
}
}
}
Row {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: I18n.tr("Hex:")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium
anchors.verticalCenter: parent.verticalCenter
}
DankTextField {
id: hexInput
width: 120
height: 38
text: root.currentColor.toString()
font.pixelSize: Theme.fontSizeMedium
textColor: {
if (text.length === 0) return Theme.surfaceText
const hexPattern = /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/
return hexPattern.test(text) ? Theme.surfaceText : Theme.error
}
placeholderText: "#000000"
backgroundColor: Theme.surfaceHover
borderWidth: 1
focusedBorderWidth: 2
topPadding: Theme.spacingS
bottomPadding: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
onAccepted: () => {
const hexPattern = /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/
if (!hexPattern.test(text)) return
const color = Qt.color(text)
if (color) {
root.currentColor = color
root.updateFromColor(color)
}
}
}
DankButton {
width: 80
buttonHeight: 36
text: I18n.tr("Apply")
backgroundColor: Theme.primary
textColor: Theme.background
anchors.verticalCenter: parent.verticalCenter
onClicked: {
const hexPattern = /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/
if (!hexPattern.test(hexInput.text)) return
const color = Qt.color(hexInput.text)
if (color) {
root.currentColor = color
root.updateFromColor(color)
root.selectedColor = root.currentColor
colorSelected(root.currentColor)
SessionData.addRecentColor(root.currentColor)
root.close()
}
}
}
Item {
width: parent.width - 460
height: 1
}
DankButton {
width: 70
buttonHeight: 36
text: I18n.tr("Cancel")
backgroundColor: "transparent"
textColor: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
onClicked: root.close()
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: "transparent"
border.color: Theme.surfaceVariantAlpha
border.width: 1
z: -1
}
}
DankButton {
width: 70
buttonHeight: 36
text: I18n.tr("Copy")
backgroundColor: Theme.primary
textColor: Theme.background
anchors.verticalCenter: parent.verticalCenter
onClicked: {
const colorString = root.currentColor.toString()
copyColorToClipboard(colorString)
}
}
}
}
}
}
}
File diff suppressed because it is too large Load Diff
+237
View File
@@ -0,0 +1,237 @@
import QtQuick
import QtCore
import Quickshell.Io
import qs.Common
import qs.Widgets
Rectangle {
id: root
property bool showFileInfo: false
property int selectedIndex: -1
property var sourceFolderModel: null
property string currentPath: ""
height: 200
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95)
border.color: Theme.secondary
border.width: 2
opacity: showFileInfo ? 1 : 0
z: 100
onShowFileInfoChanged: {
if (showFileInfo && currentFileName && currentPath) {
const fullPath = currentPath + "/" + currentFileName
fileStatProcess.selectedFilePath = fullPath
fileStatProcess.running = true
}
}
Process {
id: fileStatProcess
command: ["stat", "-c", "%y|%A|%s|%n", selectedFilePath]
property string selectedFilePath: ""
property var fileStats: null
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text && text.trim()) {
const parts = text.trim().split('|')
if (parts.length >= 4) {
fileStatProcess.fileStats = {
"modifiedTime": parts[0],
"permissions": parts[1],
"size": parseInt(parts[2]) || 0,
"fullPath": parts[3]
}
}
}
}
}
onExited: function (exitCode) {}
}
property string currentFileName: ""
property bool currentFileIsDir: false
property string currentFileExtension: ""
onCurrentFileNameChanged: {
if (showFileInfo && currentFileName && currentPath) {
const fullPath = currentPath + "/" + currentFileName
if (fullPath !== fileStatProcess.selectedFilePath) {
fileStatProcess.selectedFilePath = fullPath
fileStatProcess.running = true
}
}
}
function updateFileInfo(filePath, fileName, isDirectory) {
if (filePath && filePath !== fileStatProcess.selectedFilePath) {
fileStatProcess.selectedFilePath = filePath
currentFileName = fileName || ""
currentFileIsDir = isDirectory || false
let ext = ""
if (!isDirectory && fileName) {
const lastDot = fileName.lastIndexOf('.')
if (lastDot > 0) {
ext = fileName.substring(lastDot + 1).toLowerCase()
}
}
currentFileExtension = ext
if (showFileInfo) {
fileStatProcess.running = true
}
}
}
readonly property var currentFileDisplayData: {
if (selectedIndex < 0 || !sourceFolderModel) {
return {
"exists": false,
"name": "No selection",
"type": "",
"size": "",
"modified": "",
"permissions": "",
"extension": "",
"position": "N/A"
}
}
const hasValidFile = currentFileName !== ""
return {
"exists": hasValidFile,
"name": hasValidFile ? currentFileName : "Loading...",
"type": currentFileIsDir ? "Directory" : "File",
"size": fileStatProcess.fileStats ? formatFileSize(fileStatProcess.fileStats.size) : "Calculating...",
"modified": fileStatProcess.fileStats ? formatDateTime(fileStatProcess.fileStats.modifiedTime) : "Loading...",
"permissions": fileStatProcess.fileStats ? fileStatProcess.fileStats.permissions : "Loading...",
"extension": currentFileExtension,
"position": sourceFolderModel ? ((selectedIndex + 1) + " of " + sourceFolderModel.count) : "N/A"
}
}
Column {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacingM
spacing: Theme.spacingXS
Row {
width: parent.width
spacing: Theme.spacingS
DankIcon {
name: "info"
size: Theme.iconSize
color: Theme.secondary
}
StyledText {
text: I18n.tr("File Information")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: currentFileDisplayData.name
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
width: parent.width
elide: Text.ElideMiddle
wrapMode: Text.NoWrap
font.weight: Font.Medium
}
StyledText {
text: currentFileDisplayData.type + (currentFileDisplayData.extension ? " (." + currentFileDisplayData.extension + ")" : "")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
width: parent.width
}
StyledText {
text: currentFileDisplayData.size
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
width: parent.width
visible: currentFileDisplayData.exists && !currentFileIsDir
}
StyledText {
text: currentFileDisplayData.modified
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
width: parent.width
elide: Text.ElideRight
visible: currentFileDisplayData.exists
}
StyledText {
text: currentFileDisplayData.permissions
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
visible: currentFileDisplayData.exists
}
StyledText {
text: currentFileDisplayData.position
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
width: parent.width
}
}
}
StyledText {
text: I18n.tr("F1/I: Toggle • F10: Help")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacingM
horizontalAlignment: Text.AlignHCenter
}
function formatFileSize(bytes) {
if (bytes === 0 || !bytes) {
return "0 B"
}
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
}
function formatDateTime(dateTimeString) {
if (!dateTimeString) {
return "Unknown"
}
const parts = dateTimeString.split(' ')
if (parts.length >= 2) {
return parts[0] + " " + parts[1].split('.')[0]
}
return dateTimeString
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
+50
View File
@@ -0,0 +1,50 @@
import QtQuick
import qs.Common
import qs.Widgets
Rectangle {
id: root
property bool showHints: false
height: 80
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95)
border.color: Theme.primary
border.width: 2
opacity: showHints ? 1 : 0
z: 100
Column {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacingS
spacing: 2
StyledText {
text: I18n.tr("Tab/Shift+Tab: Nav • ←→↑↓: Grid Nav • Enter/Space: Select")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
width: parent.width
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
}
StyledText {
text: I18n.tr("Alt+←/Backspace: Back • F1/I: File Info • F10: Help • Esc: Close")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
width: parent.width
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
+162
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 networkInfoModalVisible: false
property string networkSSID: ""
property var networkData: null
function showNetworkInfo(ssid, data) {
networkSSID = ssid
networkData = data
networkInfoModalVisible = true
open()
NetworkService.fetchNetworkInfo(ssid)
}
function hideDialog() {
networkInfoModalVisible = false
close()
networkSSID = ""
networkData = null
}
visible: networkInfoModalVisible
width: 600
height: 500
enableShadow: true
onBackgroundClicked: hideDialog()
onVisibleChanged: {
if (!visible) {
networkSSID = ""
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 "${networkSSID}"`
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.networkInfoDetails && NetworkService.networkInfoDetails.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
}
}
}
}
}
}
}
}
+147
View File
@@ -0,0 +1,147 @@
import QtQuick
import Quickshell.Io
import qs.Common
import qs.Modals.Common
import qs.Modules.Notifications.Center
import qs.Services
import qs.Widgets
DankModal {
id: notificationModal
property bool notificationModalOpen: false
property var notificationListRef: null
function show() {
notificationModalOpen = true
NotificationService.onOverlayOpen()
open()
modalKeyboardController.reset()
if (modalKeyboardController && notificationListRef) {
modalKeyboardController.listView = notificationListRef
modalKeyboardController.rebuildFlatNavigation()
Qt.callLater(() => {
modalKeyboardController.keyboardNavigationActive = true
modalKeyboardController.selectedFlatIndex = 0
modalKeyboardController.updateSelectedIdFromIndex()
if (notificationListRef) {
notificationListRef.keyboardActive = true
notificationListRef.currentIndex = 0
}
modalKeyboardController.selectionVersion++
modalKeyboardController.ensureVisible()
})
}
}
function hide() {
notificationModalOpen = false
NotificationService.onOverlayClose()
close()
modalKeyboardController.reset()
}
function toggle() {
if (shouldBeVisible) {
hide()
} else {
show()
}
}
width: 500
height: 700
visible: false
onBackgroundClicked: hide()
onShouldBeVisibleChanged: (shouldBeVisible) => {
if (!shouldBeVisible) {
notificationModalOpen = false
modalKeyboardController.reset()
NotificationService.onOverlayClose()
}
}
modalFocusScope.Keys.onPressed: (event) => modalKeyboardController.handleKey(event)
NotificationKeyboardController {
id: modalKeyboardController
listView: null
isOpen: notificationModal.notificationModalOpen
onClose: () => notificationModal.hide()
}
IpcHandler {
function open(): string {
notificationModal.show();
return "NOTIFICATION_MODAL_OPEN_SUCCESS";
}
function close(): string {
notificationModal.hide();
return "NOTIFICATION_MODAL_CLOSE_SUCCESS";
}
function toggle(): string {
notificationModal.toggle();
return "NOTIFICATION_MODAL_TOGGLE_SUCCESS";
}
target: "notifications"
}
content: Component {
Item {
id: notificationKeyHandler
anchors.fill: parent
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
NotificationHeader {
id: notificationHeader
keyboardController: modalKeyboardController
}
NotificationSettings {
id: notificationSettings
expanded: notificationHeader.showSettings
}
KeyboardNavigatedNotificationList {
id: notificationList
width: parent.width
height: parent.height - y
keyboardController: modalKeyboardController
Component.onCompleted: {
notificationModal.notificationListRef = notificationList
if (modalKeyboardController) {
modalKeyboardController.listView = notificationList
modalKeyboardController.rebuildFlatNavigation()
}
}
}
}
NotificationKeyboardHints {
id: keyboardHints
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacingL
showHints: modalKeyboardController.showKeyboardHints
}
}
}
}
+454
View File
@@ -0,0 +1,454 @@
import QtQuick
import qs.Common
import qs.Modals.Common
import qs.Services
import qs.Widgets
DankModal {
id: root
property int selectedIndex: 0
property int optionCount: SessionService.hibernateSupported ? 5 : 4
property rect parentBounds: Qt.rect(0, 0, 0, 0)
property var parentScreen: null
signal powerActionRequested(string action, string title, string message)
function openCentered() {
parentBounds = Qt.rect(0, 0, 0, 0)
parentScreen = null
backgroundOpacity = 0.5
open()
}
function openFromControlCenter(bounds, targetScreen) {
parentBounds = bounds
parentScreen = targetScreen
backgroundOpacity = 0
open()
}
function selectOption(action) {
close();
const actions = {
"logout": {
"title": I18n.tr("Log Out"),
"message": I18n.tr("Are you sure you want to log out?")
},
"suspend": {
"title": I18n.tr("Suspend"),
"message": I18n.tr("Are you sure you want to suspend the system?")
},
"hibernate": {
"title": I18n.tr("Hibernate"),
"message": I18n.tr("Are you sure you want to hibernate the system?")
},
"reboot": {
"title": I18n.tr("Reboot"),
"message": I18n.tr("Are you sure you want to reboot the system?")
},
"poweroff": {
"title": I18n.tr("Power Off"),
"message": I18n.tr("Are you sure you want to power off the system?")
}
}
const selected = actions[action]
if (selected) {
root.powerActionRequested(action, selected.title, selected.message);
}
}
shouldBeVisible: false
width: 320
height: contentLoader.item ? contentLoader.item.implicitHeight : 300
enableShadow: true
screen: parentScreen
positioning: parentBounds.width > 0 ? "custom" : "center"
customPosition: {
if (parentBounds.width > 0) {
const centerX = parentBounds.x + (parentBounds.width - width) / 2
const centerY = parentBounds.y + (parentBounds.height - height) / 2
return Qt.point(centerX, centerY)
}
return Qt.point(0, 0)
}
onBackgroundClicked: () => {
return close();
}
onOpened: () => {
selectedIndex = 0;
modalFocusScope.forceActiveFocus();
}
modalFocusScope.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 = ["logout", "suspend"];
if (SessionService.hibernateSupported) actions.push("hibernate");
actions.push("reboot", "poweroff");
if (selectedIndex < actions.length) {
selectOption(actions[selectedIndex]);
}
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;
}
}
content: Component {
Item {
anchors.fill: parent
implicitHeight: mainColumn.implicitHeight + Theme.spacingL * 2
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: () => {
return close();
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
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: () => {
selectedIndex = 0;
selectOption("logout");
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: {
if (selectedIndex === 1) {
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 === 1 ? Theme.primary : "transparent"
border.width: selectedIndex === 1 ? 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: () => {
selectedIndex = 1;
selectOption("suspend");
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: {
if (selectedIndex === 2) {
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 === 2 ? Theme.primary : "transparent"
border.width: selectedIndex === 2 ? 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: () => {
selectedIndex = 2;
selectOption("hibernate");
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: {
const rebootIndex = SessionService.hibernateSupported ? 3 : 2;
if (selectedIndex === rebootIndex) {
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: selectedIndex === (SessionService.hibernateSupported ? 3 : 2) ? Theme.primary : "transparent"
border.width: selectedIndex === (SessionService.hibernateSupported ? 3 : 2) ? 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: () => {
selectedIndex = SessionService.hibernateSupported ? 3 : 2;
selectOption("reboot");
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: {
const powerOffIndex = SessionService.hibernateSupported ? 4 : 3;
if (selectedIndex === powerOffIndex) {
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: selectedIndex === (SessionService.hibernateSupported ? 4 : 3) ? Theme.primary : "transparent"
border.width: selectedIndex === (SessionService.hibernateSupported ? 4 : 3) ? 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: () => {
selectedIndex = SessionService.hibernateSupported ? 4 : 3;
selectOption("poweroff");
}
}
}
}
Item {
height: Theme.spacingS
}
}
}
}
}
+356
View File
@@ -0,0 +1,356 @@
import QtQuick
import QtQuick.Layouts
import qs.Common
import qs.Modals.Common
import qs.Modules.ProcessList
import qs.Services
import qs.Widgets
DankModal {
id: processListModal
property int currentTab: 0
property var tabNames: ["Processes", "Performance", "System"]
function show() {
if (!DgopService.dgopAvailable) {
console.warn("ProcessListModal: dgop is not available");
return ;
}
open();
UserInfoService.getUptime();
}
function hide() {
close();
if (processContextMenu.visible) {
processContextMenu.close();
}
}
function toggle() {
if (!DgopService.dgopAvailable) {
console.warn("ProcessListModal: dgop is not available");
return ;
}
if (shouldBeVisible) {
hide();
} else {
show();
}
}
width: 900
height: 680
visible: false
backgroundColor: Theme.popupBackground()
cornerRadius: Theme.cornerRadius
enableShadow: true
onBackgroundClicked: () => {
return hide();
}
Component {
id: processesTabComponent
ProcessesTab {
contextMenu: processContextMenu
}
}
Component {
id: performanceTabComponent
PerformanceTab {
}
}
Component {
id: systemTabComponent
SystemTab {
}
}
ProcessContextMenu {
id: processContextMenu
}
content: Component {
Item {
anchors.fill: parent
focus: true
Keys.onPressed: (event) => {
if (event.key === Qt.Key_Escape) {
processListModal.hide();
event.accepted = true;
} else if (event.key === Qt.Key_1) {
currentTab = 0;
event.accepted = true;
} else if (event.key === Qt.Key_2) {
currentTab = 1;
event.accepted = true;
} else if (event.key === Qt.Key_3) {
currentTab = 2;
event.accepted = true;
}
}
// Show error message when dgop is not available
Rectangle {
anchors.centerIn: parent
width: 400
height: 200
radius: Theme.cornerRadius
color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.1)
border.color: Theme.error
border.width: 2
visible: !DgopService.dgopAvailable
Column {
anchors.centerIn: parent
spacing: Theme.spacingL
DankIcon {
name: "error"
size: 48
color: Theme.error
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("System Monitor Unavailable")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.error
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("The 'dgop' tool is required for system monitoring.\nPlease install dgop to use this feature.")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
}
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
visible: DgopService.dgopAvailable
RowLayout {
Layout.fillWidth: true
height: 40
StyledText {
text: I18n.tr("System Monitor")
font.pixelSize: Theme.fontSizeLarge + 4
font.weight: Font.Bold
color: Theme.surfaceText
Layout.alignment: Qt.AlignVCenter
}
Item {
Layout.fillWidth: true
}
DankActionButton {
circular: false
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: () => {
return processListModal.hide();
}
Layout.alignment: Qt.AlignVCenter
}
}
Rectangle {
Layout.fillWidth: true
height: 52
color: Theme.surfaceContainerHigh
radius: Theme.cornerRadius
border.color: Theme.outlineLight
border.width: 1
Row {
anchors.fill: parent
anchors.margins: 4
spacing: 2
Repeater {
model: tabNames
Rectangle {
width: (parent.width - (tabNames.length - 1) * 2) / tabNames.length
height: 44
radius: Theme.cornerRadius
color: currentTab === index ? Theme.primaryPressed : (tabMouseArea.containsMouse ? Theme.primaryHoverLight : "transparent")
border.color: currentTab === index ? Theme.primary : "transparent"
border.width: currentTab === index ? 1 : 0
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: {
const tabIcons = ["list_alt", "analytics", "settings"];
return tabIcons[index] || "tab";
}
size: Theme.iconSize - 2
color: currentTab === index ? Theme.primary : Theme.surfaceText
opacity: currentTab === index ? 1 : 0.7
anchors.verticalCenter: parent.verticalCenter
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
StyledText {
text: modelData
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: currentTab === index ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: -1
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
}
MouseArea {
id: tabMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: () => {
currentTab = index;
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Theme.outlineLight
border.width: 1
Loader {
id: processesTab
anchors.fill: parent
anchors.margins: Theme.spacingS
active: processListModal.visible && currentTab === 0
visible: currentTab === 0
opacity: currentTab === 0 ? 1 : 0
sourceComponent: processesTabComponent
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
Loader {
id: performanceTab
anchors.fill: parent
anchors.margins: Theme.spacingS
active: processListModal.visible && currentTab === 1
visible: currentTab === 1
opacity: currentTab === 1 ? 1 : 0
sourceComponent: performanceTabComponent
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
Loader {
id: systemTab
anchors.fill: parent
anchors.margins: Theme.spacingS
active: processListModal.visible && currentTab === 2
visible: currentTab === 2
opacity: currentTab === 2 ? 1 : 0
sourceComponent: systemTabComponent
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
}
}
}
}
+242
View File
@@ -0,0 +1,242 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: powerTab
DankFlickable {
anchors.fill: parent
anchors.topMargin: Theme.spacingL
clip: true
contentHeight: mainColumn.height
contentWidth: width
Column {
id: mainColumn
width: parent.width
spacing: Theme.spacingXL
StyledText {
text: I18n.tr("Battery not detected - only AC power settings available")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
visible: !BatteryService.batteryAvailable
}
StyledRect {
width: parent.width
height: timeoutSection.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: timeoutSection
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
}
StyledText {
text: I18n.tr("Idle Settings")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: Math.max(0, parent.width - parent.children[0].width - parent.children[1].width - powerCategory.width - Theme.spacingM * 3)
height: parent.height
}
DankButtonGroup {
id: powerCategory
anchors.verticalCenter: parent.verticalCenter
visible: BatteryService.batteryAvailable
model: ["AC Power", "Battery"]
currentIndex: 0
selectionMode: "single"
checkEnabled: false
}
}
DankDropdown {
id: lockDropdown
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
text: I18n.tr("Automatically lock after")
options: timeoutOptions
Connections {
target: powerCategory
function onCurrentIndexChanged() {
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acLockTimeout : SessionData.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 index = timeoutValues.indexOf(currentTimeout)
currentValue = index >= 0 ? timeoutOptions[index] : "Never"
}
onValueChanged: value => {
const index = timeoutOptions.indexOf(value)
if (index >= 0) {
const timeout = timeoutValues[index]
if (powerCategory.currentIndex === 0) {
SessionData.setAcLockTimeout(timeout)
} else {
SessionData.setBatteryLockTimeout(timeout)
}
}
}
}
DankDropdown {
id: monitorDropdown
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
text: I18n.tr("Turn off monitors after")
options: timeoutOptions
Connections {
target: powerCategory
function onCurrentIndexChanged() {
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acMonitorTimeout : SessionData.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 index = timeoutValues.indexOf(currentTimeout)
currentValue = index >= 0 ? timeoutOptions[index] : "Never"
}
onValueChanged: value => {
const index = timeoutOptions.indexOf(value)
if (index >= 0) {
const timeout = timeoutValues[index]
if (powerCategory.currentIndex === 0) {
SessionData.setAcMonitorTimeout(timeout)
} else {
SessionData.setBatteryMonitorTimeout(timeout)
}
}
}
}
DankDropdown {
id: suspendDropdown
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
text: I18n.tr("Suspend system after")
options: timeoutOptions
Connections {
target: powerCategory
function onCurrentIndexChanged() {
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acSuspendTimeout : SessionData.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 index = timeoutValues.indexOf(currentTimeout)
currentValue = index >= 0 ? timeoutOptions[index] : "Never"
}
onValueChanged: value => {
const index = timeoutOptions.indexOf(value)
if (index >= 0) {
const timeout = timeoutValues[index]
if (powerCategory.currentIndex === 0) {
SessionData.setAcSuspendTimeout(timeout)
} else {
SessionData.setBatterySuspendTimeout(timeout)
}
}
}
}
DankDropdown {
id: hibernateDropdown
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
text: I18n.tr("Hibernate system after")
options: timeoutOptions
visible: SessionService.hibernateSupported
Connections {
target: powerCategory
function onCurrentIndexChanged() {
const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acHibernateTimeout : SessionData.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 index = timeoutValues.indexOf(currentTimeout)
currentValue = index >= 0 ? timeoutOptions[index] : "Never"
}
onValueChanged: value => {
const index = timeoutOptions.indexOf(value)
if (index >= 0) {
const timeout = timeoutValues[index]
if (powerCategory.currentIndex === 0) {
SessionData.setAcHibernateTimeout(timeout)
} else {
SessionData.setBatteryHibernateTimeout(timeout)
}
}
}
}
DankToggle {
width: parent.width
text: I18n.tr("Lock before suspend")
description: "Automatically lock the screen when the system prepares to suspend"
checked: SessionData.lockBeforeSuspend
onToggled: checked => SessionData.setLockBeforeSuspend(checked)
}
StyledText {
text: I18n.tr("Idle monitoring not supported - requires newer Quickshell version")
font.pixelSize: Theme.fontSizeSmall
color: Theme.error
anchors.horizontalCenter: parent.horizontalCenter
visible: !IdleService.idleMonitorAvailable
}
}
}
}
}
}
+153
View File
@@ -0,0 +1,153 @@
import QtQuick
import QtQuick.Effects
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
property var parentModal: null
width: parent.width - Theme.spacingS * 2
height: 110
radius: Theme.cornerRadius
color: "transparent"
border.width: 0
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingM
Item {
id: profileImageContainer
width: 80
height: 80
anchors.verticalCenter: parent.verticalCenter
DankCircularImage {
id: profileImage
anchors.fill: parent
imageSource: {
if (PortalService.profileImage === "") {
return "";
}
if (PortalService.profileImage.startsWith("/")) {
return "file://" + PortalService.profileImage;
}
return PortalService.profileImage;
}
fallbackIcon: "person"
}
Rectangle {
anchors.fill: parent
radius: width / 2
color: Qt.rgba(0, 0, 0, 0.7)
visible: profileMouseArea.containsMouse
Row {
anchors.centerIn: parent
spacing: 4
Rectangle {
width: 28
height: 28
radius: 14
color: Qt.rgba(255, 255, 255, 0.9)
DankIcon {
anchors.centerIn: parent
name: "edit"
size: 16
color: "black"
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: () => {
if (root.parentModal) {
root.parentModal.allowFocusOverride = true;
root.parentModal.shouldHaveFocus = false;
if (root.parentModal.profileBrowser) {
root.parentModal.profileBrowser.open();
}
}
}
}
}
Rectangle {
width: 28
height: 28
radius: 14
color: Qt.rgba(255, 255, 255, 0.9)
visible: profileImage.hasImage
DankIcon {
anchors.centerIn: parent
name: "close"
size: 16
color: "black"
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: () => {
return PortalService.setProfileImage("");
}
}
}
}
}
MouseArea {
id: profileMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
propagateComposedEvents: true
acceptedButtons: Qt.NoButton
}
}
Column {
width: 120
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
StyledText {
text: UserInfoService.fullName || "User"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
elide: Text.ElideRight
width: parent.width
}
StyledText {
text: DgopService.distribution || "Linux"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
elide: Text.ElideRight
width: parent.width
}
}
}
}
+187
View File
@@ -0,0 +1,187 @@
import QtQuick
import qs.Common
import qs.Modules.Settings
FocusScope {
id: root
property int currentIndex: 0
property var parentModal: null
focus: true
Rectangle {
anchors.fill: parent
anchors.leftMargin: 0
anchors.rightMargin: Theme.spacingS
anchors.bottomMargin: Theme.spacingM
anchors.topMargin: 0
color: "transparent"
Loader {
id: personalizationLoader
anchors.fill: parent
active: root.currentIndex === 0
visible: active
asynchronous: true
sourceComponent: Component {
PersonalizationTab {
parentModal: root.parentModal
}
}
}
Loader {
id: timeLoader
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 {
}
}
Loader {
id: topBarLoader
anchors.fill: parent
active: root.currentIndex === 3
visible: active
asynchronous: true
sourceComponent: DankBarTab {
parentModal: root.parentModal
}
}
Loader {
id: widgetsLoader
anchors.fill: parent
active: root.currentIndex === 4
visible: active
asynchronous: true
sourceComponent: WidgetTweaksTab {
}
}
Loader {
id: dockLoader
anchors.fill: parent
active: root.currentIndex === 5
visible: active
asynchronous: true
sourceComponent: Component {
DockTab {
}
}
}
Loader {
id: displaysLoader
anchors.fill: parent
active: root.currentIndex === 6
visible: active
asynchronous: true
sourceComponent: DisplaysTab {
}
}
Loader {
id: launcherLoader
anchors.fill: parent
active: root.currentIndex === 7
visible: active
asynchronous: true
sourceComponent: LauncherTab {
}
}
Loader {
id: themeColorsLoader
anchors.fill: parent
active: root.currentIndex === 8
visible: active
asynchronous: true
sourceComponent: ThemeColorsTab {
}
}
Loader {
id: powerLoader
anchors.fill: parent
active: root.currentIndex === 9
visible: active
asynchronous: true
sourceComponent: PowerSettings {
}
}
Loader {
id: pluginsLoader
anchors.fill: parent
active: root.currentIndex === 10
visible: active
asynchronous: true
sourceComponent: PluginsTab {
parentModal: root.parentModal
}
}
Loader {
id: aboutLoader
anchors.fill: parent
active: root.currentIndex === 11
visible: active
asynchronous: true
sourceComponent: AboutTab {
}
}
}
}
+195
View File
@@ -0,0 +1,195 @@
import QtQuick
import QtQuick.Effects
import Quickshell.Io
import qs.Common
import qs.Modals.Common
import qs.Modals.FileBrowser
import qs.Modules.Settings
import qs.Services
import qs.Widgets
DankModal {
id: settingsModal
property Component settingsContent
property alias profileBrowser: profileBrowser
signal closingModal()
function show() {
open();
}
function hide() {
close();
}
function toggle() {
if (shouldBeVisible) {
hide();
} else {
show();
}
}
objectName: "settingsModal"
width: 800
height: 800
visible: false
onBackgroundClicked: () => {
return hide();
}
content: settingsContent
IpcHandler {
function open(): string {
settingsModal.show();
return "SETTINGS_OPEN_SUCCESS";
}
function close(): string {
settingsModal.hide();
return "SETTINGS_CLOSE_SUCCESS";
}
function toggle(): string {
settingsModal.toggle();
return "SETTINGS_TOGGLE_SUCCESS";
}
target: "settings"
}
IpcHandler {
function browse(type: string) {
if (type === "wallpaper") {
wallpaperBrowser.allowStacking = false;
wallpaperBrowser.open();
} else if (type === "profile") {
profileBrowser.allowStacking = false;
profileBrowser.open();
}
}
target: "file"
}
FileBrowserModal {
id: profileBrowser
allowStacking: true
parentModal: settingsModal
browserTitle: "Select Profile Image"
browserIcon: "person"
browserType: "profile"
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
onFileSelected: (path) => {
PortalService.setProfileImage(path);
close();
}
onDialogClosed: () => {
allowStacking = true;
}
}
FileBrowserModal {
id: wallpaperBrowser
allowStacking: true
parentModal: settingsModal
browserTitle: "Select Wallpaper"
browserIcon: "wallpaper"
browserType: "wallpaper"
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
onFileSelected: (path) => {
SessionData.setWallpaper(path);
close();
}
onDialogClosed: () => {
allowStacking = true;
}
}
settingsContent: Component {
FocusScope {
anchors.fill: parent
focus: true
Column {
anchors.fill: parent
anchors.leftMargin: Theme.spacingL
anchors.rightMargin: Theme.spacingL
anchors.topMargin: Theme.spacingM
anchors.bottomMargin: Theme.spacingL
spacing: 0
Item {
width: parent.width
height: 35
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "settings"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Settings")
font.pixelSize: Theme.fontSizeXLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
DankActionButton {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
circular: false
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: () => {
return settingsModal.hide();
}
}
}
Row {
width: parent.width
height: parent.height - 35
spacing: 0
SettingsSidebar {
id: sidebar
parentModal: settingsModal
onCurrentIndexChanged: content.currentIndex = currentIndex
}
SettingsContent {
id: content
width: parent.width - sidebar.width
height: parent.height
parentModal: settingsModal
currentIndex: sidebar.currentIndex
}
}
}
}
}
}
+139
View File
@@ -0,0 +1,139 @@
import QtQuick
import qs.Common
import qs.Modals.Settings
import qs.Widgets
Rectangle {
id: sidebarContainer
property int currentIndex: 0
property var parentModal: null
readonly property var sidebarItems: [{
"text": I18n.tr("Personalization"),
"icon": "person"
}, {
"text": I18n.tr("Time & Date"),
"icon": "schedule"
}, {
"text": I18n.tr("Weather"),
"icon": "cloud"
}, {
"text": I18n.tr("Dank Bar"),
"icon": "toolbar"
}, {
"text": I18n.tr("Widgets"),
"icon": "widgets"
}, {
"text": I18n.tr("Dock"),
"icon": "dock_to_bottom"
}, {
"text": I18n.tr("Displays"),
"icon": "monitor"
}, {
"text": I18n.tr("Launcher"),
"icon": "apps"
}, {
"text": I18n.tr("Theme & Colors"),
"icon": "palette"
}, {
"text": I18n.tr("Power"),
"icon": "power_settings_new"
}, {
"text": I18n.tr("Plugins"),
"icon": "extension"
}, {
"text": I18n.tr("About"),
"icon": "info"
}]
width: 270
height: parent.height
color: Theme.surfaceContainer
radius: Theme.cornerRadius
Column {
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.bottomMargin: Theme.spacingS
anchors.topMargin: Theme.spacingM + 2
spacing: Theme.spacingXS
ProfileSection {
parentModal: sidebarContainer.parentModal
}
Rectangle {
width: parent.width - Theme.spacingS * 2
height: 1
color: Theme.outline
opacity: 0.2
}
Item {
width: parent.width
height: Theme.spacingL
}
Repeater {
id: sidebarRepeater
model: sidebarContainer.sidebarItems
Rectangle {
property bool isActive: sidebarContainer.currentIndex === index
width: parent.width - Theme.spacingS * 2
height: 44
radius: Theme.cornerRadius
color: isActive ? Theme.primary : tabMouseArea.containsMouse ? Theme.surfaceHover : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: modelData.icon || ""
size: Theme.iconSize - 2
color: parent.parent.isActive ? Theme.primaryText : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: modelData.text || ""
font.pixelSize: Theme.fontSizeMedium
color: parent.parent.isActive ? Theme.primaryText : Theme.surfaceText
font.weight: parent.parent.isActive ? Font.Medium : Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: tabMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: () => {
sidebarContainer.currentIndex = index;
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
+243
View File
@@ -0,0 +1,243 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Modals.Spotlight
import qs.Modules.AppDrawer
import qs.Services
import qs.Widgets
Item {
id: spotlightKeyHandler
property alias appLauncher: appLauncher
property alias searchField: searchField
property var parentModal: null
function resetScroll() {
resultsView.resetScroll()
}
anchors.fill: parent
focus: true
clip: false
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
if (parentModal)
parentModal.hide()
event.accepted = true
} else if (event.key === Qt.Key_Down) {
appLauncher.selectNext()
event.accepted = true
} else if (event.key === Qt.Key_Up) {
appLauncher.selectPrevious()
event.accepted = true
} else if (event.key === Qt.Key_Right && appLauncher.viewMode === "grid") {
appLauncher.selectNextInRow()
event.accepted = true
} else if (event.key === Qt.Key_Left && appLauncher.viewMode === "grid") {
appLauncher.selectPreviousInRow()
event.accepted = true
} else if (event.key == Qt.Key_J && event.modifiers & Qt.ControlModifier) {
appLauncher.selectNext()
event.accepted = true
} else if (event.key == Qt.Key_K && event.modifiers & Qt.ControlModifier) {
appLauncher.selectPrevious()
event.accepted = true
} else if (event.key == Qt.Key_L && event.modifiers & Qt.ControlModifier && appLauncher.viewMode === "grid") {
appLauncher.selectNextInRow()
event.accepted = true
} else if (event.key == Qt.Key_H && event.modifiers & Qt.ControlModifier && appLauncher.viewMode === "grid") {
appLauncher.selectPreviousInRow()
event.accepted = true
} else if (event.key === Qt.Key_Tab) {
if (appLauncher.viewMode === "grid") {
appLauncher.selectNextInRow()
} else {
appLauncher.selectNext()
}
event.accepted = true
} else if (event.key === Qt.Key_Backtab) {
if (appLauncher.viewMode === "grid") {
appLauncher.selectPreviousInRow()
} else {
appLauncher.selectPrevious()
}
event.accepted = true
} else if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) {
if (appLauncher.viewMode === "grid") {
appLauncher.selectNextInRow()
} else {
appLauncher.selectNext()
}
event.accepted = true
} else if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) {
if (appLauncher.viewMode === "grid") {
appLauncher.selectPreviousInRow()
} else {
appLauncher.selectPrevious()
}
event.accepted = true
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
appLauncher.launchSelected()
event.accepted = true
}
}
AppLauncher {
id: appLauncher
viewMode: SettingsData.spotlightModalViewMode
gridColumns: 4
onAppLaunched: () => {
if (parentModal)
parentModal.hide()
}
onViewModeSelected: mode => {
SettingsData.setSpotlightModalViewMode(mode)
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
clip: false
Row {
width: parent.width
spacing: Theme.spacingM
leftPadding: Theme.spacingS
DankTextField {
id: searchField
width: parent.width - 80 - Theme.spacingL
height: 56
cornerRadius: Theme.cornerRadius
backgroundColor: Theme.surfaceContainerHigh
normalBorderColor: Theme.outlineMedium
focusedBorderColor: Theme.primary
leftIconName: "search"
leftIconSize: Theme.iconSize
leftIconColor: Theme.surfaceVariantText
leftIconFocusedColor: Theme.primary
showClearButton: true
textColor: Theme.surfaceText
font.pixelSize: Theme.fontSizeLarge
enabled: parentModal ? parentModal.spotlightOpen : true
placeholderText: ""
ignoreLeftRightKeys: appLauncher.viewMode !== "list"
keyForwardTargets: [spotlightKeyHandler]
text: appLauncher.searchQuery
onTextEdited: () => {
appLauncher.searchQuery = text
}
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
if (parentModal)
parentModal.hide()
event.accepted = true
} else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length > 0) {
if (appLauncher.keyboardNavigationActive && appLauncher.model.count > 0)
appLauncher.launchSelected()
else if (appLauncher.model.count > 0)
appLauncher.launchApp(appLauncher.model.get(0))
event.accepted = true
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up || event.key === Qt.Key_Left || event.key === Qt.Key_Right || event.key === Qt.Key_Tab || event.key === Qt.Key_Backtab || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) {
event.accepted = false
}
}
}
Row {
spacing: Theme.spacingXS
visible: appLauncher.model.count > 0
anchors.verticalCenter: parent.verticalCenter
Rectangle {
width: 36
height: 36
radius: Theme.cornerRadius
color: appLauncher.viewMode === "list" ? Theme.primaryHover : listViewArea.containsMouse ? Theme.surfaceHover : "transparent"
DankIcon {
anchors.centerIn: parent
name: "view_list"
size: 18
color: appLauncher.viewMode === "list" ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: listViewArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: () => {
appLauncher.setViewMode("list")
}
}
}
Rectangle {
width: 36
height: 36
radius: Theme.cornerRadius
color: appLauncher.viewMode === "grid" ? Theme.primaryHover : gridViewArea.containsMouse ? Theme.surfaceHover : "transparent"
DankIcon {
anchors.centerIn: parent
name: "grid_view"
size: 18
color: appLauncher.viewMode === "grid" ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: gridViewArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: () => {
appLauncher.setViewMode("grid")
}
}
}
}
}
SpotlightResults {
id: resultsView
appLauncher: spotlightKeyHandler.appLauncher
contextMenu: contextMenu
}
}
SpotlightContextMenu {
id: contextMenu
appLauncher: spotlightKeyHandler.appLauncher
parentHandler: spotlightKeyHandler
}
MouseArea {
anchors.fill: parent
visible: contextMenu.visible
z: 999
onClicked: () => {
contextMenu.hide()
}
MouseArea {
// Prevent closing when clicking on the menu itself
x: contextMenu.x
y: contextMenu.y
width: contextMenu.width
height: contextMenu.height
onClicked: () => {}
}
}
}
+337
View File
@@ -0,0 +1,337 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Popup {
id: contextMenu
property var currentApp: null
property var appLauncher: null
property var parentHandler: null
function show(x, y, app) {
currentApp = app
contextMenu.x = x + 4
contextMenu.y = y + 4
contextMenu.open()
}
function hide() {
contextMenu.close()
}
width: Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2)
height: menuColumn.implicitHeight + Theme.spacingS * 2
padding: 0
closePolicy: Popup.CloseOnPressOutside
modal: false
dim: false
background: Rectangle {
radius: Theme.cornerRadius
color: Theme.popupBackground()
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
Rectangle {
anchors.fill: parent
anchors.topMargin: 4
anchors.leftMargin: 2
anchors.rightMargin: -2
anchors.bottomMargin: -4
radius: parent.radius
color: Qt.rgba(0, 0, 0, 0.15)
z: -1
}
}
enter: Transition {
NumberAnimation {
property: "opacity"
from: 0
to: 1
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
exit: Transition {
NumberAnimation {
property: "opacity"
from: 1
to: 0
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
Column {
id: menuColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 1
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadius
color: pinMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: {
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry)
return "push_pin"
const appId = contextMenu.currentApp.desktopEntry.id || contextMenu.currentApp.desktopEntry.execString || ""
return SessionData.isPinnedApp(appId) ? "keep_off" : "push_pin"
}
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: {
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry)
return I18n.tr("Pin to Dock")
const appId = contextMenu.currentApp.desktopEntry.id || contextMenu.currentApp.desktopEntry.execString || ""
return SessionData.isPinnedApp(appId) ? I18n.tr("Unpin from Dock") : I18n.tr("Pin to Dock")
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: pinMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: () => {
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry)
return
const appId = contextMenu.currentApp.desktopEntry.id || contextMenu.currentApp.desktopEntry.execString || ""
if (SessionData.isPinnedApp(appId))
SessionData.removePinnedApp(appId)
else
SessionData.addPinnedApp(appId)
contextMenu.hide()
}
}
}
Rectangle {
width: parent.width - Theme.spacingS * 2
height: 5
anchors.horizontalCenter: parent.horizontalCenter
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
}
Repeater {
model: contextMenu.currentApp && contextMenu.currentApp.desktopEntry && contextMenu.currentApp.desktopEntry.actions ? contextMenu.currentApp.desktopEntry.actions : []
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadius
color: actionMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
Item {
anchors.verticalCenter: parent.verticalCenter
width: Theme.iconSize - 2
height: Theme.iconSize - 2
visible: modelData.icon && modelData.icon !== ""
IconImage {
anchors.fill: parent
source: modelData.icon ? Quickshell.iconPath(modelData.icon, true) : ""
smooth: true
asynchronous: true
visible: status === Image.Ready
}
}
StyledText {
text: modelData.name || ""
font.pixelSize: Theme.fontSizeSmall
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)
}
}
MouseArea {
id: actionMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData && contextMenu.currentApp && contextMenu.currentApp.desktopEntry) {
SessionService.launchDesktopAction(contextMenu.currentApp.desktopEntry, modelData)
if (appLauncher) {
appLauncher.appLaunched(contextMenu.currentApp)
}
}
contextMenu.hide()
}
}
}
}
Rectangle {
visible: contextMenu.currentApp && contextMenu.currentApp.desktopEntry && contextMenu.currentApp.desktopEntry.actions && contextMenu.currentApp.desktopEntry.actions.length > 0
width: parent.width - Theme.spacingS * 2
height: 5
anchors.horizontalCenter: parent.horizontalCenter
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
}
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadius
color: launchMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "launch"
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Launch")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: launchMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: () => {
if (contextMenu.currentApp && appLauncher)
appLauncher.launchApp(contextMenu.currentApp)
contextMenu.hide()
}
}
}
Rectangle {
visible: SessionService.hasPrimeRun
width: parent.width - Theme.spacingS * 2
height: 5
anchors.horizontalCenter: parent.horizontalCenter
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
}
Rectangle {
visible: SessionService.hasPrimeRun
width: parent.width
height: 32
radius: Theme.cornerRadius
color: primeRunMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "memory"
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Launch on dGPU")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: primeRunMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: () => {
if (contextMenu.currentApp && contextMenu.currentApp.desktopEntry) {
SessionService.launchDesktopEntry(contextMenu.currentApp.desktopEntry, true)
if (appLauncher) {
appLauncher.appLaunched(contextMenu.currentApp)
}
}
contextMenu.hide()
}
}
}
}
}
+118
View File
@@ -0,0 +1,118 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Modals.Common
import qs.Modules.AppDrawer
import qs.Services
import qs.Widgets
DankModal {
id: spotlightModal
property bool spotlightOpen: false
property Component spotlightContent
function show() {
spotlightOpen = true
open()
Qt.callLater(() => {
if (contentLoader.item && contentLoader.item.searchField) {
contentLoader.item.searchField.forceActiveFocus()
}
})
}
function hide() {
spotlightOpen = false
close()
}
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 (contentLoader.item.resetScroll) {
contentLoader.item.resetScroll()
}
if (contentLoader.item.searchField) {
contentLoader.item.searchField.text = ""
}
}
}
function toggle() {
if (spotlightOpen) {
hide()
} else {
show()
}
}
shouldBeVisible: spotlightOpen
width: 550
height: 700
backgroundColor: Theme.popupBackground()
cornerRadius: Theme.cornerRadius
borderColor: Theme.outlineMedium
borderWidth: 1
enableShadow: true
keepContentLoaded: true
onVisibleChanged: () => {
if (visible && !spotlightOpen) {
show()
}
if (visible && contentLoader.item) {
Qt.callLater(() => {
if (contentLoader.item.searchField) {
contentLoader.item.searchField.forceActiveFocus()
}
})
}
}
onBackgroundClicked: () => {
return hide()
}
content: spotlightContent
Connections {
function onCloseAllModalsExcept(excludedModal) {
if (excludedModal !== spotlightModal && !allowStacking && spotlightOpen) {
spotlightOpen = false
}
}
target: ModalManager
}
IpcHandler {
function open(): string {
spotlightModal.show()
return "SPOTLIGHT_OPEN_SUCCESS"
}
function close(): string {
spotlightModal.hide()
return "SPOTLIGHT_CLOSE_SUCCESS"
}
function toggle(): string {
spotlightModal.toggle()
return "SPOTLIGHT_TOGGLE_SUCCESS"
}
target: "spotlight"
}
spotlightContent: Component {
SpotlightContent {
parentModal: spotlightModal
}
}
}
+326
View File
@@ -0,0 +1,326 @@
import QtQuick
import Quickshell
import Quickshell.Widgets
import qs.Common
import qs.Widgets
Rectangle {
id: resultsContainer
property var appLauncher: null
property var contextMenu: null
function resetScroll() {
resultsList.contentY = 0
resultsGrid.contentY = 0
}
width: parent.width
height: parent.height - y
radius: Theme.cornerRadius
color: "transparent"
clip: true
DankListView {
id: resultsList
property int itemHeight: 60
property int iconSize: 40
property bool showDescription: true
property int itemSpacing: Theme.spacingS
property bool hoverUpdatesSelection: false
property bool keyboardNavigationActive: appLauncher ? appLauncher.keyboardNavigationActive : false
signal keyboardNavigationReset
signal itemClicked(int index, var modelData)
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
function ensureVisible(index) {
if (index < 0 || index >= count)
return
const itemY = index * (itemHeight + itemSpacing)
const itemBottom = itemY + itemHeight
if (itemY < contentY)
contentY = itemY
else if (itemBottom > contentY + height)
contentY = itemBottom - height
}
anchors.fill: parent
anchors.margins: Theme.spacingS
visible: appLauncher && appLauncher.viewMode === "list"
model: appLauncher ? appLauncher.model : null
currentIndex: appLauncher ? appLauncher.selectedIndex : -1
clip: true
spacing: itemSpacing
focus: true
interactive: true
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
reuseItems: true
onCurrentIndexChanged: {
if (keyboardNavigationActive)
ensureVisible(currentIndex)
}
onItemClicked: (index, modelData) => {
if (appLauncher)
appLauncher.launchApp(modelData)
}
onItemRightClicked: (index, modelData, mouseX, mouseY) => {
if (contextMenu)
contextMenu.show(mouseX, mouseY, modelData)
}
onKeyboardNavigationReset: () => {
if (appLauncher)
appLauncher.keyboardNavigationActive = false
}
delegate: Rectangle {
width: ListView.view.width
height: resultsList.itemHeight
radius: Theme.cornerRadius
color: ListView.isCurrentItem ? Theme.primaryPressed : listMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceContainerHigh
Row {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingL
Item {
width: resultsList.iconSize
height: resultsList.iconSize
anchors.verticalCenter: parent.verticalCenter
IconImage {
id: listIconImg
anchors.fill: parent
source: Quickshell.iconPath(model.icon, true)
asynchronous: true
visible: status === Image.Ready
}
Rectangle {
anchors.fill: parent
visible: !listIconImg.visible
color: Theme.surfaceLight
radius: Theme.cornerRadius
border.width: 1
border.color: Theme.primarySelected
StyledText {
anchors.centerIn: parent
text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
font.pixelSize: resultsList.iconSize * 0.4
color: Theme.primary
font.weight: Font.Bold
}
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - resultsList.iconSize - Theme.spacingL
spacing: Theme.spacingXS
StyledText {
width: parent.width
text: model.name || ""
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
}
StyledText {
width: parent.width
text: model.comment || "Application"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
elide: Text.ElideRight
maximumLineCount: 1
visible: resultsList.showDescription && model.comment && model.comment.length > 0
}
}
}
MouseArea {
id: listMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
z: 10
onEntered: () => {
if (resultsList.hoverUpdatesSelection && !resultsList.keyboardNavigationActive)
resultsList.currentIndex = index
}
onPositionChanged: () => {
resultsList.keyboardNavigationReset()
}
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
resultsList.itemClicked(index, model)
} else if (mouse.button === Qt.RightButton) {
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)
}
}
}
}
}
DankGridView {
id: resultsGrid
property int currentIndex: appLauncher ? appLauncher.selectedIndex : -1
property int columns: 4
property bool adaptiveColumns: false
property int minCellWidth: 120
property int maxCellWidth: 160
property int cellPadding: 8
property real iconSizeRatio: 0.55
property int maxIconSize: 48
property int minIconSize: 32
property bool hoverUpdatesSelection: false
property bool keyboardNavigationActive: appLauncher ? appLauncher.keyboardNavigationActive : false
property int baseCellWidth: adaptiveColumns ? Math.max(minCellWidth, Math.min(maxCellWidth, width / columns)) : (width - Theme.spacingS * 2) / columns
property int baseCellHeight: baseCellWidth + 20
property int actualColumns: adaptiveColumns ? Math.floor(width / cellWidth) : columns
property int remainingSpace: width - (actualColumns * cellWidth)
signal keyboardNavigationReset
signal itemClicked(int index, var modelData)
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
function ensureVisible(index) {
if (index < 0 || index >= count)
return
const itemY = Math.floor(index / actualColumns) * cellHeight
const itemBottom = itemY + cellHeight
if (itemY < contentY)
contentY = itemY
else if (itemBottom > contentY + height)
contentY = itemBottom - height
}
anchors.fill: parent
anchors.margins: Theme.spacingS
visible: appLauncher && appLauncher.viewMode === "grid"
model: appLauncher ? appLauncher.model : null
clip: true
cellWidth: baseCellWidth
cellHeight: baseCellHeight
leftMargin: Math.max(Theme.spacingS, remainingSpace / 2)
rightMargin: leftMargin
focus: true
interactive: true
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
reuseItems: true
onCurrentIndexChanged: {
if (keyboardNavigationActive)
ensureVisible(currentIndex)
}
onItemClicked: (index, modelData) => {
if (appLauncher)
appLauncher.launchApp(modelData)
}
onItemRightClicked: (index, modelData, mouseX, mouseY) => {
if (contextMenu)
contextMenu.show(mouseX, mouseY, modelData)
}
onKeyboardNavigationReset: () => {
if (appLauncher)
appLauncher.keyboardNavigationActive = false
}
delegate: Rectangle {
width: resultsGrid.cellWidth - resultsGrid.cellPadding
height: resultsGrid.cellHeight - resultsGrid.cellPadding
radius: Theme.cornerRadius
color: resultsGrid.currentIndex === index ? Theme.primaryPressed : gridMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceContainerHigh
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
Item {
property int iconSize: Math.min(resultsGrid.maxIconSize, Math.max(resultsGrid.minIconSize, resultsGrid.cellWidth * resultsGrid.iconSizeRatio))
width: iconSize
height: iconSize
anchors.horizontalCenter: parent.horizontalCenter
IconImage {
id: gridIconImg
anchors.fill: parent
source: Quickshell.iconPath(model.icon, true)
smooth: true
asynchronous: true
visible: status === Image.Ready
}
Rectangle {
anchors.fill: parent
visible: !gridIconImg.visible
color: Theme.surfaceLight
radius: Theme.cornerRadius
border.width: 1
border.color: Theme.primarySelected
StyledText {
anchors.centerIn: parent
text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
font.pixelSize: Math.min(28, parent.width * 0.5)
color: Theme.primary
font.weight: Font.Bold
}
}
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
width: resultsGrid.cellWidth - 12
text: model.name || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
maximumLineCount: 2
wrapMode: Text.WordWrap
}
}
MouseArea {
id: gridMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
z: 10
onEntered: () => {
if (resultsGrid.hoverUpdatesSelection && !resultsGrid.keyboardNavigationActive)
resultsGrid.currentIndex = index
}
onPositionChanged: () => {
resultsGrid.keyboardNavigationReset()
}
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
resultsGrid.itemClicked(index, model)
} else if (mouse.button === Qt.RightButton) {
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)
}
}
}
}
}
}
+368
View File
@@ -0,0 +1,368 @@
import QtQuick
import qs.Common
import qs.Modals.Common
import qs.Services
import qs.Widgets
DankModal {
id: root
property string wifiPasswordSSID: ""
property string wifiPasswordInput: ""
property string wifiUsernameInput: ""
property bool requiresEnterprise: false
function show(ssid) {
wifiPasswordSSID = ssid
wifiPasswordInput = ""
wifiUsernameInput = ""
const network = NetworkService.wifiNetworks.find(n => n.ssid === ssid)
requiresEnterprise = network?.enterprise || false
open()
Qt.callLater(() => {
if (contentLoader.item) {
if (requiresEnterprise && contentLoader.item.usernameInput) {
contentLoader.item.usernameInput.forceActiveFocus()
} else if (contentLoader.item.passwordInput) {
contentLoader.item.passwordInput.forceActiveFocus()
}
}
})
}
shouldBeVisible: false
width: 420
height: requiresEnterprise ? 310 : 230
onShouldBeVisibleChanged: () => {
if (!shouldBeVisible) {
wifiPasswordInput = ""
wifiUsernameInput = ""
}
}
onOpened: {
Qt.callLater(() => {
if (contentLoader.item) {
if (requiresEnterprise && contentLoader.item.usernameInput) {
contentLoader.item.usernameInput.forceActiveFocus()
} else if (contentLoader.item.passwordInput) {
contentLoader.item.passwordInput.forceActiveFocus()
}
}
})
}
onBackgroundClicked: () => {
close()
wifiPasswordInput = ""
wifiUsernameInput = ""
}
Connections {
target: NetworkService
function onPasswordDialogShouldReopenChanged() {
if (NetworkService.passwordDialogShouldReopen && NetworkService.connectingSSID !== "") {
wifiPasswordSSID = NetworkService.connectingSSID
wifiPasswordInput = ""
open()
NetworkService.passwordDialogShouldReopen = false
}
}
}
content: Component {
FocusScope {
id: wifiContent
property alias usernameInput: usernameInput
property alias passwordInput: passwordInput
anchors.fill: parent
focus: true
Keys.onEscapePressed: event => {
close()
wifiPasswordInput = ""
wifiUsernameInput = ""
event.accepted = true
}
Column {
anchors.centerIn: parent
width: parent.width - Theme.spacingM * 2
spacing: Theme.spacingM
Row {
width: parent.width
Column {
width: parent.width - 40
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Connect to Wi-Fi")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: requiresEnterprise ? `Enter credentials for "${wifiPasswordSSID}"` : `Enter password for "${wifiPasswordSSID}"`
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium
width: parent.width
elide: Text.ElideRight
}
}
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: () => {
close()
wifiPasswordInput = ""
wifiUsernameInput = ""
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: Theme.surfaceHover
border.color: usernameInput.activeFocus ? Theme.primary : Theme.outlineStrong
border.width: usernameInput.activeFocus ? 2 : 1
visible: requiresEnterprise
MouseArea {
anchors.fill: parent
onClicked: () => {
usernameInput.forceActiveFocus()
}
}
DankTextField {
id: usernameInput
anchors.fill: parent
font.pixelSize: Theme.fontSizeMedium
textColor: Theme.surfaceText
text: wifiUsernameInput
placeholderText: "Username"
backgroundColor: "transparent"
enabled: root.shouldBeVisible
onTextEdited: () => {
wifiUsernameInput = text
}
onAccepted: () => {
if (passwordInput) {
passwordInput.forceActiveFocus()
}
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: Theme.surfaceHover
border.color: passwordInput.activeFocus ? Theme.primary : Theme.outlineStrong
border.width: passwordInput.activeFocus ? 2 : 1
MouseArea {
anchors.fill: parent
onClicked: () => {
passwordInput.forceActiveFocus()
}
}
DankTextField {
id: passwordInput
anchors.fill: parent
font.pixelSize: Theme.fontSizeMedium
textColor: Theme.surfaceText
text: wifiPasswordInput
echoMode: showPasswordCheckbox.checked ? TextInput.Normal : TextInput.Password
placeholderText: requiresEnterprise ? "Password" : ""
backgroundColor: "transparent"
focus: !requiresEnterprise
enabled: root.shouldBeVisible
onTextEdited: () => {
wifiPasswordInput = text
}
onAccepted: () => {
const username = requiresEnterprise ? usernameInput.text : ""
NetworkService.connectToWifi(wifiPasswordSSID, passwordInput.text, username)
close()
wifiPasswordInput = ""
wifiUsernameInput = ""
passwordInput.text = ""
if (requiresEnterprise) usernameInput.text = ""
}
Component.onCompleted: () => {
if (root.shouldBeVisible && !requiresEnterprise)
focusDelayTimer.start()
}
Timer {
id: focusDelayTimer
interval: 100
repeat: false
onTriggered: () => {
if (root.shouldBeVisible) {
if (requiresEnterprise && usernameInput) {
usernameInput.forceActiveFocus()
} else {
passwordInput.forceActiveFocus()
}
}
}
}
Connections {
target: root
function onShouldBeVisibleChanged() {
if (root.shouldBeVisible)
focusDelayTimer.start()
}
}
}
}
Row {
spacing: Theme.spacingS
Rectangle {
id: showPasswordCheckbox
property bool checked: false
width: 20
height: 20
radius: 4
color: checked ? Theme.primary : "transparent"
border.color: checked ? Theme.primary : Theme.outlineButton
border.width: 2
DankIcon {
anchors.centerIn: parent
name: "check"
size: 12
color: Theme.background
visible: parent.checked
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: () => {
showPasswordCheckbox.checked = !showPasswordCheckbox.checked
}
}
}
StyledText {
text: I18n.tr("Show password")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Item {
width: parent.width
height: 40
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Rectangle {
width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2)
height: 36
radius: Theme.cornerRadius
color: cancelArea.containsMouse ? Theme.surfaceTextHover : "transparent"
border.color: Theme.surfaceVariantAlpha
border.width: 1
StyledText {
id: cancelText
anchors.centerIn: parent
text: I18n.tr("Cancel")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
MouseArea {
id: cancelArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: () => {
close()
wifiPasswordInput = ""
wifiUsernameInput = ""
}
}
}
Rectangle {
width: Math.max(80, connectText.contentWidth + Theme.spacingM * 2)
height: 36
radius: Theme.cornerRadius
color: connectArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
enabled: requiresEnterprise ? (usernameInput.text.length > 0 && passwordInput.text.length > 0) : passwordInput.text.length > 0
opacity: enabled ? 1 : 0.5
StyledText {
id: connectText
anchors.centerIn: parent
text: I18n.tr("Connect")
font.pixelSize: Theme.fontSizeMedium
color: Theme.background
font.weight: Font.Medium
}
MouseArea {
id: connectArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: parent.enabled
onClicked: () => {
const username = requiresEnterprise ? usernameInput.text : ""
NetworkService.connectToWifi(wifiPasswordSSID, passwordInput.text, username)
close()
wifiPasswordInput = ""
wifiUsernameInput = ""
passwordInput.text = ""
if (requiresEnterprise) usernameInput.text = ""
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
}
}
}
+992
View File
@@ -0,0 +1,992 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Modules.AppDrawer
import qs.Services
import qs.Widgets
DankPopout {
id: appDrawerPopout
property var triggerScreen: null
// Setting to Exclusive, so virtual keyboards can send input to app drawer
WlrLayershell.keyboardFocus: shouldBeVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
function show() {
open()
}
function setTriggerPosition(x, y, width, section, screen) {
triggerX = x
triggerY = y
triggerWidth = width
triggerSection = section
triggerScreen = screen
}
popupWidth: 520
popupHeight: 600
triggerX: Theme.spacingL
triggerY: Math.max(26 + SettingsData.dankBarInnerPadding + 4, Theme.barHeight - 4 - (8 - SettingsData.dankBarInnerPadding)) + SettingsData.dankBarSpacing + SettingsData.dankBarBottomGap - 2
triggerWidth: 40
positioning: ""
screen: triggerScreen
onShouldBeVisibleChanged: {
if (shouldBeVisible) {
appLauncher.searchQuery = ""
appLauncher.selectedIndex = 0
appLauncher.setCategory(I18n.tr("All"))
Qt.callLater(() => {
if (contentLoader.item && contentLoader.item.searchField) {
contentLoader.item.searchField.text = ""
contentLoader.item.searchField.forceActiveFocus()
}
})
}
}
AppLauncher {
id: appLauncher
viewMode: SettingsData.appLauncherViewMode
gridColumns: 4
onAppLaunched: appDrawerPopout.close()
onViewModeSelected: function (mode) {
SettingsData.setAppLauncherViewMode(mode)
}
}
content: Component {
Rectangle {
id: launcherPanel
property alias searchField: searchField
color: Theme.popupBackground()
radius: Theme.cornerRadius
antialiasing: true
smooth: true
// Multi-layer border effect
Repeater {
model: [{
"margin": -3,
"color": Qt.rgba(0, 0, 0, 0.05),
"z": -3
}, {
"margin": -2,
"color": Qt.rgba(0, 0, 0, 0.08),
"z": -2
}, {
"margin": 0,
"color": Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12),
"z": -1
}]
Rectangle {
anchors.fill: parent
anchors.margins: modelData.margin
color: "transparent"
radius: parent.radius + Math.abs(modelData.margin)
border.color: modelData.color
border.width: 0
z: modelData.z
}
}
Item {
id: keyHandler
anchors.fill: parent
focus: true
readonly property var keyMappings: {
const mappings = {}
mappings[Qt.Key_Escape] = () => appDrawerPopout.close()
mappings[Qt.Key_Down] = () => appLauncher.selectNext()
mappings[Qt.Key_Up] = () => appLauncher.selectPrevious()
mappings[Qt.Key_Return] = () => appLauncher.launchSelected()
mappings[Qt.Key_Enter] = () => appLauncher.launchSelected()
if (appLauncher.viewMode === "grid") {
mappings[Qt.Key_Right] = () => appLauncher.selectNextInRow()
mappings[Qt.Key_Left] = () => appLauncher.selectPreviousInRow()
}
return mappings
}
Keys.onPressed: function (event) {
if (keyMappings[event.key]) {
keyMappings[event.key]()
event.accepted = true
return
}
if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) {
appLauncher.selectNext()
event.accepted = true
return
}
if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) {
appLauncher.selectPrevious()
event.accepted = true
return
}
if (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) {
appLauncher.selectNext()
event.accepted = true
return
}
if (event.key === Qt.Key_K && event.modifiers & Qt.ControlModifier) {
appLauncher.selectPrevious()
event.accepted = true
return
}
if (appLauncher.viewMode === "grid") {
if (event.key === Qt.Key_L && event.modifiers & Qt.ControlModifier) {
appLauncher.selectNextInRow()
event.accepted = true
return
}
if (event.key === Qt.Key_H && event.modifiers & Qt.ControlModifier) {
appLauncher.selectPreviousInRow()
event.accepted = true
return
}
}
}
Column {
width: parent.width - Theme.spacingS * 2
height: parent.height - Theme.spacingS * 2
x: Theme.spacingS
y: Theme.spacingS
spacing: Theme.spacingS
Item {
width: parent.width
height: 40
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("Applications")
font.pixelSize: Theme.fontSizeLarge + 4
font.weight: Font.Bold
color: Theme.surfaceText
}
StyledText {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: appLauncher.model.count + " apps"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
}
}
DankTextField {
id: searchField
width: parent.width - Theme.spacingS * 2
anchors.horizontalCenter: parent.horizontalCenter
height: 52
cornerRadius: Theme.cornerRadius
backgroundColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.7)
normalBorderColor: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
focusedBorderColor: Theme.primary
leftIconName: "search"
leftIconSize: Theme.iconSize
leftIconColor: Theme.surfaceVariantText
leftIconFocusedColor: Theme.primary
showClearButton: true
font.pixelSize: Theme.fontSizeLarge
enabled: appDrawerPopout.shouldBeVisible
ignoreLeftRightKeys: appLauncher.viewMode !== "list"
keyForwardTargets: [keyHandler]
onTextEdited: {
appLauncher.searchQuery = text
}
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Escape) {
appDrawerPopout.close()
event.accepted = true
return
}
const isEnterKey = [Qt.Key_Return, Qt.Key_Enter].includes(event.key)
const hasText = text.length > 0
if (isEnterKey && hasText) {
if (appLauncher.keyboardNavigationActive && appLauncher.model.count > 0) {
appLauncher.launchSelected()
} else if (appLauncher.model.count > 0) {
appLauncher.launchApp(appLauncher.model.get(0))
}
event.accepted = true
return
}
const navigationKeys = [Qt.Key_Down, Qt.Key_Up, Qt.Key_Left, Qt.Key_Right]
const isNavigationKey = navigationKeys.includes(event.key)
const isEmptyEnter = isEnterKey && !hasText
event.accepted = !(isNavigationKey || isEmptyEnter)
}
Connections {
function onShouldBeVisibleChanged() {
if (!appDrawerPopout.shouldBeVisible) {
searchField.focus = false
}
}
target: appDrawerPopout
}
}
Row {
width: parent.width
height: 40
spacing: Theme.spacingM
visible: searchField.text.length === 0
leftPadding: Theme.spacingS
Rectangle {
width: 180
height: 40
radius: Theme.cornerRadius
color: "transparent"
DankDropdown {
anchors.fill: parent
text: ""
dropdownWidth: 180
currentValue: appLauncher.selectedCategory
options: appLauncher.categories
optionIcons: appLauncher.categoryIcons
onValueChanged: function (value) {
appLauncher.setCategory(value)
}
}
}
Item {
width: parent.width - 290
height: 1
}
Row {
spacing: 4
anchors.verticalCenter: parent.verticalCenter
DankActionButton {
buttonSize: 36
circular: false
iconName: "view_list"
iconSize: 20
iconColor: appLauncher.viewMode === "list" ? Theme.primary : Theme.surfaceText
backgroundColor: appLauncher.viewMode === "list" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
onClicked: {
appLauncher.setViewMode("list")
}
}
DankActionButton {
buttonSize: 36
circular: false
iconName: "grid_view"
iconSize: 20
iconColor: appLauncher.viewMode === "grid" ? Theme.primary : Theme.surfaceText
backgroundColor: appLauncher.viewMode === "grid" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
onClicked: {
appLauncher.setViewMode("grid")
}
}
}
}
Rectangle {
width: parent.width
height: {
let usedHeight = 40 + Theme.spacingS
usedHeight += 52 + Theme.spacingS
usedHeight += (searchField.text.length === 0 ? 40 : 0)
return parent.height - usedHeight
}
radius: Theme.cornerRadius
color: "transparent"
DankListView {
id: appList
property int itemHeight: 72
property int iconSize: 56
property bool showDescription: true
property int itemSpacing: Theme.spacingS
property bool hoverUpdatesSelection: false
property bool keyboardNavigationActive: appLauncher.keyboardNavigationActive
signal keyboardNavigationReset
signal itemClicked(int index, var modelData)
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
function ensureVisible(index) {
if (index < 0 || index >= count)
return
var itemY = index * (itemHeight + itemSpacing)
var itemBottom = itemY + itemHeight
if (itemY < contentY)
contentY = itemY
else if (itemBottom > contentY + height)
contentY = itemBottom - height
}
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.bottomMargin: Theme.spacingS
visible: appLauncher.viewMode === "list"
model: appLauncher.model
currentIndex: appLauncher.selectedIndex
clip: true
spacing: itemSpacing
focus: true
interactive: true
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
reuseItems: true
onCurrentIndexChanged: {
if (keyboardNavigationActive)
ensureVisible(currentIndex)
}
onItemClicked: function (index, modelData) {
appLauncher.launchApp(modelData)
}
onItemRightClicked: function (index, modelData, mouseX, mouseY) {
contextMenu.show(mouseX, mouseY, modelData)
}
onKeyboardNavigationReset: {
appLauncher.keyboardNavigationActive = false
}
delegate: Rectangle {
width: ListView.view.width
height: appList.itemHeight
radius: Theme.cornerRadius
color: ListView.isCurrentItem ? Theme.primaryPressed : listMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceContainerHigh
Row {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingL
Item {
width: appList.iconSize
height: appList.iconSize
anchors.verticalCenter: parent.verticalCenter
IconImage {
id: listIconImg
anchors.fill: parent
anchors.margins: Theme.spacingXS
source: Quickshell.iconPath(model.icon, true)
smooth: true
asynchronous: true
visible: status === Image.Ready
}
Rectangle {
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.bottomMargin: Theme.spacingM
visible: !listIconImg.visible
color: Theme.surfaceLight
radius: Theme.cornerRadius
border.width: 0
border.color: Theme.primarySelected
StyledText {
anchors.centerIn: parent
text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
font.pixelSize: appList.iconSize * 0.4
color: Theme.primary
font.weight: Font.Bold
}
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - appList.iconSize - Theme.spacingL
spacing: Theme.spacingXS
StyledText {
width: parent.width
text: model.name || ""
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
}
StyledText {
width: parent.width
text: model.comment || "Application"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
elide: Text.ElideRight
maximumLineCount: 1
visible: appList.showDescription && model.comment && model.comment.length > 0
}
}
}
MouseArea {
id: listMouseArea
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.bottomMargin: Theme.spacingM
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
z: 10
onEntered: {
if (appList.hoverUpdatesSelection && !appList.keyboardNavigationActive)
appList.currentIndex = index
}
onPositionChanged: {
appList.keyboardNavigationReset()
}
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
appList.itemClicked(index, model)
} else if (mouse.button === Qt.RightButton) {
var globalPos = mapToItem(null, mouse.x, mouse.y)
var panelPos = contextMenu.parent.mapFromItem(null, globalPos.x, globalPos.y)
appList.itemRightClicked(index, model, panelPos.x, panelPos.y)
}
}
}
}
}
DankGridView {
id: appGrid
property int currentIndex: appLauncher.selectedIndex
property int columns: 4
property bool adaptiveColumns: false
property int minCellWidth: 120
property int maxCellWidth: 160
property int cellPadding: 8
property real iconSizeRatio: 0.6
property int maxIconSize: 56
property int minIconSize: 32
property bool hoverUpdatesSelection: false
property bool keyboardNavigationActive: appLauncher.keyboardNavigationActive
property int baseCellWidth: adaptiveColumns ? Math.max(minCellWidth, Math.min(maxCellWidth, width / columns)) : (width - Theme.spacingS * 2) / columns
property int baseCellHeight: baseCellWidth + 20
property int actualColumns: adaptiveColumns ? Math.floor(width / cellWidth) : columns
property int remainingSpace: width - (actualColumns * cellWidth)
signal keyboardNavigationReset
signal itemClicked(int index, var modelData)
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
function ensureVisible(index) {
if (index < 0 || index >= count)
return
var itemY = Math.floor(index / actualColumns) * cellHeight
var itemBottom = itemY + cellHeight
if (itemY < contentY)
contentY = itemY
else if (itemBottom > contentY + height)
contentY = itemBottom - height
}
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.bottomMargin: Theme.spacingS
visible: appLauncher.viewMode === "grid"
model: appLauncher.model
clip: true
cellWidth: baseCellWidth
cellHeight: baseCellHeight
leftMargin: Math.max(Theme.spacingS, remainingSpace / 2)
rightMargin: leftMargin
focus: true
interactive: true
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
reuseItems: true
onCurrentIndexChanged: {
if (keyboardNavigationActive)
ensureVisible(currentIndex)
}
onItemClicked: function (index, modelData) {
appLauncher.launchApp(modelData)
}
onItemRightClicked: function (index, modelData, mouseX, mouseY) {
contextMenu.show(mouseX, mouseY, modelData)
}
onKeyboardNavigationReset: {
appLauncher.keyboardNavigationActive = false
}
delegate: Rectangle {
width: appGrid.cellWidth - appGrid.cellPadding
height: appGrid.cellHeight - appGrid.cellPadding
radius: Theme.cornerRadius
color: appGrid.currentIndex === index ? Theme.primaryPressed : gridMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceContainerHigh
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
Item {
property int iconSize: Math.min(appGrid.maxIconSize, Math.max(appGrid.minIconSize, appGrid.cellWidth * appGrid.iconSizeRatio))
width: iconSize
height: iconSize
anchors.horizontalCenter: parent.horizontalCenter
IconImage {
id: gridIconImg
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.bottomMargin: Theme.spacingS
source: Quickshell.iconPath(model.icon, true)
smooth: true
asynchronous: true
visible: status === Image.Ready
}
Rectangle {
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.bottomMargin: Theme.spacingS
visible: !gridIconImg.visible
color: Theme.surfaceLight
radius: Theme.cornerRadius
border.width: 0
border.color: Theme.primarySelected
StyledText {
anchors.centerIn: parent
text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
font.pixelSize: Math.min(28, parent.width * 0.5)
color: Theme.primary
font.weight: Font.Bold
}
}
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
width: appGrid.cellWidth - 12
text: model.name || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
maximumLineCount: 2
wrapMode: Text.WordWrap
}
}
MouseArea {
id: gridMouseArea
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.bottomMargin: Theme.spacingS
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
z: 10
onEntered: {
if (appGrid.hoverUpdatesSelection && !appGrid.keyboardNavigationActive)
appGrid.currentIndex = index
}
onPositionChanged: {
appGrid.keyboardNavigationReset()
}
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
appGrid.itemClicked(index, model)
} else if (mouse.button === Qt.RightButton) {
var globalPos = mapToItem(null, mouse.x, mouse.y)
var panelPos = contextMenu.parent.mapFromItem(null, globalPos.x, globalPos.y)
appGrid.itemRightClicked(index, model, panelPos.x, panelPos.y)
}
}
}
}
}
}
}
}
}
}
Popup {
id: contextMenu
property var currentApp: null
readonly property string appId: (currentApp && currentApp.desktopEntry) ? (currentApp.desktopEntry.id || currentApp.desktopEntry.execString || "") : ""
readonly property bool isPinned: appId && SessionData.isPinnedApp(appId)
function show(x, y, app) {
currentApp = app
contextMenu.x = x + 4
contextMenu.y = y + 4
contextMenu.open()
}
function hide() {
contextMenu.close()
}
width: Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2)
height: menuColumn.implicitHeight + Theme.spacingS * 2
padding: 0
closePolicy: Popup.CloseOnPressOutside
modal: false
dim: false
background: Rectangle {
radius: Theme.cornerRadius
color: Theme.popupBackground()
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
Rectangle {
anchors.fill: parent
anchors.topMargin: 4
anchors.leftMargin: 2
anchors.rightMargin: -2
anchors.bottomMargin: -4
radius: parent.radius
color: Qt.rgba(0, 0, 0, 0.15)
z: -1
}
}
enter: Transition {
NumberAnimation {
property: "opacity"
from: 0
to: 1
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
exit: Transition {
NumberAnimation {
property: "opacity"
from: 1
to: 0
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
Column {
id: menuColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 1
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadius
color: pinMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: contextMenu.isPinned ? "keep_off" : "push_pin"
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: contextMenu.isPinned ? I18n.tr("Unpin from Dock") : I18n.tr("Pin to Dock")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: pinMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry) {
return
}
if (contextMenu.isPinned) {
SessionData.removePinnedApp(contextMenu.appId)
} else {
SessionData.addPinnedApp(contextMenu.appId)
}
contextMenu.hide()
}
}
}
Rectangle {
width: parent.width - Theme.spacingS * 2
height: 5
anchors.horizontalCenter: parent.horizontalCenter
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
}
Repeater {
model: contextMenu.currentApp && contextMenu.currentApp.desktopEntry && contextMenu.currentApp.desktopEntry.actions ? contextMenu.currentApp.desktopEntry.actions : []
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadius
color: actionMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
Item {
anchors.verticalCenter: parent.verticalCenter
width: Theme.iconSize - 2
height: Theme.iconSize - 2
visible: modelData.icon && modelData.icon !== ""
IconImage {
anchors.fill: parent
source: modelData.icon ? Quickshell.iconPath(modelData.icon, true) : ""
smooth: true
asynchronous: true
visible: status === Image.Ready
}
}
StyledText {
text: modelData.name || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: actionMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData && contextMenu.currentApp && contextMenu.currentApp.desktopEntry) {
SessionService.launchDesktopAction(contextMenu.currentApp.desktopEntry, modelData)
appLauncher.appLaunched(contextMenu.currentApp)
}
contextMenu.hide()
}
}
}
}
Rectangle {
visible: contextMenu.currentApp && contextMenu.currentApp.desktopEntry && contextMenu.currentApp.desktopEntry.actions && contextMenu.currentApp.desktopEntry.actions.length > 0
width: parent.width - Theme.spacingS * 2
height: 5
anchors.horizontalCenter: parent.horizontalCenter
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
}
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadius
color: launchMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "launch"
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Launch")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: launchMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (contextMenu.currentApp)
appLauncher.launchApp(contextMenu.currentApp)
contextMenu.hide()
}
}
}
Rectangle {
visible: SessionService.hasPrimeRun
width: parent.width - Theme.spacingS * 2
height: 5
anchors.horizontalCenter: parent.horizontalCenter
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
}
Rectangle {
visible: SessionService.hasPrimeRun
width: parent.width
height: 32
radius: Theme.cornerRadius
color: primeRunMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "memory"
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Launch on dGPU")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: primeRunMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (contextMenu.currentApp && contextMenu.currentApp.desktopEntry) {
SessionService.launchDesktopEntry(contextMenu.currentApp.desktopEntry, true)
appLauncher.appLaunched(contextMenu.currentApp)
}
contextMenu.hide()
}
}
}
}
}
MouseArea {
anchors.fill: parent
visible: contextMenu.visible
z: 999
onClicked: {
contextMenu.hide()
}
MouseArea {
x: contextMenu.x
y: contextMenu.y
width: contextMenu.width
height: contextMenu.height
onClicked: {
// Prevent closing when clicking on the menu itself
}
}
}
}
+175
View File
@@ -0,0 +1,175 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property string searchQuery: ""
property string selectedCategory: I18n.tr("All")
property string viewMode: "list" // "list" or "grid"
property int selectedIndex: 0
property int maxResults: 50
property int gridColumns: 4
property bool debounceSearch: true
property int debounceInterval: 50
property bool keyboardNavigationActive: false
property bool suppressUpdatesWhileLaunching: false
readonly 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")))
}
readonly property var categoryIcons: categories.map(category => AppSearchService.getCategoryIcon(category))
property var appUsageRanking: AppUsageHistoryData.appUsageRanking || {}
property alias model: filteredModel
property var _watchApplications: AppSearchService.applications
signal appLaunched(var app)
signal categorySelected(string category)
signal viewModeSelected(string mode)
function updateFilteredModel() {
if (suppressUpdatesWhileLaunching) {
suppressUpdatesWhileLaunching = false
return
}
filteredModel.clear()
selectedIndex = 0
keyboardNavigationActive = false
let apps = []
const allCategory = I18n.tr("All")
if (searchQuery.length === 0) {
apps = selectedCategory === allCategory ? AppSearchService.getAppsInCategory(allCategory) : AppSearchService.getAppsInCategory(selectedCategory).slice(0, maxResults)
} 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)
} 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 || "")
})
}
apps.forEach(app => {
if (app) {
filteredModel.append({
"name": app.name || "",
"exec": app.execString || "",
"icon": app.icon || "application-x-executable",
"comment": app.comment || "",
"categories": app.categories || [],
"desktopEntry": app
})
}
})
}
function selectNext() {
if (filteredModel.count === 0) {
return
}
keyboardNavigationActive = true
selectedIndex = viewMode === "grid" ? Math.min(selectedIndex + gridColumns, filteredModel.count - 1) : Math.min(selectedIndex + 1, filteredModel.count - 1)
}
function selectPrevious() {
if (filteredModel.count === 0) {
return
}
keyboardNavigationActive = true
selectedIndex = viewMode === "grid" ? Math.max(selectedIndex - gridColumns, 0) : Math.max(selectedIndex - 1, 0)
}
function selectNextInRow() {
if (filteredModel.count === 0 || viewMode !== "grid") {
return
}
keyboardNavigationActive = true
selectedIndex = Math.min(selectedIndex + 1, filteredModel.count - 1)
}
function selectPreviousInRow() {
if (filteredModel.count === 0 || viewMode !== "grid") {
return
}
keyboardNavigationActive = true
selectedIndex = Math.max(selectedIndex - 1, 0)
}
function launchSelected() {
if (filteredModel.count === 0 || selectedIndex < 0 || selectedIndex >= filteredModel.count) {
return
}
const selectedApp = filteredModel.get(selectedIndex)
launchApp(selectedApp)
}
function launchApp(appData) {
if (!appData) {
return
}
suppressUpdatesWhileLaunching = true
SessionService.launchDesktopEntry(appData.desktopEntry)
appLaunched(appData)
AppUsageHistoryData.addAppUsage(appData.desktopEntry)
}
function setCategory(category) {
selectedCategory = category
categorySelected(category)
}
function setViewMode(mode) {
viewMode = mode
viewModeSelected(mode)
}
onSearchQueryChanged: {
if (debounceSearch) {
searchDebounceTimer.restart()
} else {
updateFilteredModel()
}
}
onSelectedCategoryChanged: updateFilteredModel()
onAppUsageRankingChanged: updateFilteredModel()
on_WatchApplicationsChanged: updateFilteredModel()
Component.onCompleted: {
updateFilteredModel()
}
ListModel {
id: filteredModel
}
Timer {
id: searchDebounceTimer
interval: root.debounceInterval
repeat: false
onTriggered: updateFilteredModel()
}
}
+142
View File
@@ -0,0 +1,142 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property var categories: []
property string selectedCategory: I18n.tr("All")
property bool compact: false
signal categorySelected(string category)
readonly property int maxCompactItems: 8
readonly property int itemHeight: 36
readonly property color selectedBorderColor: "transparent"
readonly property color unselectedBorderColor: "transparent"
function handleCategoryClick(category) {
categorySelected(category)
}
function getButtonWidth(itemCount, containerWidth) {
return itemCount > 0 ? (containerWidth - (itemCount - 1) * Theme.spacingS) / itemCount : 0
}
height: compact ? itemHeight : (itemHeight * 2 + Theme.spacingS)
Row {
visible: compact
width: parent.width
spacing: Theme.spacingS
Repeater {
model: categories ? categories.slice(0, Math.min(categories.length || 0, maxCompactItems)) : []
Rectangle {
property int itemCount: Math.min(categories ? categories.length || 0 : 0, maxCompactItems)
height: root.itemHeight
width: root.getButtonWidth(itemCount, parent.width)
radius: Theme.cornerRadius
color: selectedCategory === modelData ? Theme.primary : Theme.surfaceContainerHigh
StyledText {
anchors.centerIn: parent
text: modelData
color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
elide: Text.ElideRight
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.handleCategoryClick(modelData)
}
}
}
}
Column {
visible: !compact
width: parent.width
spacing: Theme.spacingS
Row {
width: parent.width
spacing: Theme.spacingS
Repeater {
model: categories ? categories.slice(0, Math.min(4, categories.length || 0)) : []
Rectangle {
property int itemCount: Math.min(4, categories ? categories.length || 0 : 0)
height: root.itemHeight
width: root.getButtonWidth(itemCount, parent.width)
radius: Theme.cornerRadius
color: selectedCategory === modelData ? Theme.primary : Theme.surfaceContainerHigh
border.color: selectedCategory === modelData ? selectedBorderColor : unselectedBorderColor
StyledText {
anchors.centerIn: parent
text: modelData
color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
elide: Text.ElideRight
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.handleCategoryClick(modelData)
}
}
}
}
Row {
width: parent.width
spacing: Theme.spacingS
visible: categories && categories.length > 4
Repeater {
model: categories && categories.length > 4 ? categories.slice(4) : []
Rectangle {
property int itemCount: categories && categories.length > 4 ? categories.length - 4 : 0
height: root.itemHeight
width: root.getButtonWidth(itemCount, parent.width)
radius: Theme.cornerRadius
color: selectedCategory === modelData ? Theme.primary : Theme.surfaceContainerHigh
border.color: selectedCategory === modelData ? selectedBorderColor : unselectedBorderColor
StyledText {
anchors.centerIn: parent
text: modelData
color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
elide: Text.ElideRight
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.handleCategoryClick(modelData)
}
}
}
}
}
}
@@ -0,0 +1,246 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.Plugins
PluginComponent {
id: root
Ref {
service: VpnService
}
ccWidgetIcon: VpnService.isBusy ? "sync" : (VpnService.connected ? "vpn_lock" : "vpn_key_off")
ccWidgetPrimaryText: "VPN"
ccWidgetSecondaryText: {
if (!VpnService.connected)
return "Disconnected"
const names = VpnService.activeNames || []
if (names.length <= 1)
return names[0] || "Connected"
return names[0] + " +" + (names.length - 1)
}
ccWidgetIsActive: VpnService.connected
onCcWidgetToggled: {
if (VpnService.connected) {
VpnService.disconnectAllActive()
} else if (VpnService.profiles.length > 0) {
VpnService.connect(VpnService.profiles[0].uuid)
}
}
ccDetailContent: Component {
Rectangle {
id: detailRoot
implicitHeight: detailColumn.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
Column {
id: detailColumn
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
RowLayout {
spacing: Theme.spacingS
width: parent.width
StyledText {
text: {
if (!VpnService.connected)
return "Active: None"
const names = VpnService.activeNames || []
if (names.length <= 1)
return "Active: " + (names[0] || "VPN")
return "Active: " + names[0] + " +" + (names.length - 1)
}
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
Item {
Layout.fillWidth: true
}
Rectangle {
height: 28
radius: 14
color: discAllArea.containsMouse ? Theme.errorHover : Theme.surfaceLight
visible: VpnService.connected
width: 110
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "link_off"
size: Theme.fontSizeSmall
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Disconnect")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
}
MouseArea {
id: discAllArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: VpnService.disconnectAllActive()
}
}
}
Rectangle {
height: 1
width: parent.width
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
}
DankFlickable {
width: parent.width
height: 160
contentHeight: listCol.height
clip: true
Column {
id: listCol
width: parent.width
spacing: Theme.spacingXS
Item {
width: parent.width
height: VpnService.profiles.length === 0 ? 120 : 0
visible: height > 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: "playlist_remove"
size: 36
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("No VPN profiles found")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("Add a VPN in NetworkManager")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
Repeater {
model: VpnService.profiles
delegate: Rectangle {
required property var modelData
width: parent ? parent.width : 300
height: 50
radius: Theme.cornerRadius
color: rowArea.containsMouse ? Theme.primaryHoverLight : (VpnService.isActiveUuid(modelData.uuid) ? Theme.primaryPressed : Theme.surfaceLight)
border.width: VpnService.isActiveUuid(modelData.uuid) ? 2 : 1
border.color: VpnService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.outlineLight
RowLayout {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
DankIcon {
name: VpnService.isActiveUuid(modelData.uuid) ? "vpn_lock" : "vpn_key_off"
size: Theme.iconSize - 4
color: VpnService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText
Layout.alignment: Qt.AlignVCenter
}
Column {
spacing: 2
Layout.alignment: Qt.AlignVCenter
StyledText {
text: modelData.name
font.pixelSize: Theme.fontSizeMedium
color: VpnService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText
}
StyledText {
text: {
if (modelData.type === "wireguard")
return "WireGuard"
const svc = modelData.serviceType || ""
if (svc.indexOf("openvpn") !== -1)
return "OpenVPN"
if (svc.indexOf("wireguard") !== -1)
return "WireGuard (plugin)"
if (svc.indexOf("openconnect") !== -1)
return "OpenConnect"
if (svc.indexOf("fortissl") !== -1 || svc.indexOf("forti") !== -1)
return "Fortinet"
if (svc.indexOf("strongswan") !== -1)
return "IPsec (strongSwan)"
if (svc.indexOf("libreswan") !== -1)
return "IPsec (Libreswan)"
if (svc.indexOf("l2tp") !== -1)
return "L2TP/IPsec"
if (svc.indexOf("pptp") !== -1)
return "PPTP"
if (svc.indexOf("vpnc") !== -1)
return "Cisco (vpnc)"
if (svc.indexOf("sstp") !== -1)
return "SSTP"
if (svc)
return svc.split('.').pop()
return "VPN"
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
}
}
Item {
Layout.fillWidth: true
}
}
MouseArea {
id: rowArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: VpnService.toggle(modelData.uuid)
}
}
}
}
}
}
}
}
}
@@ -0,0 +1,120 @@
import QtQuick
import qs.Common
import qs.Widgets
Rectangle {
id: root
property string iconName: ""
property string text: ""
property string secondaryText: ""
property bool isActive: false
property bool enabled: true
property int widgetIndex: 0
property var widgetData: null
property bool editMode: false
signal clicked()
width: parent ? parent.width : 200
height: 60
radius: {
if (Theme.cornerRadius === 0) return 0
return isActive ? Theme.cornerRadius : Theme.cornerRadius + 4
}
readonly property color _tileBgActive: Theme.primary
readonly property color _tileBgInactive:
Theme.surfaceContainerHigh
readonly property color _tileRingActive:
Qt.rgba(Theme.primaryText.r, Theme.primaryText.g, Theme.primaryText.b, 0.22)
color: isActive ? _tileBgActive : _tileBgInactive
border.color: isActive ? _tileRingActive : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: isActive ? 1 : 1
opacity: enabled ? 1.0 : 0.6
function hoverTint(base) {
const factor = 1.2
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor)
}
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: mouseArea.containsMouse ? hoverTint(root.color) : "transparent"
opacity: mouseArea.containsMouse ? 0.08 : 0.0
Behavior on opacity {
NumberAnimation { duration: Theme.shortDuration }
}
}
Row {
anchors.fill: parent
anchors.leftMargin: Theme.spacingL + 2
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingM
DankIcon {
name: root.iconName
size: Theme.iconSize
color: isActive ? Theme.primaryText : Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - Theme.iconSize - parent.spacing
height: parent.height
Column {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 2
Typography {
width: parent.width
text: root.text
style: Typography.Style.Body
color: isActive ? Theme.primaryText : Theme.surfaceText
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
Typography {
width: parent.width
text: root.secondaryText
style: Typography.Style.Caption
color: isActive ? Theme.primaryText : Theme.surfaceVariantText
visible: text.length > 0
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: root.enabled
onClicked: root.clicked()
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on radius {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
@@ -0,0 +1,165 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Modules.ControlCenter.Details
import qs.Modules.ControlCenter.Models
Item {
id: root
property string expandedSection: ""
property var expandedWidgetData: null
property var bluetoothCodecSelector: null
property var pluginDetailInstance: null
property var widgetModel: null
Loader {
id: pluginDetailLoader
width: parent.width
height: 250
y: Theme.spacingS
active: false
sourceComponent: null
}
Loader {
id: coreDetailLoader
width: parent.width
height: 250
y: Theme.spacingS
active: false
sourceComponent: null
}
onExpandedSectionChanged: {
if (pluginDetailInstance) {
pluginDetailInstance.destroy()
pluginDetailInstance = null
}
pluginDetailLoader.active = false
coreDetailLoader.active = false
if (!root.expandedSection) {
return
}
if (root.expandedSection.startsWith("builtin_")) {
const builtinId = root.expandedSection
let builtinInstance = null
if (builtinId === "builtin_vpn") {
if (widgetModel?.vpnLoader) {
widgetModel.vpnLoader.active = true
}
builtinInstance = widgetModel.vpnBuiltinInstance
}
if (!builtinInstance || !builtinInstance.ccDetailContent) {
return
}
pluginDetailLoader.sourceComponent = builtinInstance.ccDetailContent
pluginDetailLoader.active = parent.height > 0
return
}
if (root.expandedSection.startsWith("plugin_")) {
const pluginId = root.expandedSection.replace("plugin_", "")
const pluginComponent = PluginService.pluginWidgetComponents[pluginId]
if (!pluginComponent) {
return
}
pluginDetailInstance = pluginComponent.createObject(null)
if (!pluginDetailInstance || !pluginDetailInstance.ccDetailContent) {
if (pluginDetailInstance) {
pluginDetailInstance.destroy()
pluginDetailInstance = null
}
return
}
pluginDetailLoader.sourceComponent = pluginDetailInstance.ccDetailContent
pluginDetailLoader.active = parent.height > 0
return
}
if (root.expandedSection.startsWith("diskUsage_")) {
coreDetailLoader.sourceComponent = diskUsageDetailComponent
coreDetailLoader.active = parent.height > 0
return
}
switch (root.expandedSection) {
case "network":
case "wifi": coreDetailLoader.sourceComponent = networkDetailComponent; break
case "bluetooth": coreDetailLoader.sourceComponent = bluetoothDetailComponent; break
case "audioOutput": coreDetailLoader.sourceComponent = audioOutputDetailComponent; break
case "audioInput": coreDetailLoader.sourceComponent = audioInputDetailComponent; break
case "battery": coreDetailLoader.sourceComponent = batteryDetailComponent; break
default: return
}
coreDetailLoader.active = parent.height > 0
}
Component {
id: networkDetailComponent
NetworkDetail {}
}
Component {
id: bluetoothDetailComponent
BluetoothDetail {
id: bluetoothDetail
onShowCodecSelector: function(device) {
if (root.bluetoothCodecSelector) {
root.bluetoothCodecSelector.show(device)
root.bluetoothCodecSelector.codecSelected.connect(function(deviceAddress, codecName) {
bluetoothDetail.updateDeviceCodecDisplay(deviceAddress, codecName)
})
}
}
}
}
Component {
id: audioOutputDetailComponent
AudioOutputDetail {}
}
Component {
id: audioInputDetailComponent
AudioInputDetail {}
}
Component {
id: batteryDetailComponent
BatteryDetail {}
}
Component {
id: diskUsageDetailComponent
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)
}
}
}
}
}
@@ -0,0 +1,91 @@
import QtQuick
import qs.Common
import qs.Modules.ControlCenter.Details
Item {
id: root
property string expandedSection: ""
property var expandedWidgetData: null
height: active ? 250 : 0
visible: active
readonly property bool active: expandedSection !== ""
Behavior on height {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Easing.OutCubic
}
}
Loader {
anchors.fill: parent
anchors.topMargin: Theme.spacingS
sourceComponent: {
if (!root.active) return null
if (expandedSection.startsWith("diskUsage_")) {
return diskUsageDetailComponent
}
switch (expandedSection) {
case "wifi": return networkDetailComponent
case "bluetooth": return bluetoothDetailComponent
case "audioOutput": return audioOutputDetailComponent
case "audioInput": return audioInputDetailComponent
case "battery": return batteryDetailComponent
default: return null
}
}
}
Component {
id: networkDetailComponent
NetworkDetail {}
}
Component {
id: bluetoothDetailComponent
BluetoothDetail {}
}
Component {
id: audioOutputDetailComponent
AudioOutputDetail {}
}
Component {
id: audioInputDetailComponent
AudioInputDetail {}
}
Component {
id: batteryDetailComponent
BatteryDetail {}
}
Component {
id: diskUsageDetailComponent
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)
}
}
}
}
}
@@ -0,0 +1,979 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Modules.ControlCenter.Widgets
import qs.Modules.ControlCenter.Components
import "../utils/layout.js" as LayoutUtils
Column {
id: root
property bool editMode: false
property string expandedSection: ""
property int expandedWidgetIndex: -1
property var model: null
property var expandedWidgetData: null
property var bluetoothCodecSelector: null
property bool darkModeTransitionPending: false
signal expandClicked(var widgetData, int globalIndex)
signal removeWidget(int index)
signal moveWidget(int fromIndex, int toIndex)
signal toggleWidgetSize(int index)
spacing: editMode ? Theme.spacingL : Theme.spacingS
property var currentRowWidgets: []
property real currentRowWidth: 0
property int expandedRowIndex: -1
property var colorPickerModal: null
function calculateRowsAndWidgets() {
return LayoutUtils.calculateRowsAndWidgets(root, expandedSection, expandedWidgetIndex)
}
property var layoutResult: {
const dummy = [expandedSection, expandedWidgetIndex, model?.controlCenterWidgets]
return calculateRowsAndWidgets()
}
onLayoutResultChanged: {
expandedRowIndex = layoutResult.expandedRowIndex
}
function moveToTop(item) {
const children = root.children
for (var i = 0; i < children.length; i++) {
if (children[i] === item)
continue
if (children[i].z)
children[i].z = Math.min(children[i].z, 999)
}
item.z = 1000
}
Repeater {
model: root.layoutResult.rows
Column {
width: root.width
spacing: 0
property int rowIndex: index
property var rowWidgets: modelData
property bool isSliderOnlyRow: {
const widgets = rowWidgets || []
if (widgets.length === 0)
return false
return widgets.every(w => w.id === "volumeSlider" || w.id === "brightnessSlider" || w.id === "inputVolumeSlider")
}
topPadding: isSliderOnlyRow ? (root.editMode ? 4 : -6) : 0
bottomPadding: isSliderOnlyRow ? (root.editMode ? 4 : -6) : 0
Flow {
width: parent.width
spacing: Theme.spacingS
Repeater {
model: rowWidgets || []
DragDropWidgetWrapper {
widgetData: modelData
property int globalWidgetIndex: {
const widgets = SettingsData.controlCenterWidgets || []
for (var i = 0; i < widgets.length; i++) {
if (widgets[i].id === modelData.id) {
if (modelData.id === "diskUsage") {
if (widgets[i].instanceId === modelData.instanceId) {
return i
}
} else {
return i
}
}
}
return -1
}
property int widgetWidth: modelData.width || 50
width: {
const baseWidth = root.width
const spacing = Theme.spacingS
if (widgetWidth <= 25) {
return (baseWidth - spacing * 3) / 4
} else if (widgetWidth <= 50) {
return (baseWidth - spacing) / 2
} else if (widgetWidth <= 75) {
return (baseWidth - spacing * 2) * 0.75
} else {
return baseWidth
}
}
height: isSliderOnlyRow ? 48 : 60
editMode: root.editMode
widgetIndex: globalWidgetIndex
gridCellWidth: width
gridCellHeight: height
gridColumns: 4
gridLayout: root
isSlider: {
const id = modelData.id || ""
return id === "volumeSlider" || id === "brightnessSlider" || id === "inputVolumeSlider"
}
widgetComponent: {
const id = modelData.id || ""
if (id.startsWith("builtin_")) {
return builtinPluginWidgetComponent
} else if (id.startsWith("plugin_")) {
return pluginWidgetComponent
} else if (id === "wifi" || id === "bluetooth" || id === "audioOutput" || id === "audioInput") {
return compoundPillComponent
} else if (id === "volumeSlider") {
return audioSliderComponent
} else if (id === "brightnessSlider") {
return brightnessSliderComponent
} else if (id === "inputVolumeSlider") {
return inputAudioSliderComponent
} else if (id === "battery") {
return widgetWidth <= 25 ? smallBatteryComponent : batteryPillComponent
} else if (id === "diskUsage") {
return diskUsagePillComponent
} else if (id === "colorPicker") {
return colorPickerPillComponent
} else {
return widgetWidth <= 25 ? smallToggleComponent : toggleButtonComponent
}
}
onWidgetMoved: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex)
onRemoveWidget: index => root.removeWidget(index)
onToggleWidgetSize: index => root.toggleWidgetSize(index)
}
}
}
DetailHost {
width: parent.width
height: active ? (250 + Theme.spacingS) : 0
property bool active: {
if (root.expandedSection === "")
return false
if (root.expandedSection.startsWith("diskUsage_") && root.expandedWidgetData) {
const expandedInstanceId = root.expandedWidgetData.instanceId
return rowWidgets.some(w => w.id === "diskUsage" && w.instanceId === expandedInstanceId)
}
return rowIndex === root.expandedRowIndex
}
visible: active
expandedSection: root.expandedSection
expandedWidgetData: root.expandedWidgetData
bluetoothCodecSelector: root.bluetoothCodecSelector
widgetModel: root.model
}
}
}
Component {
id: errorPillComponent
ErrorPill {
property var widgetData: parent.widgetData || {}
width: parent.width
height: 60
primaryMessage: {
if (!DMSService.dmsAvailable) {
return I18n.tr("DMS_SOCKET not available")
}
return I18n.tr("NM not supported")
}
secondaryMessage: I18n.tr("update dms for NM integration.")
}
}
Component {
id: compoundPillComponent
CompoundPill {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
property var widgetDef: root.model?.getWidgetForId(widgetData.id || "")
width: parent.width
height: 60
iconName: {
switch (widgetData.id || "") {
case "wifi":
{
if (NetworkService.wifiToggling)
return "sync"
if (NetworkService.networkStatus === "ethernet")
return "settings_ethernet"
if (NetworkService.networkStatus === "wifi")
return NetworkService.wifiSignalIcon
if (NetworkService.wifiEnabled)
return "wifi_off"
return "wifi_off"
}
case "bluetooth":
{
if (!BluetoothService.available)
return "bluetooth_disabled"
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled)
return "bluetooth_disabled"
const primaryDevice = (() => {
if (!BluetoothService.adapter || !BluetoothService.adapter.devices)
return null
let devices = [...BluetoothService.adapter.devices.values.filter(dev => dev && (dev.paired || dev.trusted))]
for (let device of devices) {
if (device && device.connected)
return device
}
return null
})()
if (primaryDevice)
return BluetoothService.getDeviceIcon(primaryDevice)
return "bluetooth"
}
case "audioOutput":
{
if (!AudioService.sink)
return "volume_off"
let volume = AudioService.sink.audio.volume
let muted = AudioService.sink.audio.muted
if (muted || volume === 0.0)
return "volume_off"
if (volume <= 0.33)
return "volume_down"
if (volume <= 0.66)
return "volume_up"
return "volume_up"
}
case "audioInput":
{
if (!AudioService.source)
return "mic_off"
let muted = AudioService.source.audio.muted
return muted ? "mic_off" : "mic"
}
default:
return widgetDef?.icon || "help"
}
}
primaryText: {
switch (widgetData.id || "") {
case "wifi":
{
if (NetworkService.wifiToggling)
return NetworkService.wifiEnabled ? "Disabling WiFi..." : "Enabling WiFi..."
if (NetworkService.networkStatus === "ethernet")
return "Ethernet"
if (NetworkService.networkStatus === "wifi" && NetworkService.currentWifiSSID)
return NetworkService.currentWifiSSID
if (NetworkService.wifiEnabled)
return "Not connected"
return "WiFi off"
}
case "bluetooth":
{
if (!BluetoothService.available)
return "Bluetooth"
if (!BluetoothService.adapter)
return "No adapter"
if (!BluetoothService.adapter.enabled)
return "Disabled"
return "Enabled"
}
case "audioOutput":
return AudioService.sink?.description || "No output device"
case "audioInput":
return AudioService.source?.description || "No input device"
default:
return widgetDef?.text || "Unknown"
}
}
secondaryText: {
switch (widgetData.id || "") {
case "wifi":
{
if (NetworkService.wifiToggling)
return "Please wait..."
if (NetworkService.networkStatus === "ethernet")
return "Connected"
if (NetworkService.networkStatus === "wifi")
return NetworkService.wifiSignalStrength > 0 ? NetworkService.wifiSignalStrength + "%" : "Connected"
if (NetworkService.wifiEnabled)
return "Select network"
return ""
}
case "bluetooth":
{
if (!BluetoothService.available)
return "No adapters"
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled)
return "Off"
const primaryDevice = (() => {
if (!BluetoothService.adapter || !BluetoothService.adapter.devices)
return null
let devices = [...BluetoothService.adapter.devices.values.filter(dev => dev && (dev.paired || dev.trusted))]
for (let device of devices) {
if (device && device.connected)
return device
}
return null
})()
if (primaryDevice)
return primaryDevice.name || primaryDevice.alias || primaryDevice.deviceName || "Connected Device"
return "No devices"
}
case "audioOutput":
{
if (!AudioService.sink)
return "Select device"
if (AudioService.sink.audio.muted)
return "Muted"
return Math.round(AudioService.sink.audio.volume * 100) + "%"
}
case "audioInput":
{
if (!AudioService.source)
return "Select device"
if (AudioService.source.audio.muted)
return "Muted"
return Math.round(AudioService.source.audio.volume * 100) + "%"
}
default:
return widgetDef?.description || ""
}
}
isActive: {
switch (widgetData.id || "") {
case "wifi":
{
if (NetworkService.wifiToggling)
return false
if (NetworkService.networkStatus === "ethernet")
return true
if (NetworkService.networkStatus === "wifi")
return true
return NetworkService.wifiEnabled
}
case "bluetooth":
return !!(BluetoothService.available && BluetoothService.adapter && BluetoothService.adapter.enabled)
case "audioOutput":
return !!(AudioService.sink && !AudioService.sink.audio.muted)
case "audioInput":
return !!(AudioService.source && !AudioService.source.audio.muted)
default:
return false
}
}
enabled: widgetDef?.enabled ?? true
onToggled: {
if (root.editMode)
return
switch (widgetData.id || "") {
case "wifi":
{
if (NetworkService.networkStatus !== "ethernet" && !NetworkService.wifiToggling) {
NetworkService.toggleWifiRadio()
}
break
}
case "bluetooth":
{
if (BluetoothService.available && BluetoothService.adapter) {
BluetoothService.adapter.enabled = !BluetoothService.adapter.enabled
}
break
}
case "audioOutput":
{
if (AudioService.sink && AudioService.sink.audio) {
AudioService.sink.audio.muted = !AudioService.sink.audio.muted
}
break
}
case "audioInput":
{
if (AudioService.source && AudioService.source.audio) {
AudioService.source.audio.muted = !AudioService.source.audio.muted
}
break
}
}
}
onExpandClicked: {
if (root.editMode)
return
root.expandClicked(widgetData, widgetIndex)
}
onWheelEvent: function (wheelEvent) {
if (root.editMode)
return
const id = widgetData.id || ""
if (id === "audioOutput") {
if (!AudioService.sink || !AudioService.sink.audio)
return
let delta = wheelEvent.angleDelta.y
let currentVolume = AudioService.sink.audio.volume * 100
let newVolume
if (delta > 0)
newVolume = Math.min(100, currentVolume + 5)
else
newVolume = Math.max(0, currentVolume - 5)
AudioService.sink.audio.muted = false
AudioService.sink.audio.volume = newVolume / 100
wheelEvent.accepted = true
} else if (id === "audioInput") {
if (!AudioService.source || !AudioService.source.audio)
return
let delta = wheelEvent.angleDelta.y
let currentVolume = AudioService.source.audio.volume * 100
let newVolume
if (delta > 0)
newVolume = Math.min(100, currentVolume + 5)
else
newVolume = Math.max(0, currentVolume - 5)
AudioService.source.audio.muted = false
AudioService.source.audio.volume = newVolume / 100
wheelEvent.accepted = true
}
}
}
}
Component {
id: audioSliderComponent
Item {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
width: parent.width
height: 16
AudioSliderRow {
anchors.centerIn: parent
width: parent.width
height: 14
property color sliderTrackColor: Theme.surfaceContainerHigh
}
}
}
Component {
id: brightnessSliderComponent
Item {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
width: parent.width
height: 16
BrightnessSliderRow {
anchors.centerIn: parent
width: parent.width
height: 14
property color sliderTrackColor: Theme.surfaceContainerHigh
}
}
}
Component {
id: inputAudioSliderComponent
Item {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
width: parent.width
height: 16
InputAudioSliderRow {
anchors.centerIn: parent
width: parent.width
height: 14
property color sliderTrackColor: Theme.surfaceContainerHigh
}
}
}
Component {
id: batteryPillComponent
BatteryPill {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
width: parent.width
height: 60
onExpandClicked: {
if (!root.editMode) {
root.expandClicked(widgetData, widgetIndex)
}
}
}
}
Component {
id: smallBatteryComponent
SmallBatteryButton {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
width: parent.width
height: 48
onClicked: {
if (!root.editMode) {
root.expandClicked(widgetData, widgetIndex)
}
}
}
}
Component {
id: toggleButtonComponent
ToggleButton {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
width: parent.width
height: 60
iconName: {
switch (widgetData.id || "") {
case "nightMode":
return DisplayService.nightModeEnabled ? "nightlight" : "dark_mode"
case "darkMode":
return "contrast"
case "doNotDisturb":
return SessionData.doNotDisturb ? "do_not_disturb_on" : "do_not_disturb_off"
case "idleInhibitor":
return SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle"
default:
return "help"
}
}
text: {
switch (widgetData.id || "") {
case "nightMode":
return "Night Mode"
case "darkMode":
return SessionData.isLightMode ? "Light Mode" : "Dark Mode"
case "doNotDisturb":
return "Do Not Disturb"
case "idleInhibitor":
return SessionService.idleInhibited ? "Keeping Awake" : "Keep Awake"
default:
return "Unknown"
}
}
iconRotation: {
if (widgetData.id !== "darkMode")
return 0
if (darkModeTransitionPending) {
return SessionData.isLightMode ? 180 : 0
}
return SessionData.isLightMode ? 180 : 0
}
isActive: {
switch (widgetData.id || "") {
case "nightMode":
return DisplayService.nightModeEnabled || false
case "darkMode":
return SessionData.isLightMode
case "doNotDisturb":
return SessionData.doNotDisturb || false
case "idleInhibitor":
return SessionService.idleInhibited || false
default:
return false
}
}
enabled: !root.editMode
onClicked: {
if (root.editMode)
return
switch (widgetData.id || "") {
case "nightMode":
{
if (DisplayService.automationAvailable)
DisplayService.toggleNightMode()
break
}
case "darkMode":
{
Theme.screenTransition()
Theme.setLightMode(!SessionData.isLightMode)
break
}
case "doNotDisturb":
{
SessionData.setDoNotDisturb(!SessionData.doNotDisturb)
break
}
case "idleInhibitor":
{
SessionService.toggleIdleInhibit()
break
}
}
}
}
}
Component {
id: smallToggleComponent
SmallToggleButton {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
width: parent.width
height: 48
iconName: {
switch (widgetData.id || "") {
case "nightMode":
return DisplayService.nightModeEnabled ? "nightlight" : "dark_mode"
case "darkMode":
return "contrast"
case "doNotDisturb":
return SessionData.doNotDisturb ? "do_not_disturb_on" : "do_not_disturb_off"
case "idleInhibitor":
return SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle"
default:
return "help"
}
}
iconRotation: {
if (widgetData.id !== "darkMode")
return 0
if (darkModeTransitionPending) {
return SessionData.isLightMode ? 180 : 0
}
return SessionData.isLightMode ? 180 : 0
}
isActive: {
switch (widgetData.id || "") {
case "nightMode":
return DisplayService.nightModeEnabled || false
case "darkMode":
return SessionData.isLightMode
case "doNotDisturb":
return SessionData.doNotDisturb || false
case "idleInhibitor":
return SessionService.idleInhibited || false
default:
return false
}
}
enabled: !root.editMode
onClicked: {
if (root.editMode)
return
switch (widgetData.id || "") {
case "nightMode":
{
if (DisplayService.automationAvailable)
DisplayService.toggleNightMode()
break
}
case "darkMode":
{
Theme.screenTransition()
Theme.setLightMode(!SessionData.isLightMode)
break
}
case "doNotDisturb":
{
SessionData.setDoNotDisturb(!SessionData.doNotDisturb)
break
}
case "idleInhibitor":
{
SessionService.toggleIdleInhibit()
break
}
}
}
}
}
Component {
id: diskUsagePillComponent
DiskUsagePill {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
width: parent.width
height: 60
mountPath: widgetData.mountPath || "/"
instanceId: widgetData.instanceId || ""
onExpandClicked: {
if (!root.editMode) {
root.expandClicked(widgetData, widgetIndex)
}
}
}
}
Component {
id: colorPickerPillComponent
ColorPickerPill {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
width: parent.width
height: 60
colorPickerModal: root.colorPickerModal
}
}
Component {
id: builtinPluginWidgetComponent
Loader {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
property int widgetWidth: widgetData.width || 50
width: parent.width
height: 60
property var builtinInstance: null
Component.onCompleted: {
const id = widgetData.id || ""
if (id === "builtin_vpn") {
if (root.model?.vpnLoader) {
root.model.vpnLoader.active = true
}
builtinInstance = Qt.binding(() => root.model?.vpnBuiltinInstance)
}
}
sourceComponent: {
if (!builtinInstance)
return null
const hasDetail = builtinInstance.ccDetailContent !== null
if (widgetWidth <= 25) {
return builtinSmallToggleComponent
} else if (hasDetail) {
return builtinCompoundPillComponent
} else {
return builtinToggleComponent
}
}
}
}
Component {
id: builtinCompoundPillComponent
CompoundPill {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
property var builtinInstance: parent.builtinInstance
iconName: builtinInstance?.ccWidgetIcon || "extension"
primaryText: builtinInstance?.ccWidgetPrimaryText || "Built-in"
secondaryText: builtinInstance?.ccWidgetSecondaryText || ""
isActive: builtinInstance?.ccWidgetIsActive || false
onToggled: {
if (root.editMode)
return
if (builtinInstance) {
builtinInstance.ccWidgetToggled()
}
}
onExpandClicked: {
if (root.editMode)
return
root.expandClicked(widgetData, widgetIndex)
}
}
}
Component {
id: builtinToggleComponent
ToggleButton {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
property var builtinInstance: parent.builtinInstance
iconName: builtinInstance?.ccWidgetIcon || "extension"
text: builtinInstance?.ccWidgetPrimaryText || "Built-in"
isActive: builtinInstance?.ccWidgetIsActive || false
enabled: !root.editMode
onClicked: {
if (root.editMode)
return
if (builtinInstance) {
builtinInstance.ccWidgetToggled()
}
}
}
}
Component {
id: builtinSmallToggleComponent
SmallToggleButton {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
property var builtinInstance: parent.builtinInstance
iconName: builtinInstance?.ccWidgetIcon || "extension"
isActive: builtinInstance?.ccWidgetIsActive || false
enabled: !root.editMode
onClicked: {
if (root.editMode)
return
if (builtinInstance) {
builtinInstance.ccWidgetToggled()
}
}
}
}
Component {
id: pluginWidgetComponent
Loader {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
property int widgetWidth: widgetData.width || 50
width: parent.width
height: 60
property var pluginInstance: null
property string pluginId: widgetData.id?.replace("plugin_", "") || ""
sourceComponent: {
if (!pluginInstance)
return null
const hasDetail = pluginInstance.ccDetailContent !== null
if (widgetWidth <= 25) {
return pluginSmallToggleComponent
} else if (hasDetail) {
return pluginCompoundPillComponent
} else {
return pluginToggleComponent
}
}
Component.onCompleted: {
Qt.callLater(() => {
const pluginComponent = PluginService.pluginWidgetComponents[pluginId]
if (pluginComponent) {
const instance = pluginComponent.createObject(null, {
"pluginId": pluginId,
"pluginService": PluginService,
"visible": false,
"width": 0,
"height": 0
})
if (instance) {
pluginInstance = instance
}
}
})
}
Connections {
target: PluginService
function onPluginDataChanged(changedPluginId) {
if (changedPluginId === pluginId && pluginInstance) {
pluginInstance.loadPluginData()
}
}
}
Component.onDestruction: {
if (pluginInstance) {
pluginInstance.destroy()
}
}
}
}
Component {
id: pluginCompoundPillComponent
CompoundPill {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
property var pluginInstance: parent.pluginInstance
iconName: pluginInstance?.ccWidgetIcon || "extension"
primaryText: pluginInstance?.ccWidgetPrimaryText || "Plugin"
secondaryText: pluginInstance?.ccWidgetSecondaryText || ""
isActive: pluginInstance?.ccWidgetIsActive || false
onToggled: {
if (root.editMode)
return
if (pluginInstance) {
pluginInstance.ccWidgetToggled()
}
}
onExpandClicked: {
if (root.editMode)
return
root.expandClicked(widgetData, widgetIndex)
}
}
}
Component {
id: pluginToggleComponent
ToggleButton {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
property var pluginInstance: parent.pluginInstance
property var widgetDef: root.model?.getWidgetForId(widgetData.id || "")
iconName: pluginInstance?.ccWidgetIcon || widgetDef?.icon || "extension"
text: pluginInstance?.ccWidgetPrimaryText || widgetDef?.text || "Plugin"
secondaryText: pluginInstance?.ccWidgetSecondaryText || ""
isActive: pluginInstance?.ccWidgetIsActive || false
enabled: !root.editMode
onClicked: {
if (root.editMode)
return
if (pluginInstance) {
pluginInstance.ccWidgetToggled()
}
}
}
}
Component {
id: pluginSmallToggleComponent
SmallToggleButton {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
property var pluginInstance: parent.pluginInstance
property var widgetDef: root.model?.getWidgetForId(widgetData.id || "")
iconName: pluginInstance?.ccWidgetIcon || widgetDef?.icon || "extension"
isActive: pluginInstance?.ccWidgetIsActive || false
enabled: !root.editMode
onClicked: {
if (root.editMode)
return
if (pluginInstance && pluginInstance.ccDetailContent) {
root.expandClicked(widgetData, widgetIndex)
} else if (pluginInstance) {
pluginInstance.ccWidgetToggled()
}
}
}
}
}
@@ -0,0 +1,289 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property bool editMode: false
property var widgetData: null
property int widgetIndex: -1
property bool isSlider: false
property Component widgetComponent: null
property real gridCellWidth: 100
property real gridCellHeight: 60
property int gridColumns: 4
property var gridLayout: null
z: dragArea.drag.active ? 10000 : 1
signal widgetMoved(int fromIndex, int toIndex)
signal removeWidget(int index)
signal toggleWidgetSize(int index)
width: {
const widgetWidth = widgetData?.width || 50
if (widgetWidth <= 25) return gridCellWidth
else if (widgetWidth <= 50) return gridCellWidth * 2
else if (widgetWidth <= 75) return gridCellWidth * 3
else return gridCellWidth * 4
}
height: isSlider ? 16 : gridCellHeight
Rectangle {
id: dragIndicator
anchors.fill: parent
color: "transparent"
border.color: Theme.primary
border.width: dragArea.drag.active ? 2 : 0
radius: Theme.cornerRadius
opacity: dragArea.drag.active ? 0.8 : 1.0
z: dragArea.drag.active ? 10000 : 1
Behavior on border.width {
NumberAnimation { duration: 150 }
}
Behavior on opacity {
NumberAnimation { duration: 150 }
}
}
Loader {
id: widgetLoader
anchors.fill: parent
sourceComponent: widgetComponent
property var widgetData: root.widgetData
property int widgetIndex: root.widgetIndex
property int globalWidgetIndex: root.widgetIndex
property int widgetWidth: root.widgetData?.width || 50
MouseArea {
id: editModeBlocker
anchors.fill: parent
enabled: root.editMode
acceptedButtons: Qt.AllButtons
onPressed: function(mouse) { mouse.accepted = true }
onWheel: function(wheel) { wheel.accepted = true }
z: 100
}
}
MouseArea {
id: dragArea
anchors.fill: parent
enabled: editMode
cursorShape: editMode ? Qt.OpenHandCursor : Qt.PointingHandCursor
drag.target: editMode ? root : null
drag.axis: Drag.XAndYAxis
drag.smoothed: true
onPressed: function(mouse) {
if (editMode) {
cursorShape = Qt.ClosedHandCursor
if (root.gridLayout && root.gridLayout.moveToTop) {
root.gridLayout.moveToTop(root)
}
}
}
onReleased: function(mouse) {
if (editMode) {
cursorShape = Qt.OpenHandCursor
root.snapToGrid()
}
}
}
Drag.active: dragArea.drag.active
Drag.hotSpot.x: width / 2
Drag.hotSpot.y: height / 2
function swapIndices(i, j) {
if (i === j) return;
const arr = SettingsData.controlCenterWidgets;
if (!arr || i < 0 || j < 0 || i >= arr.length || j >= arr.length) return;
const copy = arr.slice();
const tmp = copy[i];
copy[i] = copy[j];
copy[j] = tmp;
SettingsData.setControlCenterWidgets(copy);
}
function snapToGrid() {
if (!editMode || !gridLayout) return
const globalPos = root.mapToItem(gridLayout, 0, 0)
const cellWidth = gridLayout.width / gridColumns
const cellHeight = gridCellHeight + Theme.spacingS
const centerX = globalPos.x + (root.width / 2)
const centerY = globalPos.y + (root.height / 2)
let targetCol = Math.max(0, Math.floor(centerX / cellWidth))
let targetRow = Math.max(0, Math.floor(centerY / cellHeight))
targetCol = Math.min(targetCol, gridColumns - 1)
const newIndex = findBestInsertionIndex(targetRow, targetCol)
if (newIndex !== widgetIndex && newIndex >= 0 && newIndex < (SettingsData.controlCenterWidgets?.length || 0)) {
swapIndices(widgetIndex, newIndex)
}
}
function findBestInsertionIndex(targetRow, targetCol) {
const widgets = SettingsData.controlCenterWidgets || [];
const n = widgets.length;
if (!n || widgetIndex < 0 || widgetIndex >= n) return -1;
function spanFor(width) {
const w = width ?? 50;
if (w <= 25) return 1;
if (w <= 50) return 2;
if (w <= 75) return 3;
return 4;
}
const cols = gridColumns || 4;
let row = 0, col = 0;
let draggedOrigKey = null;
const pos = [];
for (let i = 0; i < n; i++) {
const span = Math.min(spanFor(widgets[i].width), cols);
if (col + span > cols) {
row++;
col = 0;
}
const startCol = col;
const centerKey = row * cols + (startCol + (span - 1) / 2);
if (i === widgetIndex) {
draggedOrigKey = centerKey;
} else {
pos.push({ index: i, row, startCol, span, centerKey });
}
col += span;
if (col >= cols) {
row++;
col = 0;
}
}
if (pos.length === 0) return -1;
const centerColCoord = targetCol + 0.5;
const targetKey = targetRow * cols + centerColCoord;
for (let k = 0; k < pos.length; k++) {
const p = pos[k];
if (p.row === targetRow && centerColCoord >= p.startCol && centerColCoord < (p.startCol + p.span)) {
return p.index;
}
}
let lo = 0, hi = pos.length - 1;
if (targetKey <= pos[0].centerKey) return pos[0].index;
if (targetKey >= pos[hi].centerKey) return pos[hi].index;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
const mk = pos[mid].centerKey;
if (targetKey < mk) hi = mid - 1;
else if (targetKey > mk) lo = mid + 1;
else return pos[mid].index;
}
const movingUp = (draggedOrigKey != null) ? (targetKey < draggedOrigKey) : false;
return (movingUp ? pos[lo].index : pos[hi].index);
}
Rectangle {
width: 16
height: 16
radius: 8
color: Theme.error
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: -4
visible: editMode
z: 10
DankIcon {
anchors.centerIn: parent
name: "close"
size: 12
color: Theme.primaryText
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: removeWidget(widgetIndex)
}
}
SizeControls {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.margins: -6
visible: editMode
z: 10
currentSize: root.widgetData?.width || 50
isSlider: root.isSlider
widgetIndex: root.widgetIndex
onSizeChanged: (newSize) => {
var widgets = SettingsData.controlCenterWidgets.slice()
if (widgetIndex >= 0 && widgetIndex < widgets.length) {
widgets[widgetIndex].width = newSize
SettingsData.setControlCenterWidgets(widgets)
}
}
}
Rectangle {
id: dragHandle
width: 16
height: 12
radius: 2
color: Theme.primary
anchors.top: parent.top
anchors.left: parent.left
anchors.margins: 4
visible: editMode
z: 15
opacity: dragArea.drag.active ? 1.0 : 0.7
DankIcon {
anchors.centerIn: parent
name: "drag_indicator"
size: 10
color: Theme.primaryText
}
Behavior on opacity {
NumberAnimation { duration: 150 }
}
}
Rectangle {
anchors.fill: parent
color: editMode ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: Theme.cornerRadius
border.color: "transparent"
border.width: 0
z: -1
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
}
}
}
@@ -0,0 +1,239 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Widgets
Row {
id: root
property var availableWidgets: []
property Item popoutContent: null
signal addWidget(string widgetId)
signal resetToDefault()
signal clearAll()
height: 48
spacing: Theme.spacingS
onAddWidget: addWidgetPopup.close()
Popup {
id: addWidgetPopup
parent: popoutContent
x: parent ? Math.round((parent.width - width) / 2) : 0
y: parent ? Math.round((parent.height - height) / 2) : 0
width: 400
height: 300
modal: true
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
background: Rectangle {
color: Theme.surfaceContainer
border.color: Theme.primarySelected
border.width: 0
radius: Theme.cornerRadius
}
contentItem: Item {
anchors.fill: parent
anchors.margins: Theme.spacingL
Row {
id: headerRow
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
spacing: Theme.spacingM
DankIcon {
name: "add_circle"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Typography {
text: I18n.tr("Add Widget")
style: Typography.Style.Subtitle
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankListView {
anchors.top: headerRow.bottom
anchors.topMargin: Theme.spacingM
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
spacing: Theme.spacingS
model: root.availableWidgets
delegate: Rectangle {
width: 400 - Theme.spacingL * 2
height: 50
radius: Theme.cornerRadius
color: widgetMouseArea.containsMouse ? Theme.primaryHover : Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0
Row {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
DankIcon {
name: modelData.icon
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
width: 400 - Theme.spacingL * 2 - Theme.iconSize - Theme.spacingM * 3 - Theme.iconSize
Typography {
text: modelData.text
style: Typography.Style.Body
color: Theme.surfaceText
elide: Text.ElideRight
width: parent.width
}
Typography {
text: modelData.description
style: Typography.Style.Caption
color: Theme.outline
elide: Text.ElideRight
width: parent.width
}
}
DankIcon {
name: "add"
size: Theme.iconSize - 4
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: widgetMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.addWidget(modelData.id)
}
}
}
}
}
}
Rectangle {
width: (parent.width - Theme.spacingS * 2) / 3
height: 48
radius: Theme.cornerRadius
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
border.color: Theme.primary
border.width: 0
Row {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: "add"
size: Theme.iconSize - 2
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Typography {
text: I18n.tr("Add Widget")
style: Typography.Style.Button
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: addWidgetPopup.open()
}
}
Rectangle {
width: (parent.width - Theme.spacingS * 2) / 3
height: 48
radius: Theme.cornerRadius
color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12)
border.color: Theme.warning
border.width: 0
Row {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: "settings_backup_restore"
size: Theme.iconSize - 2
color: Theme.warning
anchors.verticalCenter: parent.verticalCenter
}
Typography {
text: I18n.tr("Defaults")
style: Typography.Style.Button
color: Theme.warning
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.resetToDefault()
}
}
Rectangle {
width: (parent.width - Theme.spacingS * 2) / 3
height: 48
radius: Theme.cornerRadius
color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
border.color: Theme.error
border.width: 0
Row {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: "clear_all"
size: Theme.iconSize - 2
color: Theme.error
anchors.verticalCenter: parent.verticalCenter
}
Typography {
text: I18n.tr("Reset")
style: Typography.Style.Button
color: Theme.error
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.clearAll()
}
}
}
@@ -0,0 +1,112 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
property bool editMode: false
signal powerButtonClicked()
signal lockRequested()
signal editModeToggled()
implicitHeight: 70
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 0
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingL
anchors.rightMargin: Theme.spacingL
spacing: Theme.spacingM
DankCircularImage {
id: avatarContainer
width: 60
height: 60
imageSource: {
if (PortalService.profileImage === "")
return ""
if (PortalService.profileImage.startsWith("/"))
return "file://" + PortalService.profileImage
return PortalService.profileImage
}
fallbackIcon: "person"
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
Typography {
text: UserInfoService.fullName
|| UserInfoService.username || "User"
style: Typography.Style.Subtitle
color: Theme.surfaceText
}
Typography {
text: (UserInfoService.uptime || "Unknown")
style: Typography.Style.Caption
color: Theme.surfaceVariantText
}
}
}
Row {
id: actionButtonsRow
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: Theme.spacingXS
spacing: Theme.spacingXS
DankActionButton {
buttonSize: 36
iconName: "lock"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
backgroundColor: "transparent"
onClicked: {
root.lockRequested()
}
}
DankActionButton {
buttonSize: 36
iconName: "power_settings_new"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
backgroundColor: "transparent"
onClicked: root.powerButtonClicked()
}
DankActionButton {
buttonSize: 36
iconName: "settings"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
backgroundColor: "transparent"
onClicked: {
settingsModal.show()
}
}
DankActionButton {
buttonSize: 36
iconName: editMode ? "done" : "edit"
iconSize: Theme.iconSize - 4
iconColor: editMode ? Theme.primary : Theme.surfaceText
backgroundColor: "transparent"
onClicked: root.editModeToggled()
}
}
}
@@ -0,0 +1,52 @@
import QtQuick
import qs.Common
import qs.Widgets
Rectangle {
id: root
property string iconName: ""
property string text: ""
signal pressed()
height: 34
radius: Theme.cornerRadius
color: mouseArea.containsMouse ? Qt.rgba(
Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : Qt.rgba(
Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
0.5)
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: root.iconName
size: Theme.fontSizeSmall
color: mouseArea.containsMouse ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Typography {
text: root.text
style: Typography.Style.Button
color: mouseArea.containsMouse ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: root.pressed()
}
}
@@ -0,0 +1,52 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Widgets
Row {
id: root
property int currentSize: 50
property bool isSlider: false
property int widgetIndex: -1
signal sizeChanged(int newSize)
readonly property var availableSizes: isSlider ? [50, 100] : [25, 50, 75, 100]
spacing: 2
Repeater {
model: root.availableSizes
Rectangle {
width: 16
height: 16
radius: 3
color: modelData === root.currentSize ? Theme.primary : Theme.surfaceContainer
border.color: modelData === root.currentSize ? Theme.primary : Theme.outline
border.width: 1
StyledText {
anchors.centerIn: parent
text: modelData.toString()
font.pixelSize: 8
font.weight: Font.Medium
color: modelData === root.currentSize ? Theme.primaryText : Theme.surfaceText
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
root.currentSize = modelData
root.sizeChanged(modelData)
}
}
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
}
}
}
}
@@ -0,0 +1,46 @@
import QtQuick
import qs.Common
import qs.Widgets
StyledText {
id: root
enum Style {
Title,
Subtitle,
Body,
Caption,
Button
}
property int style: Typography.Style.Body
font.pixelSize: {
switch (style) {
case Typography.Style.Title: return Theme.fontSizeXLarge
case Typography.Style.Subtitle: return Theme.fontSizeLarge
case Typography.Style.Body: return Theme.fontSizeMedium
case Typography.Style.Caption: return Theme.fontSizeSmall
case Typography.Style.Button: return Theme.fontSizeSmall
default: return Theme.fontSizeMedium
}
}
font.weight: {
switch (style) {
case Typography.Style.Title: return Font.Bold
case Typography.Style.Subtitle: return Font.Medium
case Typography.Style.Body: return Font.Normal
case Typography.Style.Caption: return Font.Normal
case Typography.Style.Button: return Font.Medium
default: return Font.Normal
}
}
color: {
switch (style) {
case Typography.Style.Caption: return Theme.surfaceVariantText
default: return Theme.surfaceText
}
}
}
@@ -0,0 +1,230 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Modules.ControlCenter
import qs.Modules.ControlCenter.Widgets
import qs.Modules.ControlCenter.Details
import qs.Modules.DankBar
import qs.Services
import qs.Widgets
import qs.Modules.ControlCenter.Components
import qs.Modules.ControlCenter.Models
import "./utils/state.js" as StateUtils
DankPopout {
id: root
property string expandedSection: ""
property var triggerScreen: null
property bool editMode: false
property int expandedWidgetIndex: -1
property var expandedWidgetData: null
signal lockRequested
function collapseAll() {
expandedSection = ""
expandedWidgetIndex = -1
expandedWidgetData = null
}
onEditModeChanged: {
if (editMode) {
collapseAll()
}
}
onVisibleChanged: {
if (!visible) {
collapseAll()
}
}
readonly property color _containerBg: Theme.surfaceContainerHigh
function setTriggerPosition(x, y, width, section, screen) {
StateUtils.setTriggerPosition(root, x, y, width, section, screen)
}
function openWithSection(section) {
StateUtils.openWithSection(root, section)
}
function toggleSection(section) {
StateUtils.toggleSection(root, section)
}
popupWidth: 550
popupHeight: Math.min((triggerScreen?.height ?? 1080) - 100, contentLoader.item && contentLoader.item.implicitHeight > 0 ? contentLoader.item.implicitHeight + 20 : 400)
triggerX: (triggerScreen?.width ?? 1920) - 600 - Theme.spacingL
triggerY: Theme.barHeight - 4 + SettingsData.dankBarSpacing
triggerWidth: 80
positioning: ""
screen: triggerScreen
shouldBeVisible: false
visible: shouldBeVisible
onShouldBeVisibleChanged: {
if (shouldBeVisible) {
Qt.callLater(() => {
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
})
}
}
WidgetModel {
id: widgetModel
}
content: Component {
Rectangle {
id: controlContent
implicitHeight: mainColumn.implicitHeight + Theme.spacingM
property alias bluetoothCodecSelector: bluetoothCodecSelector
color: {
const transparency = Theme.popupTransparency
const surface = Theme.surfaceContainer || Qt.rgba(0.1, 0.1, 0.1, 1)
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.width: 0
antialiasing: true
smooth: true
Column {
id: mainColumn
width: parent.width - Theme.spacingL * 2
x: Theme.spacingL
y: Theme.spacingL
spacing: Theme.spacingS
HeaderPane {
id: headerPane
width: parent.width
editMode: root.editMode
onEditModeToggled: root.editMode = !root.editMode
onPowerButtonClicked: {
if (powerMenuModalLoader) {
powerMenuModalLoader.active = true
if (powerMenuModalLoader.item) {
const popoutPos = controlContent.mapToItem(null, 0, 0)
const bounds = Qt.rect(popoutPos.x, popoutPos.y, controlContent.width, controlContent.height)
powerMenuModalLoader.item.openFromControlCenter(bounds, root.triggerScreen)
}
}
}
onLockRequested: {
root.close()
root.lockRequested()
}
}
DragDropGrid {
id: widgetGrid
width: parent.width
editMode: root.editMode
expandedSection: root.expandedSection
expandedWidgetIndex: root.expandedWidgetIndex
expandedWidgetData: root.expandedWidgetData
model: widgetModel
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)
onMoveWidget: (fromIndex, toIndex) => widgetModel.moveWidget(fromIndex, toIndex)
onToggleWidgetSize: (index) => widgetModel.toggleWidgetSize(index)
}
EditControls {
width: parent.width
visible: editMode
popoutContent: controlContent
availableWidgets: {
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)
onResetToDefault: () => widgetModel.resetToDefault()
onClearAll: () => widgetModel.clearAll()
}
}
BluetoothCodecSelector {
id: bluetoothCodecSelector
anchors.fill: parent
z: 10000
}
}
}
Component {
id: networkDetailComponent
NetworkDetail {}
}
Component {
id: bluetoothDetailComponent
BluetoothDetail {
id: bluetoothDetail
onShowCodecSelector: function(device) {
if (contentLoader.item && contentLoader.item.bluetoothCodecSelector) {
contentLoader.item.bluetoothCodecSelector.show(device)
contentLoader.item.bluetoothCodecSelector.codecSelected.connect(function(deviceAddress, codecName) {
bluetoothDetail.updateDeviceCodecDisplay(deviceAddress, codecName)
})
}
}
}
}
Component {
id: audioOutputDetailComponent
AudioOutputDetail {}
}
Component {
id: audioInputDetailComponent
AudioInputDetail {}
}
Component {
id: batteryDetailComponent
BatteryDetail {}
}
property var colorPickerModal: null
property var powerMenuModalLoader: null
}
@@ -0,0 +1,201 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Services.Pipewire
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
property bool hasInputVolumeSliderInCC: {
const widgets = SettingsData.controlCenterWidgets || []
return widgets.some(widget => widget.id === "inputVolumeSlider")
}
implicitHeight: headerRow.height + (hasInputVolumeSliderInCC ? 0 : volumeSlider.height) + audioContent.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
Row {
id: headerRow
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.topMargin: Theme.spacingS
height: 40
StyledText {
id: headerText
text: I18n.tr("Input Devices")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
id: volumeSlider
anchors.left: parent.left
anchors.right: parent.right
anchors.top: headerRow.bottom
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.topMargin: Theme.spacingXS
height: 35
spacing: 0
visible: !hasInputVolumeSliderInCC
Rectangle {
width: Theme.iconSize + Theme.spacingS * 2
height: Theme.iconSize + Theme.spacingS * 2
anchors.verticalCenter: parent.verticalCenter
radius: (Theme.iconSize + Theme.spacingS * 2) / 2
color: iconArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
MouseArea {
id: iconArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (AudioService.source && AudioService.source.audio) {
AudioService.source.audio.muted = !AudioService.source.audio.muted
}
}
}
DankIcon {
anchors.centerIn: parent
name: {
if (!AudioService.source || !AudioService.source.audio) return "mic_off"
let muted = AudioService.source.audio.muted
return muted ? "mic_off" : "mic"
}
size: Theme.iconSize
color: AudioService.source && AudioService.source.audio && !AudioService.source.audio.muted && AudioService.source.audio.volume > 0 ? Theme.primary : Theme.surfaceText
}
}
DankSlider {
readonly property real actualVolumePercent: AudioService.source && AudioService.source.audio ? Math.round(AudioService.source.audio.volume * 100) : 0
anchors.verticalCenter: parent.verticalCenter
width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
enabled: AudioService.source && AudioService.source.audio
minimum: 0
maximum: 100
value: AudioService.source && AudioService.source.audio ? Math.min(100, Math.round(AudioService.source.audio.volume * 100)) : 0
showValue: true
unit: "%"
valueOverride: actualVolumePercent
thumbOutlineColor: Theme.surfaceVariant
onSliderValueChanged: function(newValue) {
if (AudioService.source && AudioService.source.audio) {
AudioService.source.audio.volume = newValue / 100
if (newValue > 0 && AudioService.source.audio.muted) {
AudioService.source.audio.muted = false
}
}
}
}
}
DankFlickable {
id: audioContent
anchors.top: hasInputVolumeSliderInCC ? headerRow.bottom : volumeSlider.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: Theme.spacingM
anchors.topMargin: hasInputVolumeSliderInCC ? Theme.spacingM : Theme.spacingS
contentHeight: audioColumn.height
clip: true
Column {
id: audioColumn
width: parent.width
spacing: Theme.spacingS
Repeater {
model: Pipewire.nodes.values.filter(node => {
return node.audio && !node.isSink && !node.isStream
})
delegate: Rectangle {
required property var modelData
required property int index
width: parent.width
height: 50
radius: Theme.cornerRadius
color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.surfaceContainerHighest
border.color: modelData === AudioService.source ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 0
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
spacing: Theme.spacingS
DankIcon {
name: {
if (modelData.name.includes("bluez"))
return "headset"
else if (modelData.name.includes("usb"))
return "headset"
else
return "mic"
}
size: Theme.iconSize - 4
color: modelData === AudioService.source ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.parent.width - parent.parent.anchors.leftMargin - parent.spacing - Theme.iconSize - Theme.spacingM
StyledText {
text: AudioService.displayName(modelData)
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: modelData === AudioService.source ? Font.Medium : Font.Normal
elide: Text.ElideRight
width: parent.width
wrapMode: Text.NoWrap
}
StyledText {
text: modelData === AudioService.source ? "Active" : "Available"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
width: parent.width
wrapMode: Text.NoWrap
}
}
}
MouseArea {
id: deviceMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData) {
Pipewire.preferredDefaultAudioSource = modelData
}
}
}
}
}
}
}
}
@@ -0,0 +1,208 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Services.Pipewire
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
property bool hasVolumeSliderInCC: {
const widgets = SettingsData.controlCenterWidgets || []
return widgets.some(widget => widget.id === "volumeSlider")
}
implicitHeight: headerRow.height + (!hasVolumeSliderInCC ? volumeSlider.height : 0) + audioContent.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
Row {
id: headerRow
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.topMargin: Theme.spacingS
height: 40
StyledText {
id: headerText
text: I18n.tr("Audio Devices")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
id: volumeSlider
anchors.left: parent.left
anchors.right: parent.right
anchors.top: headerRow.bottom
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.topMargin: Theme.spacingXS
height: 35
spacing: 0
visible: !hasVolumeSliderInCC
Rectangle {
width: Theme.iconSize + Theme.spacingS * 2
height: Theme.iconSize + Theme.spacingS * 2
anchors.verticalCenter: parent.verticalCenter
radius: (Theme.iconSize + Theme.spacingS * 2) / 2
color: iconArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
MouseArea {
id: iconArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (AudioService.sink && AudioService.sink.audio) {
AudioService.sink.audio.muted = !AudioService.sink.audio.muted
}
}
}
DankIcon {
anchors.centerIn: parent
name: {
if (!AudioService.sink || !AudioService.sink.audio) return "volume_off"
let muted = AudioService.sink.audio.muted
let volume = AudioService.sink.audio.volume
if (muted || volume === 0.0) return "volume_off"
if (volume <= 0.33) return "volume_down"
if (volume <= 0.66) return "volume_up"
return "volume_up"
}
size: Theme.iconSize
color: AudioService.sink && AudioService.sink.audio && !AudioService.sink.audio.muted && AudioService.sink.audio.volume > 0 ? Theme.primary : Theme.surfaceText
}
}
DankSlider {
readonly property real actualVolumePercent: AudioService.sink && AudioService.sink.audio ? Math.round(AudioService.sink.audio.volume * 100) : 0
anchors.verticalCenter: parent.verticalCenter
width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
enabled: AudioService.sink && AudioService.sink.audio
minimum: 0
maximum: 100
value: AudioService.sink && AudioService.sink.audio ? Math.min(100, Math.round(AudioService.sink.audio.volume * 100)) : 0
showValue: true
unit: "%"
valueOverride: actualVolumePercent
thumbOutlineColor: Theme.surfaceVariant
onSliderValueChanged: function(newValue) {
if (AudioService.sink && AudioService.sink.audio) {
AudioService.sink.audio.volume = newValue / 100
if (newValue > 0 && AudioService.sink.audio.muted) {
AudioService.sink.audio.muted = false
}
AudioService.volumeChanged()
}
}
}
}
DankFlickable {
id: audioContent
anchors.top: volumeSlider.visible ? volumeSlider.bottom : headerRow.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: Theme.spacingM
anchors.topMargin: volumeSlider.visible ? Theme.spacingS : Theme.spacingM
contentHeight: audioColumn.height
clip: true
Column {
id: audioColumn
width: parent.width
spacing: Theme.spacingS
Repeater {
model: Pipewire.nodes.values.filter(node => {
return node.audio && node.isSink && !node.isStream
})
delegate: Rectangle {
required property var modelData
required property int index
width: parent.width
height: 50
radius: Theme.cornerRadius
color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.surfaceContainerHighest
border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 0
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
spacing: Theme.spacingS
DankIcon {
name: {
if (modelData.name.includes("bluez"))
return "headset"
else if (modelData.name.includes("hdmi"))
return "tv"
else if (modelData.name.includes("usb"))
return "headset"
else
return "speaker"
}
size: Theme.iconSize - 4
color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.parent.width - parent.parent.anchors.leftMargin - parent.spacing - Theme.iconSize - Theme.spacingM
StyledText {
text: AudioService.displayName(modelData)
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: modelData === AudioService.sink ? Font.Medium : Font.Normal
elide: Text.ElideRight
width: parent.width
wrapMode: Text.NoWrap
}
StyledText {
text: modelData === AudioService.sink ? "Active" : "Available"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
width: parent.width
wrapMode: Text.NoWrap
}
}
}
MouseArea {
id: deviceMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData) {
Pipewire.preferredDefaultAudioSink = modelData
}
}
}
}
}
}
}
}
@@ -0,0 +1,258 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Services.UPower
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0
function isActiveProfile(profile) {
if (typeof PowerProfiles === "undefined") {
return false
}
return PowerProfiles.profile === profile
}
function setProfile(profile) {
if (typeof PowerProfiles === "undefined") {
ToastService.showError("power-profiles-daemon not available")
return
}
PowerProfiles.profile = profile
if (PowerProfiles.profile !== profile) {
ToastService.showError("Failed to set power profile")
}
}
Column {
id: contentColumn
width: parent.width - Theme.spacingL * 2
anchors.left: parent.left
anchors.top: parent.top
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
Row {
id: headerRow
width: parent.width
height: 48
spacing: Theme.spacingM
DankIcon {
name: BatteryService.getBatteryIcon()
size: Theme.iconSizeLarge
color: {
if (BatteryService.isLowBattery && !BatteryService.isCharging)
return Theme.error
if (BatteryService.isCharging || BatteryService.isPluggedIn)
return Theme.primary
return Theme.surfaceText
}
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
width: parent.width - Theme.iconSizeLarge - Theme.spacingM
Row {
spacing: Theme.spacingS
StyledText {
text: BatteryService.batteryAvailable ? `${BatteryService.batteryLevel}%` : "Power"
font.pixelSize: Theme.fontSizeXLarge
color: {
if (BatteryService.isLowBattery && !BatteryService.isCharging) {
return Theme.error
}
if (BatteryService.isCharging) {
return Theme.primary
}
return Theme.surfaceText
}
font.weight: Font.Bold
}
StyledText {
text: BatteryService.batteryAvailable ? BatteryService.batteryStatus : "Management"
font.pixelSize: Theme.fontSizeLarge
color: {
if (BatteryService.isLowBattery && !BatteryService.isCharging) {
return Theme.error
}
if (BatteryService.isCharging) {
return Theme.primary
}
return Theme.surfaceText
}
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: {
if (!BatteryService.batteryAvailable) return "Power profile management available"
const time = BatteryService.formatTimeRemaining()
if (time !== "Unknown") {
return BatteryService.isCharging ? `Time until full: ${time}` : `Time remaining: ${time}`
}
return ""
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
visible: text.length > 0
elide: Text.ElideRight
width: parent.width
}
}
}
Row {
width: parent.width
spacing: Theme.spacingM
visible: BatteryService.batteryAvailable
StyledRect {
width: (parent.width - Theme.spacingM) / 2
height: 64
radius: Theme.cornerRadius
color: Theme.surfaceContainerHighest
border.width: 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Health")
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: BatteryService.batteryHealth
font.pixelSize: Theme.fontSizeLarge
color: {
if (BatteryService.batteryHealth === "N/A") {
return Theme.surfaceText
}
const healthNum = parseInt(BatteryService.batteryHealth)
return healthNum < 80 ? Theme.error : Theme.surfaceText
}
font.weight: Font.Bold
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
StyledRect {
width: (parent.width - Theme.spacingM) / 2
height: 64
radius: Theme.cornerRadius
color: Theme.surfaceContainerHighest
border.width: 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Capacity")
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: BatteryService.batteryCapacity > 0 ? `${BatteryService.batteryCapacity.toFixed(1)} Wh` : "Unknown"
font.pixelSize: Theme.fontSizeLarge
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: {
if (typeof PowerProfiles === "undefined") return 1
return profileModel.findIndex(profile => isActiveProfile(profile))
}
model: profileModel.map(profile => Theme.getPowerProfileLabel(profile))
currentIndex: currentProfileIndex
selectionMode: "single"
anchors.horizontalCenter: parent.horizontalCenter
onSelectionChanged: (index, selected) => {
if (!selected) return
setProfile(profileModel[index])
}
}
StyledRect {
width: parent.width
height: degradationContent.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
border.color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.3)
border.width: 0
visible: (typeof PowerProfiles !== "undefined") && PowerProfiles.degradationReason !== PerformanceDegradationReason.None
Column {
id: degradationContent
width: parent.width - Theme.spacingL * 2
anchors.left: parent.left
anchors.top: parent.top
anchors.margins: Theme.spacingL
spacing: Theme.spacingS
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "warning"
size: Theme.iconSize
color: Theme.error
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
width: parent.width - Theme.iconSize - Theme.spacingM
StyledText {
text: I18n.tr("Power Profile Degradation")
font.pixelSize: Theme.fontSizeLarge
color: Theme.error
font.weight: Font.Medium
}
StyledText {
text: (typeof PowerProfiles !== "undefined") ? PerformanceDegradationReason.toString(PowerProfiles.degradationReason) : ""
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.8)
wrapMode: Text.WordWrap
width: parent.width
}
}
}
}
}
}
}
@@ -0,0 +1,301 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property var device: null
property bool modalVisible: false
property var parentItem
property var availableCodecs: []
property string currentCodec: ""
property bool isLoading: false
signal codecSelected(string deviceAddress, string codecName)
function show(bluetoothDevice) {
device = bluetoothDevice;
isLoading = true;
availableCodecs = [];
currentCodec = "";
visible = true;
modalVisible = true;
queryCodecs();
Qt.callLater(() => {
focusScope.forceActiveFocus();
});
}
function hide() {
modalVisible = false;
Qt.callLater(() => {
visible = false;
});
}
function queryCodecs() {
if (!device)
return;
BluetoothService.getAvailableCodecs(device, function(codecs, current) {
availableCodecs = codecs;
currentCodec = current;
isLoading = false;
});
}
function selectCodec(profileName) {
if (!device || isLoading)
return;
let selectedCodec = availableCodecs.find(c => c.profile === profileName);
if (selectedCodec && device) {
BluetoothService.updateDeviceCodec(device.address, selectedCodec.name);
codecSelected(device.address, selectedCodec.name);
}
isLoading = true;
BluetoothService.switchCodec(device, profileName, function(success, message) {
isLoading = false;
if (success) {
ToastService.showToast(message, ToastService.levelInfo);
Qt.callLater(root.hide);
} else {
ToastService.showToast(message, ToastService.levelError);
}
});
}
visible: false
anchors.fill: parent
z: 2000
MouseArea {
id: modalBlocker
anchors.fill: parent
visible: modalVisible
enabled: modalVisible
hoverEnabled: true
preventStealing: true
propagateComposedEvents: false
onClicked: root.hide()
onWheel: (wheel) => { wheel.accepted = true }
onPositionChanged: (mouse) => { mouse.accepted = true }
}
Rectangle {
id: modalBackground
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.5)
opacity: modalVisible ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
FocusScope {
id: focusScope
anchors.fill: parent
focus: root.visible
enabled: root.visible
Keys.onEscapePressed: {
root.hide()
event.accepted = true
}
}
Rectangle {
id: modalContent
anchors.centerIn: parent
width: 320
height: contentColumn.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainer
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0
opacity: modalVisible ? 1 : 0
scale: modalVisible ? 1 : 0.9
MouseArea {
anchors.fill: parent
hoverEnabled: true
preventStealing: true
propagateComposedEvents: false
onClicked: (mouse) => { mouse.accepted = true }
onWheel: (wheel) => { wheel.accepted = true }
onPositionChanged: (mouse) => { mouse.accepted = true }
}
Column {
id: contentColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: device ? BluetoothService.getDeviceIcon(device) : "headset"
size: Theme.iconSize + 4
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: device ? (device.name || device.deviceName) : ""
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: I18n.tr("Audio Codec Selection")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
}
}
}
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05)
}
StyledText {
text: isLoading ? "Loading codecs..." : `Current: ${currentCodec}`
font.pixelSize: Theme.fontSizeSmall
color: isLoading ? Theme.primary : Theme.surfaceTextMedium
font.weight: Font.Medium
}
Column {
width: parent.width
spacing: Theme.spacingXS
visible: !isLoading
Repeater {
model: availableCodecs
Rectangle {
width: parent.width
height: 48
radius: Theme.cornerRadius
color: {
if (modelData.name === currentCodec)
return Theme.surfaceContainerHighest;
else if (codecMouseArea.containsMouse)
return Theme.surfaceHover;
else
return "transparent";
}
border.color: "transparent"
border.width: 0
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
Rectangle {
width: 6
height: 6
radius: 3
color: modelData.qualityColor
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: modelData.name
font.pixelSize: Theme.fontSizeMedium
color: modelData.name === currentCodec ? Theme.primary : Theme.surfaceText
font.weight: modelData.name === currentCodec ? Font.Medium : Font.Normal
}
StyledText {
text: modelData.description
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
}
}
}
DankIcon {
name: "check"
size: Theme.iconSize - 4
color: Theme.primary
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
visible: modelData.name === currentCodec
}
MouseArea {
id: codecMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: modelData.name !== currentCodec && !isLoading
onClicked: {
selectCodec(modelData.profile);
}
}
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
@@ -0,0 +1,525 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Bluetooth
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
implicitHeight: BluetoothService.adapter && BluetoothService.adapter.enabled ? headerRow.height + bluetoothContent.height + Theme.spacingM : headerRow.height
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0
property var bluetoothCodecModalRef: null
signal showCodecSelector(var device)
function updateDeviceCodecDisplay(deviceAddress, codecName) {
for (let i = 0; i < pairedRepeater.count; i++) {
let item = pairedRepeater.itemAt(i)
if (item && item.modelData && item.modelData.address === deviceAddress) {
item.currentCodec = codecName
break
}
}
}
Row {
id: headerRow
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.topMargin: Theme.spacingS
height: 40
StyledText {
id: headerText
text: I18n.tr("Bluetooth Settings")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: Math.max(0, parent.width - headerText.implicitWidth - scanButton.width - Theme.spacingM)
height: parent.height
}
Rectangle {
id: scanButton
width: 100
height: 36
radius: 18
color: {
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled)
return Theme.surfaceContainerHigh
return scanMouseArea.containsMouse ? Theme.surfaceContainerHigh : "transparent"
}
border.color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 0
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop" : "bluetooth_searching"
size: 18
color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: BluetoothService.adapter && BluetoothService.adapter.discovering ? "Scanning" : "Scan"
color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: scanMouseArea
anchors.fill: parent
hoverEnabled: true
enabled: BluetoothService.adapter && BluetoothService.adapter.enabled
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
if (BluetoothService.adapter)
BluetoothService.adapter.discovering = !BluetoothService.adapter.discovering
}
}
}
}
DankFlickable {
id: bluetoothContent
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: BluetoothService.adapter && BluetoothService.adapter.enabled
contentHeight: bluetoothColumn.height
clip: true
Column {
id: bluetoothColumn
width: parent.width
spacing: Theme.spacingS
Repeater {
id: pairedRepeater
model: {
if (!BluetoothService.adapter || !BluetoothService.adapter.devices)
return []
let devices = [...BluetoothService.adapter.devices.values.filter(dev => dev && (dev.paired || dev.trusted))]
devices.sort((a, b) => {
if (a.connected && !b.connected) return -1
if (!a.connected && b.connected) return 1
return (b.signalStrength || 0) - (a.signalStrength || 0)
})
return devices
}
delegate: Rectangle {
required property var modelData
required property int index
property string currentCodec: BluetoothService.deviceCodecs[modelData.address] || ""
width: parent.width
height: 50
radius: Theme.cornerRadius
Component.onCompleted: {
if (modelData.connected && BluetoothService.isAudioDevice(modelData)) {
BluetoothService.refreshDeviceCodec(modelData)
}
}
color: {
if (modelData.state === BluetoothDeviceState.Connecting)
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12)
if (deviceMouseArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
return Theme.surfaceContainerHighest
}
border.color: {
if (modelData.state === BluetoothDeviceState.Connecting)
return Theme.warning
if (modelData.connected)
return Theme.primary
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
}
border.width: 0
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
spacing: Theme.spacingS
DankIcon {
name: BluetoothService.getDeviceIcon(modelData)
size: Theme.iconSize - 4
color: {
if (modelData.state === BluetoothDeviceState.Connecting)
return Theme.warning
if (modelData.connected)
return Theme.primary
return Theme.surfaceText
}
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: 200
StyledText {
text: modelData.name || modelData.deviceName || "Unknown Device"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: modelData.connected ? Font.Medium : Font.Normal
elide: Text.ElideRight
width: parent.width
}
Row {
spacing: Theme.spacingXS
StyledText {
text: {
if (modelData.state === BluetoothDeviceState.Connecting)
return "Connecting..."
if (modelData.connected) {
let status = "Connected"
if (currentCodec) {
status += " • " + currentCodec
}
return status
}
return "Paired"
}
font.pixelSize: Theme.fontSizeSmall
color: {
if (modelData.state === BluetoothDeviceState.Connecting)
return Theme.warning
return Theme.surfaceVariantText
}
}
StyledText {
text: {
if (modelData.batteryAvailable && modelData.battery > 0)
return "• " + Math.round(modelData.battery * 100) + "%"
var btBattery = BatteryService.bluetoothDevices.find(dev => {
return dev.name === (modelData.name || modelData.deviceName) ||
dev.name.toLowerCase().includes((modelData.name || modelData.deviceName).toLowerCase()) ||
(modelData.name || modelData.deviceName).toLowerCase().includes(dev.name.toLowerCase())
})
return btBattery ? "• " + btBattery.percentage + "%" : ""
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
visible: text.length > 0
}
StyledText {
text: modelData.signalStrength !== undefined && modelData.signalStrength > 0 ? "• " + modelData.signalStrength + "%" : ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
visible: text.length > 0
}
}
}
}
DankActionButton {
id: pairedOptionsButton
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
iconName: "more_horiz"
buttonSize: 28
onClicked: {
if (bluetoothContextMenu.visible) {
bluetoothContextMenu.close()
} else {
bluetoothContextMenu.currentDevice = modelData
bluetoothContextMenu.popup(pairedOptionsButton, -bluetoothContextMenu.width + pairedOptionsButton.width, pairedOptionsButton.height + Theme.spacingXS)
}
}
}
MouseArea {
id: deviceMouseArea
anchors.fill: parent
anchors.rightMargin: pairedOptionsButton.width + Theme.spacingS
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData.connected) {
modelData.disconnect()
} else {
BluetoothService.connectDeviceWithTrust(modelData)
}
}
}
}
}
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
visible: pairedRepeater.count > 0 && availableRepeater.count > 0
}
Item {
width: parent.width
height: 80
visible: BluetoothService.adapter && BluetoothService.adapter.discovering && availableRepeater.count === 0
DankIcon {
anchors.centerIn: parent
name: "sync"
size: 24
color: Qt.rgba(Theme.surfaceText.r || 0.8, Theme.surfaceText.g || 0.8, Theme.surfaceText.b || 0.8, 0.4)
RotationAnimation on rotation {
running: parent.visible && BluetoothService.adapter && BluetoothService.adapter.discovering && availableRepeater.count === 0
loops: Animation.Infinite
from: 0
to: 360
duration: 1500
}
}
}
Repeater {
id: availableRepeater
model: {
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices)
return []
var filtered = Bluetooth.devices.values.filter(dev => {
return dev && !dev.paired && !dev.pairing && !dev.blocked &&
(dev.signalStrength === undefined || dev.signalStrength > 0)
})
return BluetoothService.sortDevices(filtered)
}
delegate: Rectangle {
required property var modelData
required property int index
property bool canConnect: BluetoothService.canConnect(modelData)
property bool isBusy: BluetoothService.isDeviceBusy(modelData)
width: parent.width
height: 50
radius: Theme.cornerRadius
color: availableMouseArea.containsMouse && !isBusy ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.surfaceContainerHighest
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 0
opacity: canConnect ? 1 : 0.6
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
spacing: Theme.spacingS
DankIcon {
name: BluetoothService.getDeviceIcon(modelData)
size: Theme.iconSize - 4
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: 200
StyledText {
text: modelData.name || modelData.deviceName || "Unknown Device"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
elide: Text.ElideRight
width: parent.width
}
Row {
spacing: Theme.spacingXS
StyledText {
text: {
if (modelData.pairing) return "Pairing..."
if (modelData.blocked) return "Blocked"
return BluetoothService.getSignalStrength(modelData)
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: modelData.signalStrength !== undefined && modelData.signalStrength > 0 ? "• " + modelData.signalStrength + "%" : ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
visible: text.length > 0 && !modelData.pairing && !modelData.blocked
}
}
}
}
StyledText {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
text: {
if (modelData.pairing) return "Pairing..."
if (!canConnect) return "Cannot pair"
return "Pair"
}
font.pixelSize: Theme.fontSizeSmall
color: canConnect ? Theme.primary : Theme.surfaceVariantText
font.weight: Font.Medium
}
MouseArea {
id: availableMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: canConnect && !isBusy ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: canConnect && !isBusy
onClicked: {
if (modelData) {
BluetoothService.connectDeviceWithTrust(modelData)
}
}
}
}
}
Item {
width: parent.width
height: 60
visible: !BluetoothService.adapter
StyledText {
anchors.centerIn: parent
text: I18n.tr("No Bluetooth adapter found")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
}
}
}
}
Menu {
id: bluetoothContextMenu
width: 150
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
property var currentDevice: null
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: bluetoothContextMenu.currentDevice && bluetoothContextMenu.currentDevice.connected ? "Disconnect" : "Connect"
height: 32
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 (bluetoothContextMenu.currentDevice) {
if (bluetoothContextMenu.currentDevice.connected) {
bluetoothContextMenu.currentDevice.disconnect()
} else {
BluetoothService.connectDeviceWithTrust(bluetoothContextMenu.currentDevice)
}
}
}
}
MenuItem {
text: I18n.tr("Audio Codec")
height: bluetoothContextMenu.currentDevice && BluetoothService.isAudioDevice(bluetoothContextMenu.currentDevice) && bluetoothContextMenu.currentDevice.connected ? 32 : 0
visible: bluetoothContextMenu.currentDevice && BluetoothService.isAudioDevice(bluetoothContextMenu.currentDevice) && bluetoothContextMenu.currentDevice.connected
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 (bluetoothContextMenu.currentDevice) {
showCodecSelector(bluetoothContextMenu.currentDevice)
}
}
}
MenuItem {
text: I18n.tr("Forget Device")
height: 32
contentItem: StyledText {
text: parent.text
font.pixelSize: Theme.fontSizeSmall
color: Theme.error
leftPadding: Theme.spacingS
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: parent.hovered ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08) : "transparent"
radius: Theme.cornerRadius / 2
}
onTriggered: {
if (bluetoothContextMenu.currentDevice) {
bluetoothContextMenu.currentDevice.forget()
}
}
}
}
}
@@ -0,0 +1,166 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
property string currentMountPath: "/"
property string instanceId: ""
signal mountPathChanged(string newMountPath)
implicitHeight: diskContent.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
Component.onCompleted: {
DgopService.addRef(["diskmounts"])
}
Component.onDestruction: {
DgopService.removeRef(["diskmounts"])
}
DankFlickable {
id: diskContent
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: diskColumn.height
clip: true
Column {
id: diskColumn
width: parent.width
spacing: Theme.spacingS
Item {
width: parent.width
height: 100
visible: !DgopService.dgopAvailable || !DgopService.diskMounts || DgopService.diskMounts.length === 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingM
DankIcon {
anchors.horizontalCenter: parent.horizontalCenter
name: DgopService.dgopAvailable ? "storage" : "error"
size: 32
color: DgopService.dgopAvailable ? Theme.primary : Theme.error
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: DgopService.dgopAvailable ? "No disk data available" : "dgop not available"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
horizontalAlignment: Text.AlignHCenter
}
}
}
Repeater {
model: DgopService.diskMounts || []
delegate: Rectangle {
required property var modelData
required property int index
width: parent.width
height: 80
radius: Theme.cornerRadius
color: Theme.surfaceContainerHighest
border.color: modelData.mount === currentMountPath ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: modelData.mount === currentMountPath ? 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: "storage"
size: Theme.iconSize
color: {
const percentStr = modelData.percent?.replace("%", "") || "0"
const percent = parseFloat(percentStr) || 0
if (percent > 90) return Theme.error
if (percent > 75) return Theme.warning
return modelData.mount === currentMountPath ? Theme.primary : Theme.surfaceText
}
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: {
const percentStr = modelData.percent?.replace("%", "") || "0"
const percent = parseFloat(percentStr) || 0
return percent.toFixed(0) + "%"
}
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: modelData.mount === "/" ? "Root Filesystem" : modelData.mount
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: modelData.mount === currentMountPath ? Font.Medium : Font.Normal
elide: Text.ElideRight
width: parent.width
}
StyledText {
text: modelData.mount
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
width: parent.width
visible: modelData.mount !== "/"
}
StyledText {
text: `${modelData.used || "?"} / ${modelData.size || "?"}`
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
width: parent.width
}
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
currentMountPath = modelData.mount
mountPathChanged(modelData.mount)
}
}
}
}
}
}
}
@@ -0,0 +1,454 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modals
Rectangle {
implicitHeight: {
if (NetworkService.wifiToggling) {
return headerRow.height + wifiToggleContent.height + Theme.spacingM
}
if (NetworkService.wifiEnabled) {
return headerRow.height + wifiContent.height + Theme.spacingM
}
return headerRow.height + wifiOffContent.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
Component.onCompleted: {
NetworkService.addRef()
}
Component.onDestruction: {
NetworkService.removeRef()
}
Row {
id: headerRow
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.topMargin: Theme.spacingS
height: 40
StyledText {
id: headerText
text: I18n.tr("Network Settings")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
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
}
model: ["Ethernet", "WiFi"]
currentIndex: currentPreferenceIndex
selectionMode: "single"
onSelectionChanged: (index, selected) => {
if (!selected) return
console.log("NetworkDetail: Setting preference to", index === 0 ? "ethernet" : "wifi")
NetworkService.setNetworkPreference(index === 0 ? "ethernet" : "wifi")
}
}
}
Item {
id: wifiToggleContent
anchors.top: headerRow.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacingM
anchors.topMargin: Theme.spacingM
visible: NetworkService.wifiToggling
height: visible ? 80 : 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingM
DankIcon {
anchors.horizontalCenter: parent.horizontalCenter
name: "sync"
size: 32
color: Theme.primary
RotationAnimation on rotation {
running: NetworkService.wifiToggling
loops: Animation.Infinite
from: 0
to: 360
duration: 1000
}
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: NetworkService.wifiEnabled ? "Disabling WiFi..." : "Enabling WiFi..."
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
horizontalAlignment: Text.AlignHCenter
}
}
}
Item {
id: wifiOffContent
anchors.top: headerRow.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacingM
anchors.topMargin: Theme.spacingM
visible: !NetworkService.wifiEnabled && !NetworkService.wifiToggling
height: visible ? 120 : 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingL
width: parent.width
DankIcon {
anchors.horizontalCenter: parent.horizontalCenter
name: "wifi_off"
size: 48
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: I18n.tr("WiFi is off")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
horizontalAlignment: Text.AlignHCenter
}
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
width: 120
height: 36
radius: 18
color: enableWifiButton.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
border.width: 0
border.color: Theme.primary
StyledText {
anchors.centerIn: parent
text: I18n.tr("Enable WiFi")
color: Theme.primary
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
}
MouseArea {
id: enableWifiButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: NetworkService.toggleWifiRadio()
}
}
}
}
DankFlickable {
id: wifiContent
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: NetworkService.wifiInterface && NetworkService.wifiEnabled && !NetworkService.wifiToggling
contentHeight: wifiColumn.height
clip: true
Column {
id: wifiColumn
width: parent.width
spacing: Theme.spacingS
Item {
width: parent.width
height: 200
visible: NetworkService.wifiInterface && NetworkService.wifiNetworks?.length < 1 && !NetworkService.wifiToggling && NetworkService.isScanning
DankIcon {
anchors.centerIn: parent
name: "refresh"
size: 48
color: Qt.rgba(Theme.surfaceText.r || 0.8, Theme.surfaceText.g || 0.8, Theme.surfaceText.b || 0.8, 0.3)
RotationAnimation on rotation {
running: NetworkService.isScanning
loops: Animation.Infinite
from: 0
to: 360
duration: 1000
}
}
}
Repeater {
model: sortedNetworks
property var sortedNetworks: {
const ssid = NetworkService.currentWifiSSID
const networks = NetworkService.wifiNetworks
let sorted = [...networks]
sorted.sort((a, b) => {
if (a.ssid === ssid) return -1
if (b.ssid === ssid) return 1
return b.signal - a.signal
})
return sorted
}
delegate: Rectangle {
required property var modelData
required property int index
width: parent.width
height: 50
radius: Theme.cornerRadius
color: networkMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.surfaceContainerHighest
border.color: modelData.ssid === NetworkService.currentWifiSSID ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 0
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
spacing: Theme.spacingS
DankIcon {
name: {
let strength = modelData.signal || 0
if (strength >= 50) return "wifi"
if (strength >= 25) return "wifi_2_bar"
return "wifi_1_bar"
}
size: Theme.iconSize - 4
color: modelData.ssid === NetworkService.currentWifiSSID ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: 200
StyledText {
text: modelData.ssid || "Unknown Network"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: modelData.ssid === NetworkService.currentWifiSSID ? Font.Medium : Font.Normal
elide: Text.ElideRight
width: parent.width
}
Row {
spacing: Theme.spacingXS
StyledText {
text: modelData.ssid === NetworkService.currentWifiSSID ? "Connected" : (modelData.secured ? "Secured" : "Open")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: modelData.saved ? "• Saved" : ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
visible: text.length > 0
}
StyledText {
text: "• " + modelData.signal + "%"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
}
DankActionButton {
id: optionsButton
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
iconName: "more_horiz"
buttonSize: 28
onClicked: {
if (networkContextMenu.visible) {
networkContextMenu.close()
} else {
networkContextMenu.currentSSID = modelData.ssid
networkContextMenu.currentSecured = modelData.secured
networkContextMenu.currentConnected = modelData.ssid === NetworkService.currentWifiSSID
networkContextMenu.currentSaved = modelData.saved
networkContextMenu.currentSignal = modelData.signal
networkContextMenu.popup(optionsButton, -networkContextMenu.width + optionsButton.width, optionsButton.height + Theme.spacingXS)
}
}
}
MouseArea {
id: networkMouseArea
anchors.fill: parent
anchors.rightMargin: optionsButton.width + Theme.spacingS
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: function(event) {
if (modelData.ssid !== NetworkService.currentWifiSSID) {
if (modelData.secured && !modelData.saved) {
wifiPasswordModal.show(modelData.ssid)
} else {
NetworkService.connectToWifi(modelData.ssid)
}
}
event.accepted = true
}
}
}
}
}
}
Menu {
id: networkContextMenu
width: 150
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
property string currentSSID: ""
property bool currentSecured: false
property bool currentConnected: false
property bool currentSaved: false
property int currentSignal: 0
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: networkContextMenu.currentConnected ? "Disconnect" : "Connect"
height: 32
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.disconnectWifi()
} else {
if (networkContextMenu.currentSecured && !networkContextMenu.currentSaved) {
wifiPasswordModal.show(networkContextMenu.currentSSID)
} else {
NetworkService.connectToWifi(networkContextMenu.currentSSID)
}
}
}
}
MenuItem {
text: I18n.tr("Network Info")
height: 32
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.getNetworkInfo(networkContextMenu.currentSSID)
networkInfoModal.showNetworkInfo(networkContextMenu.currentSSID, networkData)
}
}
MenuItem {
text: I18n.tr("Forget Network")
height: networkContextMenu.currentSaved || networkContextMenu.currentConnected ? 32 : 0
visible: networkContextMenu.currentSaved || networkContextMenu.currentConnected
contentItem: StyledText {
text: parent.text
font.pixelSize: Theme.fontSizeSmall
color: Theme.error
leftPadding: Theme.spacingS
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: parent.hovered ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08) : "transparent"
radius: Theme.cornerRadius / 2
}
onTriggered: {
NetworkService.forgetWifiNetwork(networkContextMenu.currentSSID)
}
}
}
WifiPasswordModal {
id: wifiPasswordModal
}
NetworkInfoModal {
id: networkInfoModal
}
}
@@ -0,0 +1,222 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Modules.ControlCenter.BuiltinPlugins
import "../utils/widgets.js" as WidgetUtils
QtObject {
id: root
property var vpnBuiltinInstance: null
property var vpnLoader: Loader {
active: false
sourceComponent: Component {
VpnWidget {}
}
onItemChanged: {
root.vpnBuiltinInstance = item
}
Connections {
target: SettingsData
function onControlCenterWidgetsChanged() {
const widgets = SettingsData.controlCenterWidgets || []
const hasVpnWidget = widgets.some(w => w.id === "builtin_vpn")
if (!hasVpnWidget && vpnLoader.active) {
console.log("VpnWidget: No VPN widget in control center, deactivating loader")
vpnLoader.active = false
}
}
}
}
readonly property var coreWidgetDefinitions: [{
"id": "nightMode",
"text": "Night Mode",
"description": "Blue light filter",
"icon": "nightlight",
"type": "toggle",
"enabled": DisplayService.automationAvailable,
"warning": !DisplayService.automationAvailable ? "Requires night mode support" : undefined
}, {
"id": "darkMode",
"text": "Dark Mode",
"description": "System theme toggle",
"icon": "contrast",
"type": "toggle",
"enabled": true
}, {
"id": "doNotDisturb",
"text": "Do Not Disturb",
"description": "Block notifications",
"icon": "do_not_disturb_on",
"type": "toggle",
"enabled": true
}, {
"id": "idleInhibitor",
"text": "Keep Awake",
"description": "Prevent screen timeout",
"icon": "motion_sensor_active",
"type": "toggle",
"enabled": true
}, {
"id": "wifi",
"text": "Network",
"description": "Wi-Fi and Ethernet connection",
"icon": "wifi",
"type": "connection",
"enabled": NetworkService.wifiAvailable,
"warning": !NetworkService.wifiAvailable ? "Wi-Fi not available" : undefined
}, {
"id": "bluetooth",
"text": "Bluetooth",
"description": "Device connections",
"icon": "bluetooth",
"type": "connection",
"enabled": BluetoothService.available,
"warning": !BluetoothService.available ? "Bluetooth not available" : undefined
}, {
"id": "audioOutput",
"text": "Audio Output",
"description": "Speaker settings",
"icon": "volume_up",
"type": "connection",
"enabled": true
}, {
"id": "audioInput",
"text": "Audio Input",
"description": "Microphone settings",
"icon": "mic",
"type": "connection",
"enabled": true
}, {
"id": "volumeSlider",
"text": "Volume Slider",
"description": "Audio volume control",
"icon": "volume_up",
"type": "slider",
"enabled": true
}, {
"id": "brightnessSlider",
"text": "Brightness Slider",
"description": "Display brightness control",
"icon": "brightness_6",
"type": "slider",
"enabled": DisplayService.brightnessAvailable,
"warning": !DisplayService.brightnessAvailable ? "Brightness control not available" : undefined
}, {
"id": "inputVolumeSlider",
"text": "Input Volume Slider",
"description": "Microphone volume control",
"icon": "mic",
"type": "slider",
"enabled": true
}, {
"id": "battery",
"text": "Battery",
"description": "Battery and power management",
"icon": "battery_std",
"type": "action",
"enabled": true
}, {
"id": "diskUsage",
"text": "Disk Usage",
"description": "Filesystem usage monitoring",
"icon": "storage",
"type": "action",
"enabled": DgopService.dgopAvailable,
"warning": !DgopService.dgopAvailable ? "Requires 'dgop' tool" : undefined,
"allowMultiple": true
}, {
"id": "colorPicker",
"text": "Color Picker",
"description": "Choose colors from palette",
"icon": "palette",
"type": "action",
"enabled": true
}, {
"id": "builtin_vpn",
"text": "VPN",
"description": "VPN connections",
"icon": "vpn_key",
"type": "builtin_plugin",
"enabled": VpnService.available,
"warning": !VpnService.available ? "VPN not available" : undefined,
"isBuiltinPlugin": true
}]
function getPluginWidgets() {
const plugins = []
const loadedPlugins = PluginService.getLoadedPlugins()
for (var i = 0; i < loadedPlugins.length; i++) {
const plugin = loadedPlugins[i]
if (plugin.type === "daemon") {
continue
}
const pluginComponent = PluginService.pluginWidgetComponents[plugin.id]
if (!pluginComponent) {
continue
}
const tempInstance = pluginComponent.createObject(null)
if (!tempInstance) {
continue
}
const hasCCWidget = tempInstance.ccWidgetIcon && tempInstance.ccWidgetIcon.length > 0
tempInstance.destroy()
if (!hasCCWidget) {
continue
}
plugins.push({
"id": "plugin_" + plugin.id,
"pluginId": plugin.id,
"text": plugin.name || "Plugin",
"description": plugin.description || "",
"icon": plugin.icon || "extension",
"type": "plugin",
"enabled": true,
"isPlugin": true
})
}
return plugins
}
readonly property var baseWidgetDefinitions: coreWidgetDefinitions
function getWidgetForId(widgetId) {
return WidgetUtils.getWidgetForId(baseWidgetDefinitions, widgetId)
}
function addWidget(widgetId) {
WidgetUtils.addWidget(widgetId)
}
function removeWidget(index) {
WidgetUtils.removeWidget(index)
}
function toggleWidgetSize(index) {
WidgetUtils.toggleWidgetSize(index)
}
function moveWidget(fromIndex, toIndex) {
WidgetUtils.moveWidget(fromIndex, toIndex)
}
function resetToDefault() {
WidgetUtils.resetToDefault()
}
function clearAll() {
WidgetUtils.clearAll()
}
}
+316
View File
@@ -0,0 +1,316 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Widgets
PanelWindow {
id: root
readonly property string powerOptionsText: I18n.tr("Power Options")
readonly property string logOutText: I18n.tr("Log Out")
readonly property string suspendText: I18n.tr("Suspend")
readonly property string rebootText: I18n.tr("Reboot")
readonly property string powerOffText: I18n.tr("Power Off")
property bool powerMenuVisible: false
signal powerActionRequested(string action, string title, string message)
visible: powerMenuVisible
implicitWidth: 400
implicitHeight: 320
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
MouseArea {
anchors.fill: parent
onClicked: {
powerMenuVisible = false
}
}
Rectangle {
width: Math.min(320, parent.width - Theme.spacingL * 2)
height: 320 // Fixed height to prevent cropping
x: Math.max(Theme.spacingL, parent.width - width - Theme.spacingL)
y: Theme.barHeight + Theme.spacingXS
color: Theme.popupBackground()
radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 0
opacity: powerMenuVisible ? 1 : 0
scale: powerMenuVisible ? 1 : 0.85
MouseArea {
anchors.fill: parent
onClicked: {
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
StyledText {
text: root.powerOptionsText
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: {
powerMenuVisible = false
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: logoutArea.containsMouse ? Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.08) : Qt.rgba(
Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
0.08)
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: root.logOutText
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: {
powerMenuVisible = false
root.powerActionRequested(
"logout", "Log Out",
"Are you sure you want to log out?")
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: suspendArea.containsMouse ? Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.08) : Qt.rgba(
Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
0.08)
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: root.suspendText
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: {
powerMenuVisible = false
root.powerActionRequested(
"suspend", "Suspend",
"Are you sure you want to suspend the system?")
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: rebootArea.containsMouse ? Qt.rgba(Theme.warning.r,
Theme.warning.g,
Theme.warning.b,
0.08) : Qt.rgba(
Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
0.08)
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: root.rebootText
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: {
powerMenuVisible = false
root.powerActionRequested(
"reboot", "Reboot",
"Are you sure you want to reboot the system?")
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: powerOffArea.containsMouse ? Qt.rgba(Theme.error.r,
Theme.error.g,
Theme.error.b,
0.08) : Qt.rgba(
Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
0.08)
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: root.powerOffText
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: {
powerMenuVisible = false
root.powerActionRequested(
"poweroff", "Power Off",
"Are you sure you want to power off the system?")
}
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
@@ -0,0 +1,63 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Services.Pipewire
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.ControlCenter.Widgets
CompoundPill {
id: root
property var defaultSource: AudioService.source
iconName: {
if (!defaultSource) return "mic_off"
let volume = defaultSource.audio.volume
let muted = defaultSource.audio.muted
if (muted || volume === 0.0) return "mic_off"
return "mic"
}
isActive: defaultSource && !defaultSource.audio.muted
primaryText: {
if (!defaultSource) {
return "No input device"
}
return defaultSource.description || "Audio Input"
}
secondaryText: {
if (!defaultSource) {
return "Select device"
}
if (defaultSource.audio.muted) {
return "Muted"
}
return Math.round(defaultSource.audio.volume * 100) + "%"
}
onToggled: {
if (defaultSource && defaultSource.audio) {
defaultSource.audio.muted = !defaultSource.audio.muted
}
}
onWheelEvent: function (wheelEvent) {
if (!defaultSource || !defaultSource.audio) return
let delta = wheelEvent.angleDelta.y
let currentVolume = defaultSource.audio.volume * 100
let newVolume
if (delta > 0)
newVolume = Math.min(100, currentVolume + 5)
else
newVolume = Math.max(0, currentVolume - 5)
defaultSource.audio.muted = false
defaultSource.audio.volume = newVolume / 100
wheelEvent.accepted = true
}
}
@@ -0,0 +1,66 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Services.Pipewire
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.ControlCenter.Widgets
CompoundPill {
id: root
property var defaultSink: AudioService.sink
iconName: {
if (!defaultSink) return "volume_off"
let volume = defaultSink.audio.volume
let muted = defaultSink.audio.muted
if (muted || volume === 0.0) return "volume_off"
if (volume <= 0.33) return "volume_down"
if (volume <= 0.66) return "volume_up"
return "volume_up"
}
isActive: defaultSink && !defaultSink.audio.muted
primaryText: {
if (!defaultSink) {
return "No output device"
}
return defaultSink.description || "Audio Output"
}
secondaryText: {
if (!defaultSink) {
return "Select device"
}
if (defaultSink.audio.muted) {
return "Muted"
}
return Math.round(defaultSink.audio.volume * 100) + "%"
}
onToggled: {
if (defaultSink && defaultSink.audio) {
defaultSink.audio.muted = !defaultSink.audio.muted
}
}
onWheelEvent: function (wheelEvent) {
if (!defaultSink || !defaultSink.audio) return
let delta = wheelEvent.angleDelta.y
let currentVolume = defaultSink.audio.volume * 100
let newVolume
if (delta > 0)
newVolume = Math.min(100, currentVolume + 5)
else
newVolume = Math.max(0, currentVolume - 5)
defaultSink.audio.muted = false
defaultSink.audio.volume = newVolume / 100
AudioService.volumeChanged()
wheelEvent.accepted = true
}
}
@@ -0,0 +1,81 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Services.Pipewire
import qs.Common
import qs.Services
import qs.Widgets
Row {
id: root
property var defaultSink: AudioService.sink
property color sliderTrackColor: "transparent"
height: 40
spacing: 0
Rectangle {
width: Theme.iconSize + Theme.spacingS * 2
height: Theme.iconSize + Theme.spacingS * 2
anchors.verticalCenter: parent.verticalCenter
radius: (Theme.iconSize + Theme.spacingS * 2) / 2
color: iconArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.primary, 0)
MouseArea {
id: iconArea
anchors.fill: parent
visible: defaultSink !== null
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (defaultSink) {
AudioService.suppressOSD = true
defaultSink.audio.muted = !defaultSink.audio.muted
AudioService.suppressOSD = false
}
}
}
DankIcon {
anchors.centerIn: parent
name: {
if (!defaultSink) return "volume_off"
let volume = defaultSink.audio.volume
let muted = defaultSink.audio.muted
if (muted || volume === 0.0) return "volume_off"
if (volume <= 0.33) return "volume_down"
if (volume <= 0.66) return "volume_up"
return "volume_up"
}
size: Theme.iconSize
color: defaultSink && !defaultSink.audio.muted && defaultSink.audio.volume > 0 ? Theme.primary : Theme.surfaceText
}
}
DankSlider {
readonly property real actualVolumePercent: defaultSink ? Math.round(defaultSink.audio.volume * 100) : 0
anchors.verticalCenter: parent.verticalCenter
width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
enabled: defaultSink !== null
minimum: 0
maximum: 100
value: defaultSink ? Math.min(100, Math.round(defaultSink.audio.volume * 100)) : 0
showValue: true
unit: "%"
valueOverride: actualVolumePercent
thumbOutlineColor: Theme.surfaceContainer
trackColor: root.sliderTrackColor.a > 0 ? root.sliderTrackColor : Theme.surfaceContainerHigh
onSliderValueChanged: function(newValue) {
if (defaultSink) {
defaultSink.audio.volume = newValue / 100.0
if (newValue > 0 && defaultSink.audio.muted) {
defaultSink.audio.muted = false
}
}
}
}
}
@@ -0,0 +1,48 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.ControlCenter.Widgets
CompoundPill {
id: root
iconName: BatteryService.getBatteryIcon()
isActive: BatteryService.batteryAvailable && (BatteryService.isCharging || BatteryService.isPluggedIn)
primaryText: {
if (!BatteryService.batteryAvailable) {
return "No battery"
}
return "Battery"
}
secondaryText: {
if (!BatteryService.batteryAvailable) {
return "Not available"
}
if (BatteryService.isCharging) {
return `${BatteryService.batteryLevel}% Charging`
}
if (BatteryService.isPluggedIn) {
return `${BatteryService.batteryLevel}% Plugged in`
}
return `${BatteryService.batteryLevel}%`
}
iconColor: {
if (BatteryService.isLowBattery && !BatteryService.isCharging) {
return Theme.error
}
if (BatteryService.isCharging || BatteryService.isPluggedIn) {
return Theme.primary
}
return Theme.surfaceText
}
onToggled: {
expandClicked()
}
}
@@ -0,0 +1,70 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.ControlCenter.Widgets
CompoundPill {
id: root
property var primaryDevice: {
if (!BluetoothService.adapter || !BluetoothService.adapter.devices) {
return null
}
let devices = [...BluetoothService.adapter.devices.values.filter(dev => dev && (dev.paired || dev.trusted))]
for (let device of devices) {
if (device && device.connected) {
return device
}
}
return null
}
iconName: {
if (!BluetoothService.available) {
return "bluetooth_disabled"
}
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled) {
return "bluetooth_disabled"
}
return "bluetooth"
}
isActive: !!(BluetoothService.available && BluetoothService.adapter && BluetoothService.adapter.enabled)
showExpandArea: BluetoothService.available
primaryText: {
if (!BluetoothService.available) {
return "Bluetooth"
}
if (!BluetoothService.adapter) {
return "No adapter"
}
if (!BluetoothService.adapter.enabled) {
return "Disabled"
}
return "Enabled"
}
secondaryText: {
if (!BluetoothService.available) {
return "No adapters"
}
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled) {
return "Off"
}
if (primaryDevice) {
return primaryDevice.name || primaryDevice.alias || primaryDevice.deviceName || "Connected Device"
}
return "No devices"
}
onToggled: {
if (BluetoothService.available && BluetoothService.adapter) {
BluetoothService.adapter.enabled = !BluetoothService.adapter.enabled
}
}
}
@@ -0,0 +1,162 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
Row {
id: root
height: 40
spacing: 0
Rectangle {
width: Theme.iconSize + Theme.spacingS * 2
height: Theme.iconSize + Theme.spacingS * 2
anchors.verticalCenter: parent.verticalCenter
radius: (Theme.iconSize + Theme.spacingS * 2) / 2
color: iconArea.containsMouse
? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
: Theme.withAlpha(Theme.primary, 0)
MouseArea {
id: iconArea
anchors.fill: parent
hoverEnabled: true
cursorShape: 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
}
}
onEntered: {
tooltipLoader.active = true
if (tooltipLoader.item) {
const tooltipText = DisplayService.currentDevice ? "bl device: " + DisplayService.currentDevice : "Backlight Control"
const p = iconArea.mapToItem(null, iconArea.width / 2, 0)
tooltipLoader.item.show(tooltipText, p.x, p.y - 40, null)
}
}
onExited: {
if (tooltipLoader.item) {
tooltipLoader.item.hide()
}
tooltipLoader.active = false
}
DankIcon {
anchors.centerIn: parent
name: {
if (!DisplayService.brightnessAvailable) return "brightness_low"
let brightness = DisplayService.brightnessLevel
if (brightness <= 33) return "brightness_low"
if (brightness <= 66) return "brightness_medium"
return "brightness_high"
}
size: Theme.iconSize
color: DisplayService.brightnessAvailable && DisplayService.brightnessLevel > 0 ? Theme.primary : Theme.surfaceText
}
}
}
DankSlider {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
enabled: DisplayService.brightnessAvailable
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
}
onSliderValueChanged: function(newValue) {
if (DisplayService.brightnessAvailable) {
DisplayService.setBrightness(newValue)
}
}
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
sourceComponent: DankTooltip {}
}
}
@@ -0,0 +1,33 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.ControlCenter.Widgets
CompoundPill {
id: root
property var colorPickerModal: null
isActive: true
iconName: "palette"
iconColor: Theme.primary
primaryText: "Color Picker"
secondaryText: "Choose a color"
onToggled: {
console.log("ColorPickerPill toggled, modal:", colorPickerModal)
if (colorPickerModal) {
colorPickerModal.show()
}
}
onExpandClicked: {
console.log("ColorPickerPill expandClicked, modal:", colorPickerModal)
if (colorPickerModal) {
colorPickerModal.show()
}
}
}
@@ -0,0 +1,70 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Widgets
Rectangle {
id: root
property string iconName: ""
property color iconColor: Theme.surfaceText
property string labelText: ""
property real value: 0.0
property real maximumValue: 1.0
property real minimumValue: 0.0
property bool enabled: true
signal sliderValueChanged(real value)
width: parent ? parent.width : 200
height: 60
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0
opacity: enabled ? 1.0 : 0.6
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
anchors.right: sliderContainer.left
anchors.rightMargin: Theme.spacingS
spacing: Theme.spacingS
DankIcon {
name: root.iconName
size: Theme.iconSize
color: root.iconColor
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: root.labelText
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
Rectangle {
id: sliderContainer
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: Theme.spacingM
width: 120
height: parent.height - Theme.spacingS * 2
DankSlider {
anchors.centerIn: parent
width: parent.width
enabled: root.enabled
minimum: Math.round(root.minimumValue * 100)
maximum: Math.round(root.maximumValue * 100)
value: Math.round(root.value * 100)
onSliderValueChanged: root.sliderValueChanged(newValue / 100.0)
}
}
}
@@ -0,0 +1,170 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Widgets
Rectangle {
id: root
property string iconName: ""
property color iconColor: Theme.surfaceText
property string primaryText: ""
property string secondaryText: ""
property bool expanded: false
property bool isActive: false
property bool showExpandArea: true
signal toggled()
signal expandClicked()
signal wheelEvent(var wheelEvent)
width: parent ? parent.width : 220
height: 60
radius: Theme.cornerRadius
function hoverTint(base) {
const factor = 1.2
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor)
}
readonly property color _containerBg: Theme.surfaceContainerHigh
color: {
const baseColor = bodyMouse.containsMouse ? Theme.widgetBaseHoverColor : _containerBg
return baseColor
}
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.10)
border.width: 0
antialiasing: true
readonly property color _labelPrimary: Theme.surfaceText
readonly property color _labelSecondary: Theme.surfaceVariantText
readonly property color _tileBgActive: Theme.primary
readonly property color _tileBgInactive: {
const transparency = Theme.popupTransparency
const surface = Theme.surfaceContainer || Qt.rgba(0.1, 0.1, 0.1, 1)
return Qt.rgba(surface.r, surface.g, surface.b, transparency)
}
readonly property color _tileRingActive:
Qt.rgba(Theme.primaryText.r, Theme.primaryText.g, Theme.primaryText.b, 0.22)
readonly property color _tileRingInactive:
Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.18)
readonly property color _tileIconActive: Theme.primaryText
readonly property color _tileIconInactive: Theme.primary
property int _padH: Theme.spacingS
property int _tileSize: 48
property int _tileRadius: Theme.cornerRadius
Rectangle {
id: rightHoverOverlay
anchors.fill: parent
radius: root.radius
z: 0
visible: false
color: hoverTint(_containerBg)
opacity: 0.08
antialiasing: true
Behavior on opacity { NumberAnimation { duration: Theme.shortDuration } }
}
Row {
id: row
anchors.fill: parent
anchors.leftMargin: _padH
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingM
Rectangle {
id: iconTile
z: 1
width: _tileSize
height: _tileSize
anchors.verticalCenter: parent.verticalCenter
radius: _tileRadius
color: isActive ? _tileBgActive : _tileBgInactive
border.color: isActive ? _tileRingActive : "transparent"
border.width: isActive ? 1 : 0
antialiasing: true
Rectangle {
anchors.fill: parent
radius: _tileRadius
color: hoverTint(iconTile.color)
opacity: tileMouse.pressed ? 0.3 : (tileMouse.containsMouse ? 0.2 : 0.0)
visible: opacity > 0
antialiasing: true
Behavior on opacity { NumberAnimation { duration: Theme.shortDuration } }
}
DankIcon {
anchors.centerIn: parent
name: iconName
size: Theme.iconSize
color: isActive ? _tileIconActive : _tileIconInactive
}
MouseArea {
id: tileMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.toggled()
}
}
Item {
id: body
width: row.width - iconTile.width - row.spacing
height: row.height
Column {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
width: parent.width
text: root.primaryText
color: _labelPrimary
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
StyledText {
width: parent.width
text: root.secondaryText
color: _labelSecondary
font.pixelSize: Theme.fontSizeSmall
visible: text.length > 0
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
}
MouseArea {
id: bodyMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: { rightHoverOverlay.visible = true; rightHoverOverlay.opacity = 0.08 }
onExited: { rightHoverOverlay.opacity = 0.0; rightHoverOverlay.visible = false }
onPressed: rightHoverOverlay.opacity = 0.16
onReleased: rightHoverOverlay.opacity = containsMouse ? 0.08 : 0.0
onClicked: root.expandClicked()
onWheel: function (ev) {
root.wheelEvent(ev)
}
}
}
}
focus: true
Keys.onPressed: function (ev) {
if (ev.key === Qt.Key_Space || ev.key === Qt.Key_Return) { root.toggled(); ev.accepted = true }
else if (ev.key === Qt.Key_Right) { root.expandClicked(); ev.accepted = true }
}
}
@@ -0,0 +1,29 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Widgets
Rectangle {
id: root
property string title: ""
property Component content: null
property bool isVisible: true
property int contentHeight: 300
width: parent ? parent.width : 400
implicitHeight: isVisible ? contentHeight : 0
height: implicitHeight
color: "transparent"
clip: true
Loader {
id: contentLoader
anchors.fill: parent
sourceComponent: root.content
asynchronous: true
}
}
@@ -0,0 +1,78 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.ControlCenter.Widgets
CompoundPill {
id: root
property string mountPath: "/"
property string instanceId: ""
iconName: "storage"
property var selectedMount: {
if (!DgopService.diskMounts || DgopService.diskMounts.length === 0) {
return null
}
const targetMount = DgopService.diskMounts.find(mount => mount.mount === mountPath)
return targetMount || DgopService.diskMounts.find(mount => mount.mount === "/") || DgopService.diskMounts[0]
}
property real usagePercent: {
if (!selectedMount || !selectedMount.percent) {
return 0
}
const percentStr = selectedMount.percent.replace("%", "")
return parseFloat(percentStr) || 0
}
isActive: DgopService.dgopAvailable && selectedMount !== null
primaryText: {
if (!DgopService.dgopAvailable) {
return "Disk Usage"
}
if (!selectedMount) {
return "No disk data"
}
return selectedMount.mount
}
secondaryText: {
if (!DgopService.dgopAvailable) {
return "dgop not available"
}
if (!selectedMount) {
return "No disk data available"
}
return `${selectedMount.used} / ${selectedMount.size} (${usagePercent.toFixed(0)}%)`
}
iconColor: {
if (!DgopService.dgopAvailable || !selectedMount) {
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
}
if (usagePercent > 90) {
return Theme.error
}
if (usagePercent > 75) {
return Theme.warning
}
return Theme.surfaceText
}
Component.onCompleted: {
DgopService.addRef(["diskmounts"])
}
Component.onDestruction: {
DgopService.removeRef(["diskmounts"])
}
onToggled: {
expandClicked()
}
}
@@ -0,0 +1,51 @@
import QtQuick
import qs.Common
import qs.Widgets
StyledRect {
id: root
property string primaryMessage: ""
property string secondaryMessage: ""
radius: Theme.cornerRadius
color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.1)
border.color: Theme.warning
border.width: 1
Row {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingXS
DankIcon {
name: "warning"
size: 16
color: Theme.warning
anchors.top: parent.top
anchors.topMargin: 2
}
Column {
width: parent.width - 16 - parent.spacing
spacing: Theme.spacingXS
StyledText {
width: parent.width
text: root.primaryMessage
font.pixelSize: Theme.fontSizeSmall
color: Theme.warning
font.weight: Font.Medium
wrapMode: Text.WordWrap
}
StyledText {
width: parent.width
text: root.secondaryMessage
font.pixelSize: Theme.fontSizeSmall
color: Theme.warning
visible: text.length > 0
}
}
}
}
@@ -0,0 +1,82 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Services.Pipewire
import qs.Common
import qs.Services
import qs.Widgets
Row {
id: root
property var defaultSource: AudioService.source
property color sliderTrackColor: "transparent"
height: 40
spacing: 0
Rectangle {
width: Theme.iconSize + Theme.spacingS * 2
height: Theme.iconSize + Theme.spacingS * 2
anchors.verticalCenter: parent.verticalCenter
radius: (Theme.iconSize + Theme.spacingS * 2) / 2
color: iconArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.primary, 0)
MouseArea {
id: iconArea
anchors.fill: parent
visible: defaultSource !== null
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (defaultSource) {
AudioService.suppressOSD = true
defaultSource.audio.muted = !defaultSource.audio.muted
AudioService.suppressOSD = false
}
}
}
DankIcon {
anchors.centerIn: parent
name: {
if (!defaultSource) return "mic_off"
let volume = defaultSource.audio.volume
let muted = defaultSource.audio.muted
if (muted || volume === 0.0) return "mic_off"
return "mic"
}
size: Theme.iconSize
color: defaultSource && !defaultSource.audio.muted && defaultSource.audio.volume > 0 ? Theme.primary : Theme.surfaceText
}
}
DankSlider {
readonly property real actualVolumePercent: defaultSource ? Math.round(defaultSource.audio.volume * 100) : 0
anchors.verticalCenter: parent.verticalCenter
width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
enabled: defaultSource !== null
minimum: 0
maximum: 100
value: defaultSource ? Math.min(100, Math.round(defaultSource.audio.volume * 100)) : 0
showValue: true
unit: "%"
valueOverride: actualVolumePercent
thumbOutlineColor: Theme.surfaceContainer
trackColor: root.sliderTrackColor.a > 0 ? root.sliderTrackColor : Theme.surfaceContainerHigh
onIsDraggingChanged: {
AudioService.suppressOSD = isDragging
}
onSliderValueChanged: function(newValue) {
if (defaultSource) {
defaultSource.audio.volume = newValue / 100.0
if (newValue > 0 && defaultSource.audio.muted) {
defaultSource.audio.muted = false
}
}
}
}
}
@@ -0,0 +1,100 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
property bool isActive: BatteryService.batteryAvailable && (BatteryService.isCharging || BatteryService.isPluggedIn)
property bool enabled: BatteryService.batteryAvailable
signal clicked()
width: parent ? ((parent.width - parent.spacing * 3) / 4) : 48
height: 48
radius: {
if (Theme.cornerRadius === 0) return 0
return isActive ? Theme.cornerRadius : Theme.cornerRadius + 4
}
function hoverTint(base) {
const factor = 1.2
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor)
}
readonly property color _tileBgActive: Theme.primary
readonly property color _tileBgInactive: Theme.surfaceContainerHigh
readonly property color _tileRingActive:
Qt.rgba(Theme.primaryText.r, Theme.primaryText.g, Theme.primaryText.b, 0.22)
readonly property color _tileIconActive: Theme.primaryText
readonly property color _tileIconInactive: Theme.primary
color: {
if (isActive) return _tileBgActive
const baseColor = mouseArea.containsMouse ? Theme.widgetBaseHoverColor : _tileBgInactive
return baseColor
}
border.color: isActive ? _tileRingActive : "transparent"
border.width: isActive ? 1 : 0
antialiasing: true
opacity: enabled ? 1.0 : 0.6
Rectangle {
anchors.fill: parent
radius: parent.radius
color: hoverTint(root.color)
opacity: mouseArea.pressed ? 0.3 : (mouseArea.containsMouse ? 0.2 : 0.0)
visible: opacity > 0
antialiasing: true
Behavior on opacity { NumberAnimation { duration: Theme.shortDuration } }
}
Row {
anchors.centerIn: parent
spacing: 4
DankIcon {
name: BatteryService.getBatteryIcon()
size: parent.parent.width * 0.25
color: {
if (BatteryService.isLowBattery && !BatteryService.isCharging) {
return Theme.error
}
return isActive ? _tileIconActive : _tileIconInactive
}
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: BatteryService.batteryAvailable ? `${BatteryService.batteryLevel}%` : ""
font.pixelSize: parent.parent.width * 0.15
font.weight: Font.Medium
color: {
if (BatteryService.isLowBattery && !BatteryService.isCharging) {
return Theme.error
}
return isActive ? _tileIconActive : _tileIconInactive
}
anchors.verticalCenter: parent.verticalCenter
visible: BatteryService.batteryAvailable
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: root.enabled
onClicked: root.clicked()
}
Behavior on radius {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
@@ -0,0 +1,80 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Widgets
Rectangle {
id: root
property string iconName: ""
property bool isActive: false
property bool enabled: true
property real iconRotation: 0
signal clicked()
signal iconRotationCompleted()
width: parent ? ((parent.width - parent.spacing * 3) / 4) : 48
height: 48
radius: {
if (Theme.cornerRadius === 0) return 0
return isActive ? Theme.cornerRadius : Theme.cornerRadius + 4
}
function hoverTint(base) {
const factor = 1.2
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor)
}
readonly property color _tileBgActive: Theme.primary
readonly property color _tileBgInactive: Theme.surfaceContainerHigh
readonly property color _tileRingActive:
Qt.rgba(Theme.primaryText.r, Theme.primaryText.g, Theme.primaryText.b, 0.22)
readonly property color _tileIconActive: Theme.primaryText
readonly property color _tileIconInactive: Theme.primary
color: {
if (isActive) return _tileBgActive
const baseColor = mouseArea.containsMouse ? Theme.widgetBaseHoverColor : _tileBgInactive
return baseColor
}
border.color: isActive ? _tileRingActive : "transparent"
border.width: isActive ? 1 : 0
antialiasing: true
opacity: enabled ? 1.0 : 0.6
Rectangle {
anchors.fill: parent
radius: parent.radius
color: hoverTint(root.color)
opacity: mouseArea.pressed ? 0.3 : (mouseArea.containsMouse ? 0.2 : 0.0)
visible: opacity > 0
antialiasing: true
Behavior on opacity { NumberAnimation { duration: Theme.shortDuration } }
}
DankIcon {
anchors.centerIn: parent
name: iconName
size: Theme.iconSize
color: isActive ? _tileIconActive : _tileIconInactive
rotation: iconRotation
onRotationCompleted: root.iconRotationCompleted()
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: root.enabled
onClicked: root.clicked()
}
Behavior on radius {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
@@ -0,0 +1,121 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Widgets
Rectangle {
id: root
property string iconName: ""
property string text: ""
property bool isActive: false
property bool enabled: true
property string secondaryText: ""
property real iconRotation: 0
signal clicked()
signal iconRotationCompleted()
width: parent ? parent.width : 200
height: 60
radius: {
if (Theme.cornerRadius === 0) return 0
return isActive ? Theme.cornerRadius : Theme.cornerRadius + 4
}
readonly property color _tileBgActive: Theme.primary
readonly property color _tileBgInactive: Theme.surfaceContainerHigh
readonly property color _tileRingActive:
Qt.rgba(Theme.primaryText.r, Theme.primaryText.g, Theme.primaryText.b, 0.22)
color: {
if (isActive) return _tileBgActive
const baseColor = mouseArea.containsMouse ? Theme.widgetBaseHoverColor : _tileBgInactive
return baseColor
}
border.color: isActive ? _tileRingActive : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0
opacity: enabled ? 1.0 : 0.6
function hoverTint(base) {
const factor = 1.2
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor)
}
readonly property color _containerBg: Theme.surfaceContainerHigh
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: mouseArea.containsMouse ? hoverTint(_containerBg) : Theme.withAlpha(_containerBg, 0)
opacity: mouseArea.containsMouse ? 0.08 : 0.0
Behavior on opacity {
NumberAnimation { duration: Theme.shortDuration }
}
}
Row {
anchors.fill: parent
anchors.leftMargin: Theme.spacingL + 2
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingM
DankIcon {
name: root.iconName
size: Theme.iconSize
color: isActive ? Theme.primaryText : Theme.primary
anchors.verticalCenter: parent.verticalCenter
rotation: root.iconRotation
onRotationCompleted: root.iconRotationCompleted()
}
Item {
width: parent.width - Theme.iconSize - parent.spacing
height: parent.height
Column {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
width: parent.width
text: root.text
font.pixelSize: Theme.fontSizeMedium
color: isActive ? Theme.primaryText : Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
StyledText {
width: parent.width
text: root.secondaryText
font.pixelSize: Theme.fontSizeSmall
color: isActive ? Theme.primaryText : Theme.surfaceVariantText
visible: text.length > 0
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: root.enabled
onClicked: root.clicked()
}
Behavior on radius {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
+45
View File
@@ -0,0 +1,45 @@
function calculateRowsAndWidgets(controlCenterColumn, expandedSection, expandedWidgetIndex) {
var rows = []
var currentRow = []
var currentWidth = 0
var expandedRow = -1
const widgets = SettingsData.controlCenterWidgets || []
const baseWidth = controlCenterColumn.width
const spacing = Theme.spacingS
for (var i = 0; i < widgets.length; i++) {
const widget = widgets[i]
const widgetWidth = widget.width || 50
var itemWidth
if (widgetWidth <= 25) {
itemWidth = (baseWidth - spacing * 3) / 4
} else if (widgetWidth <= 50) {
itemWidth = (baseWidth - spacing) / 2
} else if (widgetWidth <= 75) {
itemWidth = (baseWidth - spacing * 2) * 0.75
} else {
itemWidth = baseWidth
}
if (currentRow.length > 0 && (currentWidth + spacing + itemWidth > baseWidth)) {
rows.push([...currentRow])
currentRow = [widget]
currentWidth = itemWidth
} else {
currentRow.push(widget)
currentWidth += (currentRow.length > 1 ? spacing : 0) + itemWidth
}
if (expandedWidgetIndex === i) {
expandedRow = rows.length
}
}
if (currentRow.length > 0) {
rows.push(currentRow)
}
return { rows: rows, expandedRowIndex: expandedRow }
}
+25
View File
@@ -0,0 +1,25 @@
function setTriggerPosition(root, x, y, width, section, screen) {
root.triggerX = x
root.triggerY = y
root.triggerWidth = width
root.triggerSection = section
root.triggerScreen = screen
}
function openWithSection(root, section) {
if (root.shouldBeVisible) {
root.close()
} else {
root.expandedSection = section
root.open()
}
}
function toggleSection(root, section) {
if (root.expandedSection === section) {
root.expandedSection = ""
root.expandedWidgetIndex = -1
} else {
root.expandedSection = section
}
}
+85
View File
@@ -0,0 +1,85 @@
function getWidgetForId(baseWidgetDefinitions, widgetId) {
return baseWidgetDefinitions.find(w => w.id === widgetId)
}
function addWidget(widgetId) {
var widgets = SettingsData.controlCenterWidgets.slice()
var widget = {
"id": widgetId,
"enabled": true,
"width": 50
}
if (widgetId === "diskUsage") {
widget.instanceId = generateUniqueId()
widget.mountPath = "/"
}
widgets.push(widget)
SettingsData.setControlCenterWidgets(widgets)
}
function generateUniqueId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2)
}
function removeWidget(index) {
var widgets = SettingsData.controlCenterWidgets.slice()
if (index >= 0 && index < widgets.length) {
widgets.splice(index, 1)
SettingsData.setControlCenterWidgets(widgets)
}
}
function toggleWidgetSize(index) {
var widgets = SettingsData.controlCenterWidgets.slice()
if (index >= 0 && index < widgets.length) {
const currentWidth = widgets[index].width || 50
const id = widgets[index].id || ""
if (id === "wifi" || id === "bluetooth" || id === "audioOutput" || id === "audioInput") {
widgets[index].width = currentWidth <= 50 ? 100 : 50
} else {
if (currentWidth <= 25) {
widgets[index].width = 50
} else if (currentWidth <= 50) {
widgets[index].width = 100
} else {
widgets[index].width = 25
}
}
SettingsData.setControlCenterWidgets(widgets)
}
}
function reorderWidgets(newOrder) {
SettingsData.setControlCenterWidgets(newOrder)
}
function moveWidget(fromIndex, toIndex) {
let widgets = [...(SettingsData.controlCenterWidgets || [])]
if (fromIndex >= 0 && fromIndex < widgets.length && toIndex >= 0 && toIndex < widgets.length) {
const movedWidget = widgets.splice(fromIndex, 1)[0]
widgets.splice(toIndex, 0, movedWidget)
SettingsData.setControlCenterWidgets(widgets)
}
}
function resetToDefault() {
const defaultWidgets = [
{"id": "volumeSlider", "enabled": true, "width": 50},
{"id": "brightnessSlider", "enabled": true, "width": 50},
{"id": "wifi", "enabled": true, "width": 50},
{"id": "bluetooth", "enabled": true, "width": 50},
{"id": "audioOutput", "enabled": true, "width": 50},
{"id": "audioInput", "enabled": true, "width": 50},
{"id": "nightMode", "enabled": true, "width": 50},
{"id": "darkMode", "enabled": true, "width": 50}
]
SettingsData.setControlCenterWidgets(defaultWidgets)
}
function clearAll() {
SettingsData.setControlCenterWidgets([])
}

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