1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-06 05:25:41 -05:00

Compare commits

...

183 Commits

Author SHA1 Message Date
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
269 changed files with 33246 additions and 10131 deletions

View File

@@ -1,59 +1,97 @@
name: Create Release
# Release from a dispatch event from the danklinux repo
name: Create Release from DMS
on:
push:
tags:
- 'v*'
repository_dispatch:
types: [dms_release]
permissions:
contents: write
concurrency:
group: release-${{ github.ref_name }}
group: release-${{ github.event.client_payload.tag }}
cancel-in-progress: true
jobs:
create_release:
name: 📦 Create GitHub Release
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 # Fetch full history for changelog generation
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
# Generate changelog
- name: Generate Changelog
id: changelog
run: |
# Get the previous tag
PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
set -e
PREVIOUS_TAG=$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || echo "")
if [ -z "$PREVIOUS_TAG" ]; then
echo "No previous tag found, using all commits"
CHANGELOG=$(git log --oneline --pretty=format:"- %s (%h)" | head -50)
else
echo "Generating changelog from $PREVIOUS_TAG to HEAD"
CHANGELOG=$(git log --oneline --pretty=format:"- %s (%h)" $PREVIOUS_TAG..HEAD)
CHANGELOG=$(git log --oneline --pretty=format:"- %s (%h)" "${PREVIOUS_TAG}..${TAG}")
fi
# Create the changelog with proper formatting
cat > CHANGELOG.md << EOF
## What's Changed
$CHANGELOG
**Full Changelog**: https://github.com/${{ github.repository }}/compare/${PREVIOUS_TAG}...${{ github.ref_name }}
**Full Changelog**: https://github.com/${{ github.repository }}/compare/${PREVIOUS_TAG}...${TAG}
EOF
# Set output for use in release step
echo "changelog<<EOF" >> $GITHUB_OUTPUT
cat CHANGELOG.md >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
# Create GitHub Release
- name: Create GitHub Release
uses: comnoco/create-release-action@v2.0.5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create/Update DankMaterialShell Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
release_name: Release ${{ github.ref_name }}
tag_name: ${{ env.TAG }}
name: Release ${{ env.TAG }}
body: ${{ steps.changelog.outputs.changelog }}
draft: false
prerelease: ${{ contains(github.ref_name, '-') }}
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 }}

233
CLAUDE.md
View File

@@ -63,6 +63,9 @@ 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
@@ -89,6 +92,7 @@ shell.qml # Main entry point (minimal orchestration)
│ ├── DisplayService.qml
│ ├── NotificationService.qml
│ ├── WeatherService.qml
│ ├── PluginService.qml
│ └── [14 more services]
├── Modules/ # UI components (93 files)
│ ├── TopBar/ # Panel components (13 files)
@@ -104,15 +108,21 @@ shell.qml # Main entry point (minimal orchestration)
│ ├── 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]
── 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
@@ -163,6 +173,12 @@ shell.qml # Main entry point (minimal orchestration)
- **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**:
@@ -408,10 +424,10 @@ When modifying the shell:
Singleton {
id: root
property bool featureAvailable: false
property type currentValue: defaultValue
function performAction(param) {
// Implementation
}
@@ -422,7 +438,7 @@ When modifying the shell:
```qml
// In module files
property alias serviceValue: NewService.currentValue
SomeControl {
visible: NewService.featureAvailable
enabled: NewService.featureAvailable
@@ -430,6 +446,200 @@ When modifying the shell:
}
```
### 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
@@ -454,6 +664,7 @@ When modifying the shell:
- **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

View File

@@ -1,6 +1,6 @@
# Contributing
Contributions are welcome and encourages.
Contributions are welcome and encouraged.
## Formatting
@@ -27,4 +27,4 @@ Sometimes it just breaks code though. Like turning `"_\""` into `"_""`, so you m
## Pull request
Include screenshots/video if applicable in your pull request if applicable, to visualize what your change is affecting.
Include screenshots/video if applicable in your pull request if applicable, to visualize what your change is affecting.

View File

@@ -72,10 +72,6 @@ Singleton {
saveSettings()
}
function getAppUsageRanking() {
return appUsageRanking
}
function getRankedApps() {
var apps = []
for (var appId in appUsageRanking) {

62
Common/DankSocket.qml Normal file
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
Common/Facts.qml Normal file
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)]
}
}

View File

@@ -12,12 +12,19 @@ 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
@@ -63,14 +70,22 @@ Singleton {
property int batteryHibernateTimeout: 0 // Never
property bool lockBeforeSuspend: false
property var recentColors: []
property bool showThirdPartyPlugins: false
Component.onCompleted: {
loadSettings()
if (!isGreeterMode) {
loadSettings()
}
}
function loadSettings() {
parseSettings(settingsFile.text())
if (isGreeterMode) {
parseSettings(greeterSessionFile.text())
} else {
parseSettings(settingsFile.text())
}
}
function parseSettings(content) {
@@ -83,6 +98,11 @@ Singleton {
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
@@ -132,10 +152,13 @@ Singleton {
batterySuspendTimeout = settings.batterySuspendTimeout !== undefined ? settings.batterySuspendTimeout : 0
batteryHibernateTimeout = settings.batteryHibernateTimeout !== undefined ? settings.batteryHibernateTimeout : 0
lockBeforeSuspend = settings.lockBeforeSuspend !== undefined ? settings.lockBeforeSuspend : false
// Generate system themes but don't override user's theme choice
if (typeof Theme !== "undefined") {
Theme.generateSystemThemesFromCurrentTheme()
recentColors = settings.recentColors !== undefined ? settings.recentColors : []
showThirdPartyPlugins = settings.showThirdPartyPlugins !== undefined ? settings.showThirdPartyPlugins : false
if (!isGreeterMode) {
if (typeof Theme !== "undefined") {
Theme.generateSystemThemesFromCurrentTheme()
}
}
}
} catch (e) {
@@ -144,6 +167,7 @@ Singleton {
}
function saveSettings() {
if (isGreeterMode) return
settingsFile.setText(JSON.stringify({
"isLightMode": isLightMode,
"wallpaperPath": wallpaperPath,
@@ -151,6 +175,11 @@ Singleton {
"profileLastPath": profileLastPath,
"perMonitorWallpaper": perMonitorWallpaper,
"monitorWallpapers": monitorWallpapers,
"perModeWallpaper": perModeWallpaper,
"wallpaperPathLight": wallpaperPathLight,
"wallpaperPathDark": wallpaperPathDark,
"monitorWallpapersLight": monitorWallpapersLight,
"monitorWallpapersDark": monitorWallpapersDark,
"doNotDisturb": doNotDisturb,
"nightModeEnabled": nightModeEnabled,
"nightModeTemperature": nightModeTemperature,
@@ -185,15 +214,29 @@ Singleton {
"batteryLockTimeout": batteryLockTimeout,
"batterySuspendTimeout": batterySuspendTimeout,
"batteryHibernateTimeout": batteryHibernateTimeout,
"lockBeforeSuspend": lockBeforeSuspend
"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()
@@ -264,24 +307,32 @@ Singleton {
function setWallpaper(imagePath) {
wallpaperPath = imagePath
if (perModeWallpaper) {
if (isLightMode) {
wallpaperPathLight = imagePath
} else {
wallpaperPathDark = imagePath
}
}
saveSettings()
if (typeof Theme !== "undefined") {
if (Theme.currentTheme === Theme.dynamic) {
Theme.extractColors()
}
Theme.generateSystemThemesFromCurrentTheme()
}
}
function setWallpaperColor(color) {
wallpaperPath = color
if (perModeWallpaper) {
if (isLightMode) {
wallpaperPathLight = color
} else {
wallpaperPathDark = color
}
}
saveSettings()
if (typeof Theme !== "undefined") {
if (Theme.currentTheme === Theme.dynamic) {
Theme.extractColors()
}
Theme.generateSystemThemesFromCurrentTheme()
}
}
@@ -314,6 +365,16 @@ Singleton {
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
@@ -426,13 +487,52 @@ Singleton {
function setPerMonitorWallpaper(enabled) {
perMonitorWallpaper = enabled
if (enabled && perModeWallpaper) {
syncWallpaperForCurrentMode()
}
saveSettings()
// Refresh dynamic theming when per-monitor mode changes
if (typeof Theme !== "undefined") {
if (Theme.currentTheme === Theme.dynamic) {
Theme.extractColors()
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()
}
}
@@ -445,15 +545,32 @@ Singleton {
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()
// Trigger dynamic theming if this is the first monitor and dynamic theming is enabled
if (typeof Theme !== "undefined" && typeof Quickshell !== "undefined") {
var screens = Quickshell.screens
if (screens.length > 0 && screenName === screens[0].name) {
if (Theme.currentTheme === Theme.dynamic) {
Theme.extractColors()
}
Theme.generateSystemThemesFromCurrentTheme()
}
}
@@ -526,30 +643,56 @@ Singleton {
saveSettings()
}
function setShowThirdPartyPlugins(enabled) {
showThirdPartyPlugins = enabled
saveSettings()
}
FileView {
id: settingsFile
path: StandardPaths.writableLocation(StandardPaths.GenericStateLocation) + "/DankMaterialShell/session.json"
blockLoading: true
path: isGreeterMode ? "" : StandardPaths.writableLocation(StandardPaths.GenericStateLocation) + "/DankMaterialShell/session.json"
blockLoading: isGreeterMode
blockWrites: true
watchChanges: true
watchChanges: !isGreeterMode
onLoaded: {
parseSettings(settingsFile.text())
hasTriedDefaultSession = false
if (!isGreeterMode) {
parseSettings(settingsFile.text())
hasTriedDefaultSession = false
}
}
onLoadFailed: error => {
if (!hasTriedDefaultSession) {
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 \"$CONFIG_DIR/default-session.json\" \"$CONFIG_DIR/session.json\" && echo 'copied'; else echo 'not_found'; fi"]
+ "/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) {
@@ -695,4 +838,4 @@ Singleton {
}
}
}
}
}

View File

@@ -12,13 +12,30 @@ import qs.Services
Singleton {
id: root
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
enum Position {
Top,
Bottom,
Left,
Right
}
enum AnimationSpeed {
None,
Shortest,
Short,
Medium,
Long
}
// Theme settings
property string currentThemeName: "blue"
property string customThemeFile: ""
property string matugenScheme: "scheme-tonal-spot"
property real topBarTransparency: 0.75
property real topBarWidgetTransparency: 0.85
property real popupTransparency: 0.92
property real dankBarTransparency: 1.0
property real dankBarWidgetTransparency: 1.0
property real popupTransparency: 1.0
property real dockTransparency: 1
property bool use24HourClock: true
property bool useFahrenheit: false
@@ -70,12 +87,13 @@ Singleton {
property string clockDateFormat: ""
property string lockDateFormat: ""
property int mediaSize: 1
property var topBarLeftWidgets: ["launcherButton", "workspaceSwitcher", "focusedWindow"]
property var topBarCenterWidgets: ["music", "clock", "weather"]
property var topBarRightWidgets: ["systemTray", "clipboard", "cpuUsage", "memUsage", "notificationButton", "battery", "controlCenterButton"]
property alias topBarLeftWidgetsModel: leftWidgetsModel
property alias topBarCenterWidgetsModel: centerWidgetsModel
property alias topBarRightWidgetsModel: rightWidgetsModel
property var dankBarLeftWidgets: ["launcherButton", "workspaceSwitcher", "focusedWindow"]
property var dankBarCenterWidgets: ["music", "clock", "weather"]
property var dankBarRightWidgets: ["systemTray", "clipboard", "cpuUsage", "memUsage", "notificationButton", "battery", "controlCenterButton"]
property var dankBarWidgetOrder: []
property alias dankBarLeftWidgetsModel: leftWidgetsModel
property alias dankBarCenterWidgetsModel: centerWidgetsModel
property alias dankBarRightWidgetsModel: rightWidgetsModel
property string appLauncherViewMode: "list"
property string spotlightModalViewMode: "list"
property string networkPreference: "auto"
@@ -85,15 +103,19 @@ Singleton {
property bool qt5ctAvailable: false
property bool qt6ctAvailable: false
property bool gtkAvailable: false
property bool useOSLogo: false
property string osLogoColorOverride: ""
property real osLogoBrightness: 0.5
property real osLogoContrast: 1
property string launcherLogoMode: "apps"
property string launcherLogoCustomPath: ""
property string launcherLogoColorOverride: ""
property bool launcherLogoColorInvertOnMode: false
property real launcherLogoBrightness: 0.5
property real launcherLogoContrast: 1
property int launcherLogoSizeOffset: 0
property bool weatherEnabled: true
property string fontFamily: "Inter Variable"
property string monoFontFamily: "Fira Code"
property int fontWeight: Font.Normal
property real fontScale: 1.0
property real dankBarFontScale: 1.0
property bool notepadUseMonospace: true
property string notepadFontFamily: ""
property real notepadFontSize: 14
@@ -117,35 +139,51 @@ Singleton {
property bool showDock: false
property bool dockAutoHide: false
property bool dockGroupByApp: false
property bool dockOpenOnOverview: false
property int dockPosition: SettingsData.Position.Bottom
property real dockSpacing: 4
property real dockBottomGap: 0
property real cornerRadius: 12
property bool notificationOverlayEnabled: false
property bool topBarAutoHide: false
property bool topBarOpenOnOverview: false
property bool topBarVisible: true
property real topBarSpacing: 4
property real topBarBottomGap: 0
property real topBarInnerPadding: 8
property bool topBarSquareCorners: false
property bool topBarNoBackground: false
property bool topBarGothCornersEnabled: false
property bool dankBarAutoHide: false
property bool dankBarOpenOnOverview: false
property bool dankBarVisible: true
property real dankBarSpacing: 4
property real dankBarBottomGap: 0
property real dankBarInnerPadding: 4
property bool dankBarSquareCorners: false
property bool dankBarNoBackground: false
property bool dankBarGothCornersEnabled: false
property bool dankBarBorderEnabled: false
property int dankBarPosition: SettingsData.Position.Top
property bool dankBarIsVertical: dankBarPosition === SettingsData.Position.Left || dankBarPosition === SettingsData.Position.Right
property bool lockScreenShowPowerActions: true
property bool hideBrightnessSlider: false
property string widgetBackgroundColor: "sth"
property string widgetBackgroundColor: "sch"
property string surfaceBase: "s"
property int notificationTimeoutLow: 5000
property int notificationTimeoutNormal: 5000
property int notificationTimeoutCritical: 0
property int notificationPopupPosition: SettingsData.Position.Top
property bool osdAlwaysShowValue: false
property var screenPreferences: ({})
property int animationSpeed: SettingsData.AnimationSpeed.Short
readonly property string defaultFontFamily: "Inter Variable"
readonly property string defaultMonoFontFamily: "Fira Code"
readonly property string _homeUrl: StandardPaths.writableLocation(StandardPaths.HomeLocation)
readonly property string _configUrl: StandardPaths.writableLocation(StandardPaths.ConfigLocation)
readonly property string _configDir: Paths.strip(_configUrl)
readonly property string pluginSettingsPath: _configDir + "/DankMaterialShell/plugin_settings.json"
signal forceTopBarLayoutRefresh
signal forceDankBarLayoutRefresh
signal forceDockLayoutRefresh
signal widgetDataChanged
signal workspaceIconsUpdated
property bool _loading: false
property bool _pluginSettingsLoading: false
property var pluginSettings: ({})
function getEffectiveTimeFormat() {
if (use24HourClock) {
@@ -172,28 +210,57 @@ Singleton {
"size": 20,
"selectedGpuIndex": 0,
"pciId": "",
"mountPath": "/"
"mountPath": "/",
"minimumWidth": true
}
leftWidgetsModel.append(dummyItem)
centerWidgetsModel.append(dummyItem)
rightWidgetsModel.append(dummyItem)
updateListModel(leftWidgetsModel, topBarLeftWidgets)
updateListModel(centerWidgetsModel, topBarCenterWidgets)
updateListModel(rightWidgetsModel, topBarRightWidgets)
updateListModel(leftWidgetsModel, dankBarLeftWidgets)
updateListModel(centerWidgetsModel, dankBarCenterWidgets)
updateListModel(rightWidgetsModel, dankBarRightWidgets)
}
function loadSettings() {
_loading = true
parseSettings(settingsFile.text())
_loading = false
loadPluginSettings()
}
function loadPluginSettings() {
_pluginSettingsLoading = true
parsePluginSettings(pluginSettingsFile.text())
_pluginSettingsLoading = false
}
function parsePluginSettings(content) {
_pluginSettingsLoading = true
try {
if (content && content.trim()) {
pluginSettings = JSON.parse(content)
} else {
pluginSettings = {}
}
} catch (e) {
console.warn("SettingsData: Failed to parse plugin settings:", e.message)
pluginSettings = {}
} finally {
_pluginSettingsLoading = false
}
}
function parseSettings(content) {
_loading = true
var shouldMigrate = false
try {
if (content && content.trim()) {
var settings = JSON.parse(content)
if (settings.pluginSettings !== undefined) {
pluginSettings = settings.pluginSettings
shouldMigrate = true
}
// Auto-migrate from old theme system
if (settings.themeIndex !== undefined || settings.themeIsDynamic !== undefined) {
const themeNames = ["blue", "deepBlue", "purple", "green", "orange", "red", "cyan", "pink", "amber", "coral"]
@@ -208,9 +275,9 @@ Singleton {
}
customThemeFile = settings.customThemeFile !== undefined ? settings.customThemeFile : ""
matugenScheme = settings.matugenScheme !== undefined ? settings.matugenScheme : "scheme-tonal-spot"
topBarTransparency = settings.topBarTransparency !== undefined ? (settings.topBarTransparency > 1 ? settings.topBarTransparency / 100 : settings.topBarTransparency) : 0.75
topBarWidgetTransparency = settings.topBarWidgetTransparency !== undefined ? (settings.topBarWidgetTransparency > 1 ? settings.topBarWidgetTransparency / 100 : settings.topBarWidgetTransparency) : 0.85
popupTransparency = settings.popupTransparency !== undefined ? (settings.popupTransparency > 1 ? settings.popupTransparency / 100 : settings.popupTransparency) : 0.92
dankBarTransparency = settings.dankBarTransparency !== undefined ? (settings.dankBarTransparency > 1 ? settings.dankBarTransparency / 100 : settings.dankBarTransparency) : (settings.topBarTransparency !== undefined ? (settings.topBarTransparency > 1 ? settings.topBarTransparency / 100 : settings.topBarTransparency) : 1.0)
dankBarWidgetTransparency = settings.dankBarWidgetTransparency !== undefined ? (settings.dankBarWidgetTransparency > 1 ? settings.dankBarWidgetTransparency / 100 : settings.dankBarWidgetTransparency) : (settings.topBarWidgetTransparency !== undefined ? (settings.topBarWidgetTransparency > 1 ? settings.topBarWidgetTransparency / 100 : settings.topBarWidgetTransparency) : 1.0)
popupTransparency = settings.popupTransparency !== undefined ? (settings.popupTransparency > 1 ? settings.popupTransparency / 100 : settings.popupTransparency) : 1.0
dockTransparency = settings.dockTransparency !== undefined ? (settings.dockTransparency > 1 ? settings.dockTransparency / 100 : settings.dockTransparency) : 1
use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true
useFahrenheit = settings.useFahrenheit !== undefined ? settings.useFahrenheit : false
@@ -263,23 +330,24 @@ Singleton {
clockDateFormat = settings.clockDateFormat !== undefined ? settings.clockDateFormat : ""
lockDateFormat = settings.lockDateFormat !== undefined ? settings.lockDateFormat : ""
mediaSize = settings.mediaSize !== undefined ? settings.mediaSize : (settings.mediaCompactMode !== undefined ? (settings.mediaCompactMode ? 0 : 1) : 1)
if (settings.topBarWidgetOrder) {
topBarLeftWidgets = settings.topBarWidgetOrder.filter(w => {
if (settings.dankBarWidgetOrder || settings.topBarWidgetOrder) {
var widgetOrder = settings.dankBarWidgetOrder || settings.topBarWidgetOrder
dankBarLeftWidgets = widgetOrder.filter(w => {
return ["launcherButton", "workspaceSwitcher", "focusedWindow"].includes(w)
})
topBarCenterWidgets = settings.topBarWidgetOrder.filter(w => {
dankBarCenterWidgets = widgetOrder.filter(w => {
return ["clock", "music", "weather"].includes(w)
})
topBarRightWidgets = settings.topBarWidgetOrder.filter(w => {
dankBarRightWidgets = widgetOrder.filter(w => {
return ["systemTray", "clipboard", "systemResources", "notificationButton", "battery", "controlCenterButton"].includes(w)
})
} else {
var leftWidgets = settings.topBarLeftWidgets !== undefined ? settings.topBarLeftWidgets : ["launcherButton", "workspaceSwitcher", "focusedWindow"]
var centerWidgets = settings.topBarCenterWidgets !== undefined ? settings.topBarCenterWidgets : ["music", "clock", "weather"]
var rightWidgets = settings.topBarRightWidgets !== undefined ? settings.topBarRightWidgets : ["systemTray", "clipboard", "cpuUsage", "memUsage", "notificationButton", "battery", "controlCenterButton"]
topBarLeftWidgets = leftWidgets
topBarCenterWidgets = centerWidgets
topBarRightWidgets = rightWidgets
var leftWidgets = settings.dankBarLeftWidgets !== undefined ? settings.dankBarLeftWidgets : (settings.topBarLeftWidgets !== undefined ? settings.topBarLeftWidgets : ["launcherButton", "workspaceSwitcher", "focusedWindow"])
var centerWidgets = settings.dankBarCenterWidgets !== undefined ? settings.dankBarCenterWidgets : (settings.topBarCenterWidgets !== undefined ? settings.topBarCenterWidgets : ["music", "clock", "weather"])
var rightWidgets = settings.dankBarRightWidgets !== undefined ? settings.dankBarRightWidgets : (settings.topBarRightWidgets !== undefined ? settings.topBarRightWidgets : ["systemTray", "clipboard", "cpuUsage", "memUsage", "notificationButton", "battery", "controlCenterButton"])
dankBarLeftWidgets = leftWidgets
dankBarCenterWidgets = centerWidgets
dankBarRightWidgets = rightWidgets
updateListModel(leftWidgetsModel, leftWidgets)
updateListModel(centerWidgetsModel, centerWidgets)
updateListModel(rightWidgetsModel, rightWidgets)
@@ -288,14 +356,25 @@ Singleton {
spotlightModalViewMode = settings.spotlightModalViewMode !== undefined ? settings.spotlightModalViewMode : "list"
networkPreference = settings.networkPreference !== undefined ? settings.networkPreference : "auto"
iconTheme = settings.iconTheme !== undefined ? settings.iconTheme : "System Default"
useOSLogo = settings.useOSLogo !== undefined ? settings.useOSLogo : false
osLogoColorOverride = settings.osLogoColorOverride !== undefined ? settings.osLogoColorOverride : ""
osLogoBrightness = settings.osLogoBrightness !== undefined ? settings.osLogoBrightness : 0.5
osLogoContrast = settings.osLogoContrast !== undefined ? settings.osLogoContrast : 1
if (settings.useOSLogo !== undefined) {
launcherLogoMode = settings.useOSLogo ? "os" : "apps"
launcherLogoColorOverride = settings.osLogoColorOverride !== undefined ? settings.osLogoColorOverride : ""
launcherLogoBrightness = settings.osLogoBrightness !== undefined ? settings.osLogoBrightness : 0.5
launcherLogoContrast = settings.osLogoContrast !== undefined ? settings.osLogoContrast : 1
} else {
launcherLogoMode = settings.launcherLogoMode !== undefined ? settings.launcherLogoMode : "apps"
launcherLogoCustomPath = settings.launcherLogoCustomPath !== undefined ? settings.launcherLogoCustomPath : ""
launcherLogoColorOverride = settings.launcherLogoColorOverride !== undefined ? settings.launcherLogoColorOverride : ""
launcherLogoColorInvertOnMode = settings.launcherLogoColorInvertOnMode !== undefined ? settings.launcherLogoColorInvertOnMode : false
launcherLogoBrightness = settings.launcherLogoBrightness !== undefined ? settings.launcherLogoBrightness : 0.5
launcherLogoContrast = settings.launcherLogoContrast !== undefined ? settings.launcherLogoContrast : 1
launcherLogoSizeOffset = settings.launcherLogoSizeOffset !== undefined ? settings.launcherLogoSizeOffset : 0
}
fontFamily = settings.fontFamily !== undefined ? settings.fontFamily : defaultFontFamily
monoFontFamily = settings.monoFontFamily !== undefined ? settings.monoFontFamily : defaultMonoFontFamily
fontWeight = settings.fontWeight !== undefined ? settings.fontWeight : Font.Normal
fontScale = settings.fontScale !== undefined ? settings.fontScale : 1.0
dankBarFontScale = settings.dankBarFontScale !== undefined ? settings.dankBarFontScale : 1.0
notepadUseMonospace = settings.notepadUseMonospace !== undefined ? settings.notepadUseMonospace : true
notepadFontFamily = settings.notepadFontFamily !== undefined ? settings.notepadFontFamily : ""
notepadFontSize = settings.notepadFontSize !== undefined ? settings.notepadFontSize : 14
@@ -307,24 +386,34 @@ Singleton {
showDock = settings.showDock !== undefined ? settings.showDock : false
dockAutoHide = settings.dockAutoHide !== undefined ? settings.dockAutoHide : false
dockGroupByApp = settings.dockGroupByApp !== undefined ? settings.dockGroupByApp : false
dockPosition = settings.dockPosition !== undefined ? settings.dockPosition : SettingsData.Position.Bottom
dockSpacing = settings.dockSpacing !== undefined ? settings.dockSpacing : 4
dockBottomGap = settings.dockBottomGap !== undefined ? settings.dockBottomGap : 0
cornerRadius = settings.cornerRadius !== undefined ? settings.cornerRadius : 12
notificationOverlayEnabled = settings.notificationOverlayEnabled !== undefined ? settings.notificationOverlayEnabled : false
topBarAutoHide = settings.topBarAutoHide !== undefined ? settings.topBarAutoHide : false
topBarOpenOnOverview = settings.topBarOpenOnOverview !== undefined ? settings.topBarOpenOnOverview : false
topBarVisible = settings.topBarVisible !== undefined ? settings.topBarVisible : true
dankBarAutoHide = settings.dankBarAutoHide !== undefined ? settings.dankBarAutoHide : (settings.topBarAutoHide !== undefined ? settings.topBarAutoHide : false)
dankBarOpenOnOverview = settings.dankBarOpenOnOverview !== undefined ? settings.dankBarOpenOnOverview : (settings.topBarOpenOnOverview !== undefined ? settings.topBarOpenOnOverview : false)
dankBarVisible = settings.dankBarVisible !== undefined ? settings.dankBarVisible : (settings.topBarVisible !== undefined ? settings.topBarVisible : true)
dockOpenOnOverview = settings.dockOpenOnOverview !== undefined ? settings.dockOpenOnOverview : false
notificationTimeoutLow = settings.notificationTimeoutLow !== undefined ? settings.notificationTimeoutLow : 5000
notificationTimeoutNormal = settings.notificationTimeoutNormal !== undefined ? settings.notificationTimeoutNormal : 5000
notificationTimeoutCritical = settings.notificationTimeoutCritical !== undefined ? settings.notificationTimeoutCritical : 0
topBarSpacing = settings.topBarSpacing !== undefined ? settings.topBarSpacing : 4
topBarBottomGap = settings.topBarBottomGap !== undefined ? settings.topBarBottomGap : 0
topBarInnerPadding = settings.topBarInnerPadding !== undefined ? settings.topBarInnerPadding : 8
topBarSquareCorners = settings.topBarSquareCorners !== undefined ? settings.topBarSquareCorners : false
topBarNoBackground = settings.topBarNoBackground !== undefined ? settings.topBarNoBackground : false
topBarGothCornersEnabled = settings.topBarGothCornersEnabled !== undefined ? settings.topBarGothCornersEnabled : false
notificationPopupPosition = settings.notificationPopupPosition !== undefined ? settings.notificationPopupPosition : SettingsData.Position.Top
osdAlwaysShowValue = settings.osdAlwaysShowValue !== undefined ? settings.osdAlwaysShowValue : false
dankBarSpacing = settings.dankBarSpacing !== undefined ? settings.dankBarSpacing : (settings.topBarSpacing !== undefined ? settings.topBarSpacing : 4)
dankBarBottomGap = settings.dankBarBottomGap !== undefined ? settings.dankBarBottomGap : (settings.topBarBottomGap !== undefined ? settings.topBarBottomGap : 0)
dankBarInnerPadding = settings.dankBarInnerPadding !== undefined ? settings.dankBarInnerPadding : (settings.topBarInnerPadding !== undefined ? settings.topBarInnerPadding : 4)
dankBarSquareCorners = settings.dankBarSquareCorners !== undefined ? settings.dankBarSquareCorners : (settings.topBarSquareCorners !== undefined ? settings.topBarSquareCorners : false)
dankBarNoBackground = settings.dankBarNoBackground !== undefined ? settings.dankBarNoBackground : (settings.topBarNoBackground !== undefined ? settings.topBarNoBackground : false)
dankBarGothCornersEnabled = settings.dankBarGothCornersEnabled !== undefined ? settings.dankBarGothCornersEnabled : (settings.topBarGothCornersEnabled !== undefined ? settings.topBarGothCornersEnabled : false)
dankBarBorderEnabled = settings.dankBarBorderEnabled !== undefined ? settings.dankBarBorderEnabled : false
dankBarPosition = settings.dankBarPosition !== undefined ? settings.dankBarPosition : (settings.dankBarAtBottom !== undefined ? (settings.dankBarAtBottom ? SettingsData.Position.Bottom : SettingsData.Position.Top) : (settings.topBarAtBottom !== undefined ? (settings.topBarAtBottom ? SettingsData.Position.Bottom : SettingsData.Position.Top) : SettingsData.Position.Top))
lockScreenShowPowerActions = settings.lockScreenShowPowerActions !== undefined ? settings.lockScreenShowPowerActions : true
hideBrightnessSlider = settings.hideBrightnessSlider !== undefined ? settings.hideBrightnessSlider : false
widgetBackgroundColor = settings.widgetBackgroundColor !== undefined ? settings.widgetBackgroundColor : "sth"
widgetBackgroundColor = settings.widgetBackgroundColor !== undefined ? settings.widgetBackgroundColor : "sch"
surfaceBase = settings.surfaceBase !== undefined ? settings.surfaceBase : "s"
screenPreferences = settings.screenPreferences !== undefined ? settings.screenPreferences : ({})
animationSpeed = settings.animationSpeed !== undefined ? settings.animationSpeed : SettingsData.AnimationSpeed.Short
applyStoredTheme()
detectAvailableIconThemes()
detectQtTools()
@@ -338,6 +427,11 @@ Singleton {
} finally {
_loading = false
}
if (shouldMigrate) {
savePluginSettings()
saveSettings()
}
}
function saveSettings() {
@@ -347,8 +441,8 @@ Singleton {
"currentThemeName": currentThemeName,
"customThemeFile": customThemeFile,
"matugenScheme": matugenScheme,
"topBarTransparency": topBarTransparency,
"topBarWidgetTransparency": topBarWidgetTransparency,
"dankBarTransparency": dankBarTransparency,
"dankBarWidgetTransparency": dankBarWidgetTransparency,
"popupTransparency": popupTransparency,
"dockTransparency": dockTransparency,
"use24HourClock": use24HourClock,
@@ -393,21 +487,25 @@ Singleton {
"clockDateFormat": clockDateFormat,
"lockDateFormat": lockDateFormat,
"mediaSize": mediaSize,
"topBarLeftWidgets": topBarLeftWidgets,
"topBarCenterWidgets": topBarCenterWidgets,
"topBarRightWidgets": topBarRightWidgets,
"dankBarLeftWidgets": dankBarLeftWidgets,
"dankBarCenterWidgets": dankBarCenterWidgets,
"dankBarRightWidgets": dankBarRightWidgets,
"appLauncherViewMode": appLauncherViewMode,
"spotlightModalViewMode": spotlightModalViewMode,
"networkPreference": networkPreference,
"iconTheme": iconTheme,
"useOSLogo": useOSLogo,
"osLogoColorOverride": osLogoColorOverride,
"osLogoBrightness": osLogoBrightness,
"osLogoContrast": osLogoContrast,
"launcherLogoMode": launcherLogoMode,
"launcherLogoCustomPath": launcherLogoCustomPath,
"launcherLogoColorOverride": launcherLogoColorOverride,
"launcherLogoColorInvertOnMode": launcherLogoColorInvertOnMode,
"launcherLogoBrightness": launcherLogoBrightness,
"launcherLogoContrast": launcherLogoContrast,
"launcherLogoSizeOffset": launcherLogoSizeOffset,
"fontFamily": fontFamily,
"monoFontFamily": monoFontFamily,
"fontWeight": fontWeight,
"fontScale": fontScale,
"dankBarFontScale": dankBarFontScale,
"notepadUseMonospace": notepadUseMonospace,
"notepadFontFamily": notepadFontFamily,
"notepadFontSize": notepadFontSize,
@@ -419,27 +517,43 @@ Singleton {
"showDock": showDock,
"dockAutoHide": dockAutoHide,
"dockGroupByApp": dockGroupByApp,
"dockOpenOnOverview": dockOpenOnOverview,
"dockPosition": dockPosition,
"dockSpacing": dockSpacing,
"dockBottomGap": dockBottomGap,
"cornerRadius": cornerRadius,
"notificationOverlayEnabled": notificationOverlayEnabled,
"topBarAutoHide": topBarAutoHide,
"topBarOpenOnOverview": topBarOpenOnOverview,
"topBarVisible": topBarVisible,
"topBarSpacing": topBarSpacing,
"topBarBottomGap": topBarBottomGap,
"topBarInnerPadding": topBarInnerPadding,
"topBarSquareCorners": topBarSquareCorners,
"topBarNoBackground": topBarNoBackground,
"topBarGothCornersEnabled": topBarGothCornersEnabled,
"dankBarAutoHide": dankBarAutoHide,
"dankBarOpenOnOverview": dankBarOpenOnOverview,
"dankBarVisible": dankBarVisible,
"dankBarSpacing": dankBarSpacing,
"dankBarBottomGap": dankBarBottomGap,
"dankBarInnerPadding": dankBarInnerPadding,
"dankBarSquareCorners": dankBarSquareCorners,
"dankBarNoBackground": dankBarNoBackground,
"dankBarGothCornersEnabled": dankBarGothCornersEnabled,
"dankBarBorderEnabled": dankBarBorderEnabled,
"dankBarPosition": dankBarPosition,
"lockScreenShowPowerActions": lockScreenShowPowerActions,
"hideBrightnessSlider": hideBrightnessSlider,
"widgetBackgroundColor": widgetBackgroundColor,
"surfaceBase": surfaceBase,
"notificationTimeoutLow": notificationTimeoutLow,
"notificationTimeoutNormal": notificationTimeoutNormal,
"notificationTimeoutCritical": notificationTimeoutCritical,
"screenPreferences": screenPreferences
"notificationPopupPosition": notificationPopupPosition,
"osdAlwaysShowValue": osdAlwaysShowValue,
"screenPreferences": screenPreferences,
"animationSpeed": animationSpeed
}, null, 2))
}
function savePluginSettings() {
if (_pluginSettingsLoading)
return
pluginSettingsFile.setText(JSON.stringify(pluginSettings, null, 2))
}
function setShowWorkspaceIndex(enabled) {
showWorkspaceIndex = enabled
saveSettings()
@@ -552,11 +666,11 @@ Singleton {
function applyStoredTheme() {
if (typeof Theme !== "undefined")
Theme.switchTheme(currentThemeName, false)
Theme.switchTheme(currentThemeName, false, false)
else
Qt.callLater(() => {
if (typeof Theme !== "undefined")
Theme.switchTheme(currentThemeName, false)
Theme.switchTheme(currentThemeName, false, false)
})
}
@@ -579,20 +693,17 @@ Singleton {
saveSettings()
if (typeof Theme !== "undefined") {
if (Theme.currentTheme === Theme.dynamic) {
Theme.extractColors()
}
Theme.generateSystemThemesFromCurrentTheme()
}
}
function setTopBarTransparency(transparency) {
topBarTransparency = transparency
function setDankBarTransparency(transparency) {
dankBarTransparency = transparency
saveSettings()
}
function setTopBarWidgetTransparency(transparency) {
topBarWidgetTransparency = transparency
function setDankBarWidgetTransparency(transparency) {
dankBarWidgetTransparency = transparency
saveSettings()
}
@@ -727,25 +838,25 @@ Singleton {
saveSettings()
}
function setTopBarWidgetOrder(order) {
topBarWidgetOrder = order
function setDankBarWidgetOrder(order) {
dankBarWidgetOrder = order
saveSettings()
}
function setTopBarLeftWidgets(order) {
topBarLeftWidgets = order
function setDankBarLeftWidgets(order) {
dankBarLeftWidgets = order
updateListModel(leftWidgetsModel, order)
saveSettings()
}
function setTopBarCenterWidgets(order) {
topBarCenterWidgets = order
function setDankBarCenterWidgets(order) {
dankBarCenterWidgets = order
updateListModel(centerWidgetsModel, order)
saveSettings()
}
function setTopBarRightWidgets(order) {
topBarRightWidgets = order
function setDankBarRightWidgets(order) {
dankBarRightWidgets = order
updateListModel(rightWidgetsModel, order)
saveSettings()
}
@@ -759,6 +870,7 @@ Singleton {
var selectedGpuIndex = typeof order[i] === "string" ? undefined : order[i].selectedGpuIndex
var pciId = typeof order[i] === "string" ? undefined : order[i].pciId
var mountPath = typeof order[i] === "string" ? undefined : order[i].mountPath
var minimumWidth = typeof order[i] === "string" ? undefined : order[i].minimumWidth
var item = {
"widgetId": widgetId,
"enabled": enabled
@@ -771,6 +883,8 @@ Singleton {
item.pciId = pciId
if (mountPath !== undefined)
item.mountPath = mountPath
if (minimumWidth !== undefined)
item.minimumWidth = minimumWidth
listModel.append(item)
}
@@ -778,13 +892,13 @@ Singleton {
widgetDataChanged()
}
function resetTopBarWidgetsToDefault() {
function resetDankBarWidgetsToDefault() {
var defaultLeft = ["launcherButton", "workspaceSwitcher", "focusedWindow"]
var defaultCenter = ["music", "clock", "weather"]
var defaultRight = ["systemTray", "clipboard", "notificationButton", "battery", "controlCenterButton"]
topBarLeftWidgets = defaultLeft
topBarCenterWidgets = defaultCenter
topBarRightWidgets = defaultRight
dankBarLeftWidgets = defaultLeft
dankBarCenterWidgets = defaultCenter
dankBarRightWidgets = defaultRight
updateListModel(leftWidgetsModel, defaultLeft)
updateListModel(centerWidgetsModel, defaultCenter)
updateListModel(rightWidgetsModel, defaultRight)
@@ -893,23 +1007,38 @@ Singleton {
updateQtIconTheme(iconTheme)
}
function setUseOSLogo(enabled) {
useOSLogo = enabled
function setLauncherLogoMode(mode) {
launcherLogoMode = mode
saveSettings()
}
function setOSLogoColorOverride(color) {
osLogoColorOverride = color
function setLauncherLogoCustomPath(path) {
launcherLogoCustomPath = path
saveSettings()
}
function setOSLogoBrightness(brightness) {
osLogoBrightness = brightness
function setLauncherLogoColorOverride(color) {
launcherLogoColorOverride = color
saveSettings()
}
function setOSLogoContrast(contrast) {
osLogoContrast = contrast
function setLauncherLogoColorInvertOnMode(invert) {
launcherLogoColorInvertOnMode = invert
saveSettings()
}
function setLauncherLogoBrightness(brightness) {
launcherLogoBrightness = brightness
saveSettings()
}
function setLauncherLogoContrast(contrast) {
launcherLogoContrast = contrast
saveSettings()
}
function setLauncherLogoSizeOffset(offset) {
launcherLogoSizeOffset = offset
saveSettings()
}
@@ -933,6 +1062,11 @@ Singleton {
saveSettings()
}
function setDankBarFontScale(scale) {
dankBarFontScale = scale
saveSettings()
}
function setGtkThemingEnabled(enabled) {
gtkThemingEnabled = enabled
saveSettings()
@@ -951,6 +1085,24 @@ Singleton {
function setShowDock(enabled) {
showDock = enabled
if (enabled && dockPosition === dankBarPosition) {
if (dankBarPosition === SettingsData.Position.Top) {
setDockPosition(SettingsData.Position.Bottom)
return
}
if (dankBarPosition === SettingsData.Position.Bottom) {
setDockPosition(SettingsData.Position.Top)
return
}
if (dankBarPosition === SettingsData.Position.Left) {
setDockPosition(SettingsData.Position.Right)
return
}
if (dankBarPosition === SettingsData.Position.Right) {
setDockPosition(SettingsData.Position.Left)
return
}
}
saveSettings()
}
@@ -964,6 +1116,11 @@ Singleton {
saveSettings()
}
function setdockOpenOnOverview(enabled) {
dockOpenOnOverview = enabled
saveSettings()
}
function setCornerRadius(radius) {
cornerRadius = radius
saveSettings()
@@ -974,23 +1131,23 @@ Singleton {
saveSettings()
}
function setTopBarAutoHide(enabled) {
topBarAutoHide = enabled
function setDankBarAutoHide(enabled) {
dankBarAutoHide = enabled
saveSettings()
}
function setTopBarOpenOnOverview(enabled) {
topBarOpenOnOverview = enabled
function setDankBarOpenOnOverview(enabled) {
dankBarOpenOnOverview = enabled
saveSettings()
}
function setTopBarVisible(visible) {
topBarVisible = visible
function setDankBarVisible(visible) {
dankBarVisible = visible
saveSettings()
}
function toggleTopBarVisible() {
topBarVisible = !topBarVisible
function toggleDankBarVisible() {
dankBarVisible = !dankBarVisible
saveSettings()
}
@@ -1009,36 +1166,172 @@ Singleton {
saveSettings()
}
function setTopBarSpacing(spacing) {
topBarSpacing = spacing
function setNotificationPopupPosition(position) {
notificationPopupPosition = position
saveSettings()
}
function setTopBarBottomGap(gap) {
topBarBottomGap = gap
function setOsdAlwaysShowValue(enabled) {
osdAlwaysShowValue = enabled
saveSettings()
}
function setTopBarInnerPadding(padding) {
topBarInnerPadding = padding
function sendTestNotifications() {
sendTestNotification(0)
testNotifTimer1.start()
testNotifTimer2.start()
}
function sendTestNotification(index) {
const notifications = [
["Notification Position Test", "DMS test notification 1 of 3 ~ Hi there!", "preferences-system"],
["Second Test", "DMS Notification 2 of 3 ~ Check it out!", "applications-graphics"],
["Third Test", "DMS notification 3 of 3 ~ Enjoy!", "face-smile"]
]
if (index < 0 || index >= notifications.length) {
return
}
const notif = notifications[index]
testNotificationProcess.command = ["notify-send", "-h", "int:transient:1", "-a", "DMS", "-i", notif[2], notif[0], notif[1]]
testNotificationProcess.running = true
}
property Process testNotificationProcess
testNotificationProcess: Process {
command: []
running: false
}
property Timer testNotifTimer1
testNotifTimer1: Timer {
interval: 400
repeat: false
onTriggered: sendTestNotification(1)
}
property Timer testNotifTimer2
testNotifTimer2: Timer {
interval: 800
repeat: false
onTriggered: sendTestNotification(2)
}
function setDankBarSpacing(spacing) {
dankBarSpacing = spacing
saveSettings()
}
function setTopBarSquareCorners(enabled) {
topBarSquareCorners = enabled
function setDankBarBottomGap(gap) {
dankBarBottomGap = gap
saveSettings()
}
function setTopBarNoBackground(enabled) {
topBarNoBackground = enabled
function setDankBarInnerPadding(padding) {
dankBarInnerPadding = padding
saveSettings()
}
function setTopBarGothCornersEnabled(enabled) {
topBarGothCornersEnabled = enabled
function setDankBarSquareCorners(enabled) {
dankBarSquareCorners = enabled
saveSettings()
}
function setDankBarNoBackground(enabled) {
dankBarNoBackground = enabled
saveSettings()
}
function setDankBarGothCornersEnabled(enabled) {
dankBarGothCornersEnabled = enabled
saveSettings()
}
function setDankBarBorderEnabled(enabled) {
dankBarBorderEnabled = enabled
saveSettings()
}
function setDankBarPosition(position) {
dankBarPosition = position
if (position === SettingsData.Position.Bottom && dockPosition === SettingsData.Position.Bottom && showDock) {
setDockPosition(SettingsData.Position.Top)
return
}
if (position === SettingsData.Position.Top && dockPosition === SettingsData.Position.Top && showDock) {
setDockPosition(SettingsData.Position.Bottom)
return
}
if (position === SettingsData.Position.Left && dockPosition === SettingsData.Position.Left && showDock) {
setDockPosition(SettingsData.Position.Right)
return
}
if (position === SettingsData.Position.Right && dockPosition === SettingsData.Position.Right && showDock) {
setDockPosition(SettingsData.Position.Left)
return
}
saveSettings()
}
function setDockPosition(position) {
dockPosition = position
if (position === SettingsData.Position.Bottom && dankBarPosition === SettingsData.Position.Bottom && showDock) {
setDankBarPosition(SettingsData.Position.Top)
}
if (position === SettingsData.Position.Top && dankBarPosition === SettingsData.Position.Top && showDock) {
setDankBarPosition(SettingsData.Position.Bottom)
}
if (position === SettingsData.Position.Left && dankBarPosition === SettingsData.Position.Left && showDock) {
setDankBarPosition(SettingsData.Position.Right)
}
if (position === SettingsData.Position.Right && dankBarPosition === SettingsData.Position.Right && showDock) {
setDankBarPosition(SettingsData.Position.Left)
}
saveSettings()
Qt.callLater(() => forceDockLayoutRefresh())
}
function setDockSpacing(spacing) {
dockSpacing = spacing
saveSettings()
}
function setDockBottomGap(gap) {
dockBottomGap = gap
saveSettings()
}
function setDockOpenOnOverview(enabled) {
dockOpenOnOverview = enabled
saveSettings()
}
function getPopupYPosition(barHeight) {
const gothOffset = dankBarGothCornersEnabled ? Theme.cornerRadius : 0
return barHeight + dankBarSpacing + dankBarBottomGap - gothOffset + Theme.popupDistance
}
function getPopupTriggerPosition(globalPos, screen, barThickness, widgetWidth) {
const screenX = screen ? screen.x : 0
const screenY = screen ? screen.y : 0
const relativeX = globalPos.x - screenX
const relativeY = globalPos.y - screenY
if (dankBarPosition === SettingsData.Position.Left || dankBarPosition === SettingsData.Position.Right) {
return {
x: relativeY,
y: barThickness + dankBarSpacing + Theme.popupDistance,
width: widgetWidth
}
}
return {
x: relativeX,
y: barThickness + dankBarSpacing + dankBarBottomGap + Theme.popupDistance,
width: widgetWidth
}
}
function setLockScreenShowPowerActions(enabled) {
lockScreenShowPowerActions = enabled
saveSettings()
@@ -1054,6 +1347,14 @@ Singleton {
saveSettings()
}
function setSurfaceBase(base) {
surfaceBase = base
saveSettings()
if (typeof Theme !== "undefined") {
Theme.generateSystemThemesFromCurrentTheme()
}
}
function setScreenPreferences(prefs) {
screenPreferences = prefs
saveSettings()
@@ -1067,14 +1368,48 @@ Singleton {
return Quickshell.screens.filter(screen => prefs.includes(screen.name))
}
// Plugin settings functions
function getPluginSetting(pluginId, key, defaultValue) {
if (!pluginSettings[pluginId]) {
return defaultValue
}
return pluginSettings[pluginId][key] !== undefined ? pluginSettings[pluginId][key] : defaultValue
}
function setPluginSetting(pluginId, key, value) {
if (!pluginSettings[pluginId]) {
pluginSettings[pluginId] = {}
}
pluginSettings[pluginId][key] = value
savePluginSettings()
}
function removePluginSettings(pluginId) {
if (pluginSettings[pluginId]) {
delete pluginSettings[pluginId]
savePluginSettings()
}
}
function getPluginSettingsForPlugin(pluginId) {
return pluginSettings[pluginId] || {}
}
function setAnimationSpeed(speed) {
animationSpeed = speed
saveSettings()
}
function _shq(s) {
return "'" + String(s).replace(/'/g, "'\\''") + "'"
}
Component.onCompleted: {
loadSettings()
fontCheckTimer.start()
initializeListModels()
if (!isGreeterMode) {
loadSettings()
fontCheckTimer.start()
initializeListModels()
}
}
ListModel {
@@ -1115,25 +1450,47 @@ Singleton {
FileView {
id: settingsFile
path: StandardPaths.writableLocation(StandardPaths.ConfigLocation) + "/DankMaterialShell/settings.json"
path: isGreeterMode ? "" : StandardPaths.writableLocation(StandardPaths.ConfigLocation) + "/DankMaterialShell/settings.json"
blockLoading: true
blockWrites: true
atomicWrites: true
watchChanges: true
watchChanges: !isGreeterMode
onLoaded: {
parseSettings(settingsFile.text())
hasTriedDefaultSettings = false
if (!isGreeterMode) {
parseSettings(settingsFile.text())
hasTriedDefaultSettings = false
}
}
onLoadFailed: error => {
if (!hasTriedDefaultSettings) {
if (!isGreeterMode && !hasTriedDefaultSettings) {
hasTriedDefaultSettings = true
defaultSettingsCheckProcess.running = true
} else {
} else if (!isGreeterMode) {
applyStoredTheme()
}
}
}
FileView {
id: pluginSettingsFile
path: isGreeterMode ? "" : pluginSettingsPath
blockLoading: true
blockWrites: true
atomicWrites: true
watchChanges: !isGreeterMode
onLoaded: {
if (!isGreeterMode) {
parsePluginSettings(pluginSettingsFile.text())
}
}
onLoadFailed: error => {
if (!isGreeterMode) {
pluginSettings = {}
}
}
}
Process {
id: systemDefaultDetectionProcess
@@ -1198,7 +1555,7 @@ Singleton {
id: defaultSettingsCheckProcess
command: ["sh", "-c", "CONFIG_DIR=\"" + _configDir
+ "/DankMaterialShell\"; if [ -f \"$CONFIG_DIR/default-settings.json\" ] && [ ! -f \"$CONFIG_DIR/settings.json\" ]; then cp \"$CONFIG_DIR/default-settings.json\" \"$CONFIG_DIR/settings.json\" && echo 'copied'; else echo 'not_found'; fi"]
+ "/DankMaterialShell\"; if [ -f \"$CONFIG_DIR/default-settings.json\" ] && [ ! -f \"$CONFIG_DIR/settings.json\" ]; then cp --no-preserve=mode \"$CONFIG_DIR/default-settings.json\" \"$CONFIG_DIR/settings.json\" && echo 'copied'; else echo 'not_found'; fi"]
running: false
onExited: exitCode => {
if (exitCode === 0) {
@@ -1213,22 +1570,22 @@ Singleton {
IpcHandler {
function reveal(): string {
root.setTopBarVisible(true)
root.setDankBarVisible(true)
return "BAR_SHOW_SUCCESS"
}
function hide(): string {
root.setTopBarVisible(false)
root.setDankBarVisible(false)
return "BAR_HIDE_SUCCESS"
}
function toggle(): string {
root.toggleTopBarVisible()
return topBarVisible ? "BAR_SHOW_SUCCESS" : "BAR_HIDE_SUCCESS"
root.toggleDankBarVisible()
return root.dankBarVisible ? "BAR_SHOW_SUCCESS" : "BAR_HIDE_SUCCESS"
}
function status(): string {
return topBarVisible ? "visible" : "hidden"
return root.dankBarVisible ? "visible" : "hidden"
}
target: "bar"

View File

@@ -2,99 +2,101 @@
// Separated from Theme.qml to keep that file clean
const CatppuccinMocha = {
surface: "#45475a",
surface: "#313244",
surfaceText: "#cdd6f4",
surfaceVariant: "#45475a",
surfaceVariant: "#313244",
surfaceVariantText: "#a6adc8",
background: "#1e1e2e",
backgroundText: "#cdd6f4",
outline: "#6c7086",
surfaceContainer: "#313244",
surfaceContainerHigh: "#585b70"
surfaceContainer: "#45475a",
surfaceContainerHigh: "#585b70",
surfaceContainerHighest: "#6c7086"
}
const CatppuccinLatte = {
surface: "#bcc0cc",
surface: "#e6e9ef",
surfaceText: "#4c4f69",
surfaceVariant: "#bcc0cc",
surfaceVariant: "#e6e9ef",
surfaceVariantText: "#6c6f85",
background: "#eff1f5",
backgroundText: "#4c4f69",
outline: "#9ca0b0",
surfaceContainer: "#ccd0da",
surfaceContainerHigh: "#acb0be"
surfaceContainer: "#dce0e8",
surfaceContainerHigh: "#ccd0da",
surfaceContainerHighest: "#bcc0cc"
}
const CatppuccinVariants = {
"cat-rosewater": {
name: "Rosewater",
dark: { primary: "#f5e0dc", secondary: "#f2cdcd", primaryText: "#1e1e2e", primaryContainer: "#8b6b5e", surfaceTint: "#f5e0dc" },
light: { primary: "#dc8a78", secondary: "#dd7878", primaryText: "#ffffff", primaryContainer: "#f4d2ca", surfaceTint: "#dc8a78" }
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: "#885d62", surfaceTint: "#f2cdcd" },
light: { primary: "#dd7878", secondary: "#dc8a78", primaryText: "#ffffff", primaryContainer: "#f4caca", surfaceTint: "#dd7878" }
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: "#8b537a", surfaceTint: "#f5c2e7" },
light: { primary: "#ea76cb", secondary: "#8839ef", primaryText: "#ffffff", primaryContainer: "#f7c9e7", surfaceTint: "#ea76cb" }
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: "#61378a", surfaceTint: "#cba6f7" },
light: { primary: "#8839ef", secondary: "#7287fd", primaryText: "#ffffff", primaryContainer: "#e4d3ff", surfaceTint: "#8839ef" }
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: "#891c3b", surfaceTint: "#f38ba8" },
light: { primary: "#d20f39", secondary: "#e64553", primaryText: "#ffffff", primaryContainer: "#f1b8c4", surfaceTint: "#d20f39" }
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: "#81313f", surfaceTint: "#eba0ac" },
light: { primary: "#e64553", secondary: "#d20f39", primaryText: "#ffffff", primaryContainer: "#f4c3c8", surfaceTint: "#e64553" }
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: "#90441a", surfaceTint: "#fab387" },
light: { primary: "#fe640b", secondary: "#df8e1d", primaryText: "#ffffff", primaryContainer: "#ffddcc", surfaceTint: "#fe640b" }
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: "#8f7342", surfaceTint: "#f9e2af" },
light: { primary: "#df8e1d", secondary: "#40a02b", primaryText: "#ffffff", primaryContainer: "#fff3cc", surfaceTint: "#df8e1d" }
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: "#3c7534", surfaceTint: "#a6e3a1" },
light: { primary: "#40a02b", secondary: "#179299", primaryText: "#ffffff", primaryContainer: "#d4f5d4", surfaceTint: "#40a02b" }
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: "#2a7468", surfaceTint: "#94e2d5" },
light: { primary: "#179299", secondary: "#04a5e5", primaryText: "#ffffff", primaryContainer: "#ccf2f2", surfaceTint: "#179299" }
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: "#196e7e", surfaceTint: "#89dceb" },
light: { primary: "#04a5e5", secondary: "#209fb5", primaryText: "#ffffff", primaryContainer: "#ccebff", surfaceTint: "#04a5e5" }
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: "#0a597f", surfaceTint: "#74c7ec" },
light: { primary: "#209fb5", secondary: "#1e66f5", primaryText: "#ffffff", primaryContainer: "#d0f0f5", surfaceTint: "#209fb5" }
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: "#19468d", surfaceTint: "#89b4fa" },
light: { primary: "#1e66f5", secondary: "#7287fd", primaryText: "#ffffff", primaryContainer: "#ccd9ff", surfaceTint: "#1e66f5" }
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: "#4a5091", surfaceTint: "#b4befe" },
light: { primary: "#7287fd", secondary: "#8839ef", primaryText: "#ffffff", primaryContainer: "#dde1ff", surfaceTint: "#7287fd" }
dark: { primary: "#b4befe", secondary: "#cba6f7", primaryText: "#1e1e2e", primaryContainer: "#3f4481", surfaceTint: "#b4befe" },
light: { primary: "#7287fd", secondary: "#8839ef", primaryText: "#ffffff", primaryContainer: "#e5e8ff", surfaceTint: "#7287fd" }
}
}
@@ -118,16 +120,17 @@ const StockThemes = {
primaryText: "#000000",
primaryContainer: "#0d47a1",
secondary: "#8ab4f8",
surface: "#1a1c1e",
surfaceText: "#e3e8ef",
surfaceVariant: "#44464f",
surfaceVariantText: "#c4c7c5",
surface: "#101418",
surfaceText: "#e0e2e8",
surfaceVariant: "#42474e",
surfaceVariantText: "#c2c7cf",
surfaceTint: "#8ab4f8",
background: "#1a1c1e",
backgroundText: "#e3e8ef",
outline: "#8e918f",
surfaceContainer: "#1e2023",
surfaceContainerHigh: "#292b2f"
background: "#101418",
backgroundText: "#e0e2e8",
outline: "#8c9199",
surfaceContainer: "#1d2024",
surfaceContainerHigh: "#272a2f",
surfaceContainerHighest: "#32353a"
},
purple: {
name: "Purple",
@@ -135,16 +138,17 @@ const StockThemes = {
primaryText: "#381E72",
primaryContainer: "#4F378B",
secondary: "#CCC2DC",
surface: "#10121E",
surfaceText: "#E6E0E9",
surfaceVariant: "#49454F",
surfaceVariantText: "#CAC4D0",
surface: "#141218",
surfaceText: "#e6e0e9",
surfaceVariant: "#49454e",
surfaceVariantText: "#cac4cf",
surfaceTint: "#D0BCFF",
background: "#10121E",
backgroundText: "#E6E0E9",
outline: "#938F99",
surfaceContainer: "#1D1B20",
surfaceContainerHigh: "#2B2930"
background: "#141218",
backgroundText: "#e6e0e9",
outline: "#948f99",
surfaceContainer: "#211f24",
surfaceContainerHigh: "#2b292f",
surfaceContainerHighest: "#36343a"
},
green: {
name: "Green",
@@ -152,16 +156,17 @@ const StockThemes = {
primaryText: "#000000",
primaryContainer: "#1b5e20",
secondary: "#81c995",
surface: "#0f1411",
surfaceText: "#e1f5e3",
surfaceVariant: "#404943",
surfaceVariantText: "#c1cbc4",
surface: "#10140f",
surfaceText: "#e0e4db",
surfaceVariant: "#424940",
surfaceVariantText: "#c2c9bd",
surfaceTint: "#81c995",
background: "#0f1411",
backgroundText: "#e1f5e3",
outline: "#8b938c",
surfaceContainer: "#1a1f1b",
surfaceContainerHigh: "#252a26"
background: "#10140f",
backgroundText: "#e0e4db",
outline: "#8c9388",
surfaceContainer: "#1d211b",
surfaceContainerHigh: "#272b25",
surfaceContainerHighest: "#323630"
},
orange: {
name: "Orange",
@@ -169,16 +174,17 @@ const StockThemes = {
primaryText: "#000000",
primaryContainer: "#3e2723",
secondary: "#ffb74d",
surface: "#1c1410",
surfaceText: "#f5f1ea",
surfaceVariant: "#4a453a",
surfaceVariantText: "#cbc5b8",
surface: "#1a120e",
surfaceText: "#f0dfd8",
surfaceVariant: "#52443d",
surfaceVariantText: "#d7c2b9",
surfaceTint: "#ffb74d",
background: "#1c1410",
backgroundText: "#f5f1ea",
outline: "#958f84",
surfaceContainer: "#211e17",
surfaceContainerHigh: "#2c291f"
background: "#1a120e",
backgroundText: "#f0dfd8",
outline: "#a08d85",
surfaceContainer: "#271e1a",
surfaceContainerHigh: "#322824",
surfaceContainerHighest: "#3d332e"
},
red: {
name: "Red",
@@ -186,16 +192,17 @@ const StockThemes = {
primaryText: "#000000",
primaryContainer: "#4a0e0e",
secondary: "#f28b82",
surface: "#1c1011",
surfaceText: "#f5e8ea",
surfaceVariant: "#4a3f41",
surfaceVariantText: "#cbc2c4",
surface: "#1a1110",
surfaceText: "#f1dedc",
surfaceVariant: "#534341",
surfaceVariantText: "#d8c2be",
surfaceTint: "#f28b82",
background: "#1c1011",
backgroundText: "#f5e8ea",
outline: "#958b8d",
surfaceContainer: "#211b1c",
surfaceContainerHigh: "#2c2426"
background: "#1a1110",
backgroundText: "#f1dedc",
outline: "#a08c89",
surfaceContainer: "#271d1c",
surfaceContainerHigh: "#322826",
surfaceContainerHighest: "#3d3231"
},
cyan: {
name: "Cyan",
@@ -203,16 +210,17 @@ const StockThemes = {
primaryText: "#000000",
primaryContainer: "#004d5c",
secondary: "#4dd0e1",
surface: "#0f1617",
surfaceText: "#e8f4f5",
surfaceVariant: "#3f474a",
surfaceVariantText: "#c2c9cb",
surface: "#0e1416",
surfaceText: "#dee3e5",
surfaceVariant: "#3f484a",
surfaceVariantText: "#bfc8ca",
surfaceTint: "#4dd0e1",
background: "#0f1617",
backgroundText: "#e8f4f5",
outline: "#8c9194",
surfaceContainer: "#1a1f20",
surfaceContainerHigh: "#252b2c"
background: "#0e1416",
backgroundText: "#dee3e5",
outline: "#899295",
surfaceContainer: "#1b2122",
surfaceContainerHigh: "#252b2c",
surfaceContainerHighest: "#303637"
},
pink: {
name: "Pink",
@@ -220,16 +228,17 @@ const StockThemes = {
primaryText: "#000000",
primaryContainer: "#4a0e2f",
secondary: "#f8bbd9",
surface: "#1a1014",
surfaceText: "#f3e8ee",
surfaceVariant: "#483f45",
surfaceVariantText: "#c9c2c7",
surface: "#191112",
surfaceText: "#f0dee0",
surfaceVariant: "#524345",
surfaceVariantText: "#d6c2c3",
surfaceTint: "#f8bbd9",
background: "#1a1014",
backgroundText: "#f3e8ee",
outline: "#938a90",
surfaceContainer: "#1f1b1e",
surfaceContainerHigh: "#2a2428"
background: "#191112",
backgroundText: "#f0dee0",
outline: "#9f8c8e",
surfaceContainer: "#261d1e",
surfaceContainerHigh: "#312829",
surfaceContainerHighest: "#3c3233"
},
amber: {
name: "Amber",
@@ -237,16 +246,17 @@ const StockThemes = {
primaryText: "#000000",
primaryContainer: "#4a3c00",
secondary: "#ffd54f",
surface: "#1a1710",
surfaceText: "#f3f0e8",
surfaceVariant: "#49453a",
surfaceVariantText: "#cac5b8",
surface: "#17130b",
surfaceText: "#ebe1d4",
surfaceVariant: "#4d4639",
surfaceVariantText: "#d0c5b4",
surfaceTint: "#ffd54f",
background: "#1a1710",
backgroundText: "#f3f0e8",
outline: "#949084",
surfaceContainer: "#1f1e17",
surfaceContainerHigh: "#2a281f"
background: "#17130b",
backgroundText: "#ebe1d4",
outline: "#998f80",
surfaceContainer: "#231f17",
surfaceContainerHigh: "#2e2921",
surfaceContainerHighest: "#39342b"
},
coral: {
name: "Coral",
@@ -255,15 +265,16 @@ const StockThemes = {
primaryContainer: "#8c1d18",
secondary: "#f9dedc",
surface: "#1a1110",
surfaceText: "#f1e8e7",
surfaceVariant: "#4a4142",
surfaceVariantText: "#cdc2c1",
surfaceText: "#f1dedc",
surfaceVariant: "#534341",
surfaceVariantText: "#d8c2bf",
surfaceTint: "#ffb4ab",
background: "#1a1110",
backgroundText: "#f1e8e7",
outline: "#968b8a",
surfaceContainer: "#201a19",
surfaceContainerHigh: "#2b2221"
backgroundText: "#f1dedc",
outline: "#a08c8a",
surfaceContainer: "#271d1c",
surfaceContainerHigh: "#322826",
surfaceContainerHighest: "#3d3231"
},
monochrome: {
name: "Monochrome",
@@ -279,8 +290,9 @@ const StockThemes = {
background: "#131315",
backgroundText: "#e4e2e3",
outline: "#929092",
surfaceContainer: "#2a2a2a",
surfaceContainerHigh: "#2a2a2b",
surfaceContainer: "#353535",
surfaceContainerHigh: "#424242",
surfaceContainerHighest: "#505050",
error: "#ffb4ab",
warning: "#3f4759",
info: "#595e6c",
@@ -294,16 +306,17 @@ const StockThemes = {
primaryText: "#ffffff",
primaryContainer: "#e3f2fd",
secondary: "#42a5f5",
surface: "#fefefe",
surfaceText: "#1a1c1e",
surfaceVariant: "#e7e0ec",
surfaceVariantText: "#49454f",
surface: "#f7f9ff",
surfaceText: "#181c20",
surfaceVariant: "#dee3eb",
surfaceVariantText: "#42474e",
surfaceTint: "#1976d2",
background: "#fefefe",
backgroundText: "#1a1c1e",
outline: "#79747e",
surfaceContainer: "#f3f3f3",
surfaceContainerHigh: "#ececec"
background: "#f7f9ff",
backgroundText: "#181c20",
outline: "#72777f",
surfaceContainer: "#eceef4",
surfaceContainerHigh: "#e6e8ee",
surfaceContainerHighest: "#e0e2e8"
},
purple: {
name: "Purple Light",
@@ -311,16 +324,17 @@ const StockThemes = {
primaryText: "#ffffff",
primaryContainer: "#EADDFF",
secondary: "#625B71",
surface: "#FFFBFE",
surfaceText: "#1C1B1F",
surfaceVariant: "#E7E0EC",
surfaceVariantText: "#49454F",
surface: "#fef7ff",
surfaceText: "#1d1b20",
surfaceVariant: "#e7e0eb",
surfaceVariantText: "#49454e",
surfaceTint: "#6750A4",
background: "#FFFBFE",
backgroundText: "#1C1B1F",
outline: "#79747E",
surfaceContainer: "#F3EDF7",
surfaceContainerHigh: "#ECE6F0"
background: "#fef7ff",
backgroundText: "#1d1b20",
outline: "#7a757f",
surfaceContainer: "#f2ecf4",
surfaceContainerHigh: "#ece6ee",
surfaceContainerHighest: "#e6e0e9"
},
green: {
name: "Green Light",
@@ -328,16 +342,17 @@ const StockThemes = {
primaryText: "#ffffff",
primaryContainer: "#e8f5e8",
secondary: "#4caf50",
surface: "#fefefe",
surfaceText: "#1a1c1e",
surfaceVariant: "#e7e0ec",
surfaceVariantText: "#49454f",
surface: "#f7fbf1",
surfaceText: "#191d17",
surfaceVariant: "#dee5d8",
surfaceVariantText: "#424940",
surfaceTint: "#2e7d32",
background: "#fefefe",
backgroundText: "#1a1c1e",
outline: "#79747e",
surfaceContainer: "#f3f3f3",
surfaceContainerHigh: "#ececec"
background: "#f7fbf1",
backgroundText: "#191d17",
outline: "#72796f",
surfaceContainer: "#ecefe6",
surfaceContainerHigh: "#e6e9e0",
surfaceContainerHighest: "#e0e4db"
},
orange: {
name: "Orange Light",
@@ -345,16 +360,17 @@ const StockThemes = {
primaryText: "#ffffff",
primaryContainer: "#ffecb3",
secondary: "#ff9800",
surface: "#fefefe",
surfaceText: "#1a1c1e",
surfaceVariant: "#e7e0ec",
surfaceVariantText: "#49454f",
surface: "#fff8f6",
surfaceText: "#221a16",
surfaceVariant: "#f4ded5",
surfaceVariantText: "#52443d",
surfaceTint: "#e65100",
background: "#fefefe",
backgroundText: "#1a1c1e",
outline: "#79747e",
surfaceContainer: "#f3f3f3",
surfaceContainerHigh: "#ececec"
background: "#fff8f6",
backgroundText: "#221a16",
outline: "#85736c",
surfaceContainer: "#fceae3",
surfaceContainerHigh: "#f6e5de",
surfaceContainerHighest: "#f0dfd8"
},
red: {
name: "Red Light",
@@ -362,16 +378,17 @@ const StockThemes = {
primaryText: "#ffffff",
primaryContainer: "#ffebee",
secondary: "#f44336",
surface: "#fefefe",
surfaceText: "#1a1c1e",
surfaceVariant: "#e7e0ec",
surfaceVariantText: "#49454f",
surface: "#fff8f7",
surfaceText: "#231918",
surfaceVariant: "#f5ddda",
surfaceVariantText: "#534341",
surfaceTint: "#d32f2f",
background: "#fefefe",
backgroundText: "#1a1c1e",
outline: "#79747e",
surfaceContainer: "#f3f3f3",
surfaceContainerHigh: "#ececec"
background: "#fff8f7",
backgroundText: "#231918",
outline: "#857370",
surfaceContainer: "#fceae7",
surfaceContainerHigh: "#f7e4e1",
surfaceContainerHighest: "#f1dedc"
},
cyan: {
name: "Cyan Light",
@@ -379,16 +396,17 @@ const StockThemes = {
primaryText: "#ffffff",
primaryContainer: "#e0f2f1",
secondary: "#00bcd4",
surface: "#fefefe",
surfaceText: "#1a1c1e",
surfaceVariant: "#e7e0ec",
surfaceVariantText: "#49454f",
surface: "#f5fafc",
surfaceText: "#171d1e",
surfaceVariant: "#dbe4e6",
surfaceVariantText: "#3f484a",
surfaceTint: "#0097a7",
background: "#fefefe",
backgroundText: "#1a1c1e",
outline: "#79747e",
surfaceContainer: "#f3f3f3",
surfaceContainerHigh: "#ececec"
background: "#f5fafc",
backgroundText: "#171d1e",
outline: "#6f797b",
surfaceContainer: "#e9eff0",
surfaceContainerHigh: "#e3e9eb",
surfaceContainerHighest: "#dee3e5"
},
pink: {
name: "Pink Light",
@@ -396,16 +414,17 @@ const StockThemes = {
primaryText: "#ffffff",
primaryContainer: "#fce4ec",
secondary: "#e91e63",
surface: "#fefefe",
surfaceText: "#1a1c1e",
surfaceVariant: "#e7e0ec",
surfaceVariantText: "#49454f",
surface: "#fff8f7",
surfaceText: "#22191a",
surfaceVariant: "#f3dddf",
surfaceVariantText: "#524345",
surfaceTint: "#c2185b",
background: "#fefefe",
backgroundText: "#1a1c1e",
outline: "#79747e",
surfaceContainer: "#f3f3f3",
surfaceContainerHigh: "#ececec"
background: "#fff8f7",
backgroundText: "#22191a",
outline: "#847375",
surfaceContainer: "#fbeaeb",
surfaceContainerHigh: "#f5e4e5",
surfaceContainerHighest: "#f0dee0"
},
amber: {
name: "Amber Light",
@@ -413,16 +432,17 @@ const StockThemes = {
primaryText: "#000000",
primaryContainer: "#fff8e1",
secondary: "#ffc107",
surface: "#fefefe",
surfaceText: "#1a1c1e",
surfaceVariant: "#e7e0ec",
surfaceVariantText: "#49454f",
surface: "#fff8f2",
surfaceText: "#1f1b13",
surfaceVariant: "#ede1cf",
surfaceVariantText: "#4d4639",
surfaceTint: "#ff8f00",
background: "#fefefe",
backgroundText: "#1a1c1e",
outline: "#79747e",
surfaceContainer: "#f3f3f3",
surfaceContainerHigh: "#ececec"
background: "#fff8f2",
backgroundText: "#1f1b13",
outline: "#7f7667",
surfaceContainer: "#f6ecdf",
surfaceContainerHigh: "#f1e7d9",
surfaceContainerHighest: "#ebe1d4"
},
coral: {
name: "Coral Light",
@@ -430,16 +450,17 @@ const StockThemes = {
primaryText: "#ffffff",
primaryContainer: "#ffdad6",
secondary: "#ff5449",
surface: "#fefefe",
surfaceText: "#1a1c1e",
surfaceVariant: "#e7e0ec",
surfaceVariantText: "#49454f",
surface: "#fff8f7",
surfaceText: "#231918",
surfaceVariant: "#f5ddda",
surfaceVariantText: "#534341",
surfaceTint: "#8c1d18",
background: "#fefefe",
backgroundText: "#1a1c1e",
outline: "#79747e",
surfaceContainer: "#f3f3f3",
surfaceContainerHigh: "#ececec"
background: "#fff8f7",
backgroundText: "#231918",
outline: "#857371",
surfaceContainer: "#fceae7",
surfaceContainerHigh: "#f6e4e2",
surfaceContainerHighest: "#f1dedc"
},
monochrome: {
name: "Monochrome Light",
@@ -455,8 +476,9 @@ const StockThemes = {
background: "#ffffff",
backgroundText: "#1a1a1a",
outline: "#757577",
surfaceContainer: "#f5f5f6",
surfaceContainerHigh: "#eaeaeb",
surfaceContainer: "#e8e8ea",
surfaceContainerHigh: "#dcdcde",
surfaceContainerHighest: "#d0d0d2",
error: "#ba1a1a",
warning: "#f9e79f",
info: "#5d6475",

View File

@@ -16,9 +16,12 @@ Singleton {
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: false
property bool isLightMode: typeof SessionData !== "undefined" ? SessionData.isLightMode : false
readonly property string dynamic: "dynamic"
readonly property string custom : "custom"
@@ -28,9 +31,8 @@ Singleton {
readonly property string shellDir: Paths.strip(Qt.resolvedUrl(".").toString()).replace("/Common/", "")
readonly property string wallpaperPath: {
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)
@@ -72,7 +74,6 @@ Singleton {
property bool qtThemingEnabled: typeof SettingsData !== "undefined" ? (SettingsData.qt5ctAvailable || SettingsData.qt6ctAvailable) : false
property var workerRunning: false
property var matugenColors: ({})
property bool extractionRequested: false
property int colorUpdateTrigger: 0
property var customThemeData: null
@@ -81,11 +82,20 @@ Singleton {
Component.onCompleted: {
Quickshell.execDetached(["mkdir", "-p", stateDir])
matugenCheck.running = true
if (typeof SessionData !== "undefined")
if (typeof SessionData !== "undefined") {
SessionData.isLightModeChanged.connect(root.onLightModeChanged)
isLightMode = SessionData.isLightMode
}
if (typeof SettingsData !== "undefined" && SettingsData.currentThemeName) {
switchTheme(SettingsData.currentThemeName, false)
switchTheme(SettingsData.currentThemeName, false, false)
}
}
function applyGreeterTheme(themeName) {
switchTheme(themeName, false, false)
if (themeName === dynamic && dynamicColorsFileView.path) {
dynamicColorsFileView.reload()
}
}
@@ -120,6 +130,7 @@ Singleton {
"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",
@@ -154,7 +165,12 @@ Singleton {
property color primaryText: currentThemeData.primaryText
property color primaryContainer: currentThemeData.primaryContainer
property color secondary: currentThemeData.secondary
property color surface: currentThemeData.surface
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
@@ -163,8 +179,24 @@ Singleton {
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: currentThemeData.surfaceContainer
property color surfaceContainerHigh: currentThemeData.surfaceContainerHigh
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
@@ -209,11 +241,22 @@ Singleton {
property color shadowMedium: Qt.rgba(0, 0, 0, 0.08)
property color shadowStrong: Qt.rgba(0, 0, 0, 0.3)
property int shorterDuration: 100
property int shortDuration: 150
property int mediumDuration: 300
property int longDuration: 500
property int extraLongDuration: 1000
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
@@ -233,8 +276,8 @@ Singleton {
property real iconSizeLarge: 32
property real panelTransparency: 0.85
property real widgetTransparency: typeof SettingsData !== "undefined" && SettingsData.topBarWidgetTransparency !== undefined ? SettingsData.topBarWidgetTransparency : 0.85
property real popupTransparency: typeof SettingsData !== "undefined" && SettingsData.popupTransparency !== undefined ? SettingsData.popupTransparency : 0.92
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()
@@ -243,11 +286,15 @@ Singleton {
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
extractColors()
} else if (themeName === custom) {
currentTheme = custom
currentThemeCategory = custom
@@ -256,34 +303,45 @@ Singleton {
}
} else {
currentTheme = themeName
// Determine category based on theme name
if (StockThemes.isCatppuccinVariant(themeName)) {
currentThemeCategory = "catppuccin"
} else {
currentThemeCategory = "generic"
}
}
if (savePrefs && typeof SettingsData !== "undefined")
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode)
if (savePrefs && typeof SettingsData !== "undefined" && !isGreeterMode)
SettingsData.setTheme(currentTheme)
generateSystemThemesFromCurrentTheme()
if (!isGreeterMode) {
generateSystemThemesFromCurrentTheme()
}
}
function setLightMode(light, savePrefs = true) {
screenTransition()
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")
if (savePrefs && typeof SessionData !== "undefined" && !isGreeterMode)
SessionData.setLightMode(isLightMode)
PortalService.setLightMode(isLightMode)
generateSystemThemesFromCurrentTheme()
if (!isGreeterMode) {
PortalService.setLightMode(isLightMode)
generateSystemThemesFromCurrentTheme()
}
}
function toggleLightMode(savePrefs = true) {
setLightMode(!isLightMode, savePrefs)
setLightMode(!isLightMode, savePrefs, true)
}
function forceGenerateSystemThemes() {
screenTransition()
if (!matugenAvailable) {
return
}
@@ -307,8 +365,10 @@ Singleton {
}
function switchThemeCategory(category, defaultTheme) {
currentThemeCategory = category
switchTheme(defaultTheme, true, false)
screenTransition()
themeCategoryTransitionTimer.category = category
themeCategoryTransitionTimer.defaultTheme = defaultTheme
themeCategoryTransitionTimer.restart()
}
function getCatppuccinColor(variantName) {
@@ -332,7 +392,6 @@ Singleton {
}
function loadCustomTheme(themeData) {
screenTransition()
if (themeData.dark || themeData.light) {
const colorMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark"
const selectedTheme = themeData[colorMode] || themeData.dark || themeData.light
@@ -367,7 +426,7 @@ Singleton {
property real notepadTransparency: SettingsData.notepadTransparencyOverride >= 0 ? SettingsData.notepadTransparencyOverride : popupTransparency
property var widgetBaseBackgroundColor: {
const colorMode = typeof SettingsData !== "undefined" ? SettingsData.widgetBackgroundColor : "sth"
const colorMode = typeof SettingsData !== "undefined" ? SettingsData.widgetBackgroundColor : "sch"
switch (colorMode) {
case "s":
return surface
@@ -388,7 +447,7 @@ Singleton {
}
property var widgetBackground: {
const colorMode = typeof SettingsData !== "undefined" ? SettingsData.widgetBackgroundColor : "sth"
const colorMode = typeof SettingsData !== "undefined" ? SettingsData.widgetBackgroundColor : "sch"
switch (colorMode) {
case "s":
return Qt.rgba(surface.r, surface.g, surface.b, widgetTransparency)
@@ -414,6 +473,19 @@ Singleton {
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()
@@ -504,17 +576,6 @@ Singleton {
}
}
function extractColors() {
extractionRequested = true
if (matugenAvailable)
if (rawWallpaperPath.startsWith("we:")) {
fileCheckerTimer.start()
} else {
fileChecker.running = true
}
else
matugenCheck.running = true
}
function onLightModeChanged() {
if (matugenColors && Object.keys(matugenColors).length > 0) {
@@ -541,7 +602,8 @@ Singleton {
"value": value,
"mode": isLight ? "light" : "dark",
"iconTheme": iconTheme || "System Default",
"matugenType": matugenType || "scheme-tonal-spot"
"matugenType": matugenType || "scheme-tonal-spot",
"surfaceBase": (typeof SettingsData !== "undefined" && SettingsData.surfaceBase) ? SettingsData.surfaceBase : "sc"
}
const json = JSON.stringify(desired)
@@ -553,16 +615,17 @@ Singleton {
console.log("calling matugen worker")
systemThemeGenerator.command = [
"sh", "-c",
`sleep 1 && ${shellDir}/scripts/matugen-worker.sh '${stateDir}' '${shellDir}' --run`
`sleep 1 && ${shellDir}/scripts/matugen-worker.sh '${stateDir}' '${shellDir}' '${configDir}' --run`
]
} else {
systemThemeGenerator.command = [shellDir + "/scripts/matugen-worker.sh", stateDir, shellDir, "--run"]
systemThemeGenerator.command = [shellDir + "/scripts/matugen-worker.sh", stateDir, shellDir, configDir, "--run"]
}
systemThemeGenerator.running = true
}
function generateSystemThemesFromCurrentTheme() {
if (!matugenAvailable)
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode)
if (!matugenAvailable || isGreeterMode)
return
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode)
@@ -626,78 +689,66 @@ Singleton {
qtApplier.running = true
}
function extractJsonFromText(text) {
if (!text)
return null
function withAlpha(c, a) { return Qt.rgba(c.r, c.g, c.b, a); }
const start = text.search(/[{\[]/)
if (start === -1)
return null
const open = text[start]
const pairs = {
"{": '}',
"[": ']'
}
const close = pairs[open]
if (!close)
return null
let inString = false
let escape = false
const stack = [open]
for (var i = start + 1; i < text.length; i++) {
const ch = text[i]
if (inString) {
if (escape) {
escape = false
} else if (ch === '\\') {
escape = true
} else if (ch === '"') {
inString = false
}
continue
}
if (ch === '"') {
inString = true
continue
}
if (ch === '{' || ch === '[') {
stack.push(ch)
continue
}
if (ch === '}' || ch === ']') {
const last = stack.pop()
if (!last || pairs[last] !== ch) {
return null
}
if (stack.length === 0) {
return text.slice(start, i + 1)
}
}
}
return null
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 ""
return colorOverride
}
property string effectiveLogoColor: {
if (typeof SettingsData === "undefined") return ""
const colorOverride = SettingsData.launcherLogoColorOverride
if (!colorOverride || colorOverride === "") return ""
if (!SettingsData.launcherLogoColorInvertOnMode) {
return colorOverride
}
if (typeof SessionData !== "undefined" && SessionData.isLightMode) {
return invertHex(baseLogoColor)
}
return baseLogoColor
}
Process {
id: matugenCheck
command: ["which", "matugen"]
onExited: code => {
matugenAvailable = (code === 0) && !envDisableMatugen
if (!matugenAvailable) {
console.log("matugen not not available in path or disabled via DMS_DISABLE_MATUGEN")
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode)
if (!matugenAvailable || isGreeterMode) {
return
}
if (extractionRequested) {
if (rawWallpaperPath.startsWith("we:")) {
fileCheckerTimer.start()
} else {
fileChecker.running = true
}
}
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode)
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default"
@@ -733,128 +784,7 @@ Singleton {
}
}
Process {
id: fileChecker
command: ["test", "-r", wallpaperPath]
onExited: code => {
if (code === 0) {
matugenProcess.running = true
} else if (wallpaperPath.startsWith("#")) {
colorMatugenProcess.running = true
}
}
}
Timer {
id: fileCheckerTimer
interval: 1000
repeat: false
onTriggered: {
fileChecker.running = true
}
}
Process {
id: matugenProcess
command: {
const scheme = (typeof SettingsData !== "undefined" && SettingsData.matugenScheme) ? SettingsData.matugenScheme : "scheme-tonal-spot"
return ["matugen", "image", wallpaperPath, "--json", "hex", "-t", scheme]
}
stdout: StdioCollector {
id: matugenCollector
onStreamFinished: {
if (!matugenCollector.text) {
if (typeof ToastService !== "undefined") {
ToastService.wallpaperErrorStatus = "error"
ToastService.showError("Wallpaper Processing Failed: Empty JSON extracted from matugen output.")
}
return
}
const extractedJson = extractJsonFromText(matugenCollector.text)
if (!extractedJson) {
if (typeof ToastService !== "undefined") {
ToastService.wallpaperErrorStatus = "error"
ToastService.showError("Wallpaper Processing Failed: Invalid JSON extracted from matugen output.")
}
console.log("Raw matugen output:", matugenCollector.text)
return
}
try {
root.matugenColors = JSON.parse(extractedJson)
root.colorUpdateTrigger++
if (typeof ToastService !== "undefined") {
ToastService.clearWallpaperError()
}
} catch (e) {
if (typeof ToastService !== "undefined") {
ToastService.wallpaperErrorStatus = "error"
ToastService.showError("Wallpaper processing failed (JSON parse error after extraction)")
}
}
}
}
onExited: code => {
if (code !== 0) {
if (typeof ToastService !== "undefined") {
ToastService.wallpaperErrorStatus = "error"
ToastService.showError("Matugen command failed with exit code " + code)
}
}
}
}
Process {
id: colorMatugenProcess
command: {
const scheme = (typeof SettingsData !== "undefined" && SettingsData.matugenScheme) ? SettingsData.matugenScheme : "scheme-tonal-spot"
return ["matugen", "color", "hex", wallpaperPath, "--json", "hex", "-t", scheme]
}
stdout: StdioCollector {
id: colorMatugenCollector
onStreamFinished: {
if (!colorMatugenCollector.text) {
if (typeof ToastService !== "undefined") {
ToastService.wallpaperErrorStatus = "error"
ToastService.showError("Color Processing Failed: Empty JSON extracted from matugen output.")
}
return
}
const extractedJson = extractJsonFromText(colorMatugenCollector.text)
if (!extractedJson) {
if (typeof ToastService !== "undefined") {
ToastService.wallpaperErrorStatus = "error"
ToastService.showError("Color Processing Failed: Invalid JSON extracted from matugen output.")
}
console.log("Raw matugen output:", colorMatugenCollector.text)
return
}
try {
root.matugenColors = JSON.parse(extractedJson)
root.colorUpdateTrigger++
if (typeof ToastService !== "undefined") {
ToastService.clearWallpaperError()
}
} catch (e) {
if (typeof ToastService !== "undefined") {
ToastService.wallpaperErrorStatus = "error"
ToastService.showError("Color processing failed (JSON parse error after extraction)")
}
}
}
}
onExited: code => {
if (code !== 0) {
if (typeof ToastService !== "undefined") {
ToastService.wallpaperErrorStatus = "error"
ToastService.showError("Matugen color command failed with exit code " + code)
}
}
}
}
Process {
id: ensureStateDir
@@ -867,10 +797,7 @@ Singleton {
onExited: exitCode => {
workerRunning = false
if (exitCode === 2) {
// Exit code 2 means wallpaper/color not found - this is expected on first run
console.log("Theme worker: wallpaper/color not found, skipping theme generation")
} else if (exitCode !== 0) {
if (exitCode !== 0 && exitCode !== 2) {
if (typeof ToastService !== "undefined") {
ToastService.showError("Theme worker failed (" + exitCode + ")")
}
@@ -957,21 +884,70 @@ Singleton {
}
}
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 ? "light" : "dark"
return root.isLightMode ? "dark" : "light"
}
function light(): string {
root.setLightMode(true)
root.setLightMode(true, true, true)
return "light"
}
function dark(): string {
root.setLightMode(false)
root.setLightMode(false, true, true)
return "dark"
}
@@ -979,4 +955,35 @@ Singleton {
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)
}
}
}

29
DMSGreeter.qml Normal file
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
DMSShell.qml Normal file
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: qsTr("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
DMSShellIPC.qml Normal file
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"
}
}

View File

@@ -77,15 +77,12 @@ Item {
width: parent.width
height: parent.height - ClipboardConstants.headerHeight - 70
radius: Theme.cornerRadius
color: Theme.surfaceLight
border.color: Theme.outlineLight
border.width: 1
color: "transparent"
clip: true
DankListView {
id: clipboardListView
anchors.fill: parent
anchors.margins: Theme.spacingS
model: filteredModel
currentIndex: clipboardContent.modal ? clipboardContent.modal.selectedIndex : 0
@@ -119,7 +116,7 @@ Item {
}
StyledText {
text: "No clipboard entries found"
text: qsTr("No clipboard entries found")
anchors.centerIn: parent
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText

View File

@@ -24,17 +24,10 @@ Rectangle {
radius: Theme.cornerRadius
color: {
if (isSelected) {
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.2)
return Theme.primaryPressed
}
return mouseArea.containsMouse ? Theme.primaryHover : Theme.primaryBackground
return mouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceContainerHigh
}
border.color: {
if (isSelected) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.5)
}
return Theme.outlineStrong
}
border.width: isSelected ? 1.5 : 1
Row {
anchors.fill: parent

View File

@@ -53,7 +53,7 @@ QtObject {
modal.hide()
event.accepted = true
}
} else if (event.key === Qt.Key_Down) {
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_Tab) {
if (!modal.keyboardNavigationActive) {
modal.keyboardNavigationActive = true
modal.selectedIndex = 0
@@ -62,7 +62,7 @@ QtObject {
selectNext()
event.accepted = true
}
} else if (event.key === Qt.Key_Up) {
} else if (event.key === Qt.Key_Up || event.key === Qt.Key_Backtab) {
if (!modal.keyboardNavigationActive) {
modal.keyboardNavigationActive = true
modal.selectedIndex = 0
@@ -74,6 +74,42 @@ QtObject {
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()

View File

@@ -26,7 +26,7 @@ Rectangle {
}
StyledText {
text: "Shift+Del: Clear All • Esc: Close"
text: qsTr("Shift+Del: Clear All • Esc: Close")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter

View File

@@ -1,37 +0,0 @@
import QtQuick
import Qt.labs.platform
import Quickshell
import qs.Common
import qs.Services
Item {
id: colorPickerModal
signal colorSelected(color selectedColor)
function show() {
colorDialog.open()
}
function hide() {
colorDialog.close()
}
function copyColorToClipboard(colorValue) {
Quickshell.execDetached(["sh", "-c", `echo "${colorValue}" | wl-copy`])
ToastService.showInfo(`Color ${colorValue} copied to clipboard`)
console.log("Copied color to clipboard:", colorValue)
}
ColorDialog {
id: colorDialog
title: "Color Picker - Select and copy color"
color: Theme.primary
onAccepted: {
const colorString = color.toString()
copyColorToClipboard(colorString)
colorSelected(color)
}
}
}

View File

@@ -93,6 +93,48 @@ DankModal {
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

View File

@@ -1,5 +1,4 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Wayland
import qs.Common
@@ -15,6 +14,15 @@ PanelWindow {
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"
@@ -22,7 +30,7 @@ PanelWindow {
property bool closeOnEscapeKey: true
property bool closeOnBackgroundClick: true
property string animationType: "scale"
property int animationDuration: Theme.shorterDuration
property int animationDuration: Theme.shortDuration
property var animationEasing: Theme.emphasizedEasing
property color backgroundColor: Theme.surfaceContainer
property color borderColor: Theme.outlineMedium
@@ -34,6 +42,7 @@ PanelWindow {
property bool shouldHaveFocus: shouldBeVisible
property bool allowFocusOverride: false
property bool allowStacking: false
property bool keepContentLoaded: false
signal opened
signal dialogClosed
@@ -90,7 +99,7 @@ PanelWindow {
Timer {
id: closeTimer
interval: animationDuration + 50
interval: animationDuration + 100
onTriggered: {
visible = false
}
@@ -133,22 +142,26 @@ PanelWindow {
Rectangle {
id: contentContainer
width: root.width
height: root.height
anchors.centerIn: positioning === "center" ? parent : undefined
width: px(root.width)
height: px(root.height)
anchors.centerIn: undefined
x: {
if (positioning === "top-right") {
return Math.max(Theme.spacingL, root.screenWidth - width - Theme.spacingL)
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 root.customPosition.x
return snap(root.customPosition.x)
}
return 0
}
y: {
if (positioning === "top-right") {
return Theme.barHeight + Theme.spacingXS
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 root.customPosition.y
return snap(root.customPosition.y)
}
return 0
}
@@ -156,50 +169,48 @@ PanelWindow {
radius: root.cornerRadius
border.color: root.borderColor
border.width: root.borderWidth
layer.enabled: root.enableShadow
clip: false
opacity: root.shouldBeVisible ? 1 : 0
scale: root.animationType === "scale" ? (root.shouldBeVisible ? 1 : 0.9) : 1
transform: root.animationType === "slide" ? slideTransform : null
Translate {
id: slideTransform
x: root.shouldBeVisible ? 0 : 15
y: root.shouldBeVisible ? 0 : -30
}
readonly property real rawX: root.shouldBeVisible ? 0 : 15
readonly property real rawY: root.shouldBeVisible ? 0 : -30
Loader {
id: contentLoader
anchors.fill: parent
active: root.visible
asynchronous: false
x: snap(rawX)
y: snap(rawY)
}
Behavior on opacity {
NumberAnimation {
duration: root.animationDuration
easing.type: root.animationEasing
duration: animationDuration
easing.type: animationEasing
}
}
Behavior on scale {
enabled: root.animationType === "scale"
FocusScope {
anchors.fill: parent
focus: root.shouldBeVisible
clip: false
NumberAnimation {
duration: root.animationDuration
easing.type: root.animationEasing
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())
}
}
}
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 8
shadowBlur: 1
shadowColor: Theme.shadowStrong
shadowOpacity: 0.3
}
}
FocusScope {
@@ -207,8 +218,8 @@ PanelWindow {
objectName: "modalFocusScope"
anchors.fill: parent
visible: root.visible // Only active when the modal is visible
focus: root.visible
visible: root.shouldBeVisible || root.visible
focus: root.shouldBeVisible
Keys.onEscapePressed: event => {
if (root.closeOnEscapeKey && shouldHaveFocus) {
root.close()
@@ -223,7 +234,7 @@ PanelWindow {
Connections {
function onShouldHaveFocusChanged() {
if (shouldHaveFocus && visible) {
if (shouldHaveFocus && shouldBeVisible) {
Qt.callLater(() => focusScope.forceActiveFocus())
}
}

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: qsTr("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: qsTr("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: qsTr("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: qsTr("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: qsTr("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: qsTr("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: qsTr("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: qsTr("Copy")
backgroundColor: Theme.primary
textColor: Theme.background
anchors.verticalCenter: parent.verticalCenter
onClicked: {
const colorString = root.currentColor.toString()
copyColorToClipboard(colorString)
}
}
}
}
}
}
}

View File

@@ -35,6 +35,7 @@ DankModal {
property bool weAvailable: false
property string wePath: ""
property bool weMode: false
property var parentModal: null
signal fileSelected(string path)
@@ -131,6 +132,8 @@ DankModal {
objectName: "fileBrowserModal"
allowStacking: true
closeOnEscapeKey: false
shouldHaveFocus: shouldBeVisible
Component.onCompleted: {
currentPath = getLastPath()
}
@@ -165,10 +168,23 @@ DankModal {
visible: false
onBackgroundClicked: close()
onOpened: {
modalFocusScope.forceActiveFocus()
if (parentModal) {
parentModal.shouldHaveFocus = false
parentModal.allowFocusOverride = true
}
Qt.callLater(() => {
if (contentLoader && contentLoader.item) {
contentLoader.item.forceActiveFocus()
}
})
}
modalFocusScope.Keys.onPressed: function (event) {
keyboardController.handleKey(event)
onDialogClosed: {
if (parentModal) {
parentModal.allowFocusOverride = false
parentModal.shouldHaveFocus = Qt.binding(() => {
return parentModal.shouldBeVisible
})
}
}
onVisibleChanged: {
if (visible) {
@@ -239,7 +255,12 @@ DankModal {
return
}
if (!keyboardNavigationActive) {
if (event.key === Qt.Key_Tab || event.key === Qt.Key_Down || event.key === Qt.Key_Right) {
const isInitKey = event.key === Qt.Key_Tab || event.key === Qt.Key_Down || event.key === Qt.Key_Right ||
(event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) ||
(event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) ||
(event.key === Qt.Key_L && event.modifiers & Qt.ControlModifier)
if (isInitKey) {
keyboardNavigationActive = true
if (currentPath !== homeDir) {
backButtonFocused = true
@@ -281,6 +302,69 @@ DankModal {
}
event.accepted = true
break
case Qt.Key_N:
if (event.modifiers & Qt.ControlModifier) {
if (backButtonFocused) {
backButtonFocused = false
selectedIndex = 0
} else if (selectedIndex < totalItems - 1) {
selectedIndex++
}
event.accepted = true
}
break
case Qt.Key_P:
if (event.modifiers & Qt.ControlModifier) {
if (selectedIndex > 0) {
selectedIndex--
} else if (currentPath !== homeDir) {
backButtonFocused = true
selectedIndex = -1
}
event.accepted = true
}
break
case Qt.Key_J:
if (event.modifiers & Qt.ControlModifier) {
if (selectedIndex < totalItems - 1) {
selectedIndex++
}
event.accepted = true
}
break
case Qt.Key_K:
if (event.modifiers & Qt.ControlModifier) {
if (selectedIndex > 0) {
selectedIndex--
} else if (currentPath !== homeDir) {
backButtonFocused = true
selectedIndex = -1
}
event.accepted = true
}
break
case Qt.Key_H:
if (event.modifiers & Qt.ControlModifier) {
if (!backButtonFocused && selectedIndex > 0) {
selectedIndex--
} else if (currentPath !== homeDir) {
backButtonFocused = true
selectedIndex = -1
}
event.accepted = true
}
break
case Qt.Key_L:
if (event.modifiers & Qt.ControlModifier) {
if (backButtonFocused) {
backButtonFocused = false
selectedIndex = 0
} else if (selectedIndex < totalItems - 1) {
selectedIndex++
}
event.accepted = true
}
break
case Qt.Key_Left:
if (backButtonFocused)
return
@@ -387,6 +471,16 @@ DankModal {
Item {
anchors.fill: parent
Keys.onPressed: event => {
keyboardController.handleKey(event)
}
onVisibleChanged: {
if (visible) {
forceActiveFocus()
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
@@ -687,7 +781,7 @@ DankModal {
width: parent.width - saveButton.width - Theme.spacingM
height: 40
text: defaultFileName
placeholderText: "Enter filename..."
placeholderText: qsTr("Enter filename...")
ignoreLeftRightKeys: false
focus: saveMode
topPadding: Theme.spacingS
@@ -720,7 +814,7 @@ DankModal {
StyledText {
anchors.centerIn: parent
text: "Save"
text: qsTr("Save")
color: fileNameInput.text.trim() !== "" ? Theme.primaryText : Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeMedium
}

View File

@@ -134,7 +134,7 @@ Rectangle {
}
StyledText {
text: "File Information"
text: qsTr("File Information")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium

View File

@@ -56,7 +56,7 @@ DankModal {
spacing: Theme.spacingXS
StyledText {
text: "Network Information"
text: qsTr("Network Information")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
@@ -126,7 +126,7 @@ DankModal {
id: closeText
anchors.centerIn: parent
text: "Close"
text: qsTr("Close")
font.pixelSize: Theme.fontSizeMedium
color: Theme.background
font.weight: Font.Medium

View File

@@ -9,31 +9,47 @@ DankModal {
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": "Log Out",
"message": "Are you sure you want to log out?"
"title": qsTr("Log Out"),
"message": qsTr("Are you sure you want to log out?")
},
"suspend": {
"title": "Suspend",
"message": "Are you sure you want to suspend the system?"
"title": qsTr("Suspend"),
"message": qsTr("Are you sure you want to suspend the system?")
},
"hibernate": {
"title": "Hibernate",
"message": "Are you sure you want to hibernate the system?"
"title": qsTr("Hibernate"),
"message": qsTr("Are you sure you want to hibernate the system?")
},
"reboot": {
"title": "Reboot",
"message": "Are you sure you want to reboot the system?"
"title": qsTr("Reboot"),
"message": qsTr("Are you sure you want to reboot the system?")
},
"poweroff": {
"title": "Power Off",
"message": "Are you sure you want to power off the system?"
"title": qsTr("Power Off"),
"message": qsTr("Are you sure you want to power off the system?")
}
}
const selected = actions[action]
@@ -47,6 +63,16 @@ DankModal {
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();
}
@@ -57,13 +83,11 @@ DankModal {
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:
selectedIndex = (selectedIndex + 1) % optionCount;
event.accepted = true;
break;
case Qt.Key_Tab:
selectedIndex = (selectedIndex + 1) % optionCount;
event.accepted = true;
@@ -78,6 +102,30 @@ DankModal {
}
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;
}
}
@@ -96,7 +144,7 @@ DankModal {
width: parent.width
StyledText {
text: "Power Options"
text: qsTr("Power Options")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
@@ -153,7 +201,7 @@ DankModal {
}
StyledText {
text: "Log Out"
text: qsTr("Log Out")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
@@ -206,7 +254,7 @@ DankModal {
}
StyledText {
text: "Suspend"
text: qsTr("Suspend")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
@@ -260,7 +308,7 @@ DankModal {
}
StyledText {
text: "Hibernate"
text: qsTr("Hibernate")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
@@ -314,7 +362,7 @@ DankModal {
}
StyledText {
text: "Reboot"
text: qsTr("Reboot")
font.pixelSize: Theme.fontSizeMedium
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
font.weight: Font.Medium
@@ -368,7 +416,7 @@ DankModal {
}
StyledText {
text: "Power Off"
text: qsTr("Power Off")
font.pixelSize: Theme.fontSizeMedium
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
font.weight: Font.Medium

View File

@@ -123,7 +123,7 @@ DankModal {
}
StyledText {
text: "System Monitor Unavailable"
text: qsTr("System Monitor Unavailable")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.error
@@ -154,7 +154,7 @@ DankModal {
height: 40
StyledText {
text: "System Monitor"
text: qsTr("System Monitor")
font.pixelSize: Theme.fontSizeLarge + 4
font.weight: Font.Bold
color: Theme.surfaceText
@@ -181,7 +181,7 @@ DankModal {
Rectangle {
Layout.fillWidth: true
height: 52
color: Theme.surfaceSelected
color: Theme.surfaceContainerHigh
radius: Theme.cornerRadius
border.color: Theme.outlineLight
border.width: 1
@@ -281,7 +281,7 @@ DankModal {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Theme.cornerRadius
color: Theme.surfaceLight
color: Theme.surfaceContainerHigh
border.color: Theme.outlineLight
border.width: 1

View File

@@ -19,7 +19,7 @@ Item {
spacing: Theme.spacingXL
StyledText {
text: "Battery not detected - only AC power settings available"
text: qsTr("Battery not detected - only AC power settings available")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
visible: !BatteryService.batteryAvailable
@@ -31,7 +31,7 @@ Item {
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: 1
border.width: 0
Column {
id: timeoutSection
@@ -51,7 +51,7 @@ Item {
}
StyledText {
text: "Idle Settings"
text: qsTr("Idle Settings")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
@@ -79,8 +79,7 @@ Item {
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]
width: parent.width
text: "Automatically lock after"
text: qsTr("Automatically lock after")
options: timeoutOptions
Connections {
@@ -116,8 +115,7 @@ Item {
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]
width: parent.width
text: "Turn off monitors after"
text: qsTr("Turn off monitors after")
options: timeoutOptions
Connections {
@@ -153,8 +151,7 @@ Item {
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]
width: parent.width
text: "Suspend system after"
text: qsTr("Suspend system after")
options: timeoutOptions
Connections {
@@ -190,8 +187,7 @@ Item {
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]
width: parent.width
text: "Hibernate system after"
text: qsTr("Hibernate system after")
options: timeoutOptions
visible: SessionService.hibernateSupported
@@ -225,14 +221,14 @@ Item {
DankToggle {
width: parent.width
text: "Lock before suspend"
text: qsTr("Lock before suspend")
description: "Automatically lock the screen when the system prepares to suspend"
checked: SessionData.lockBeforeSuspend
onToggled: checked => SessionData.setLockBeforeSuspend(checked)
}
StyledText {
text: "Idle monitoring not supported - requires newer Quickshell version"
text: qsTr("Idle monitoring not supported - requires newer Quickshell version")
font.pixelSize: Theme.fontSizeSmall
color: Theme.error
anchors.horizontalCenter: parent.horizontalCenter

View File

@@ -2,11 +2,12 @@ import QtQuick
import qs.Common
import qs.Modules.Settings
Item {
FocusScope {
id: root
property int currentIndex: 0
property var parentModal: null
focus: true
Rectangle {
anchors.fill: parent
@@ -67,7 +68,8 @@ Item {
visible: active
asynchronous: true
sourceComponent: TopBarTab {
sourceComponent: DankBarTab {
parentModal: root.parentModal
}
}
@@ -154,13 +156,27 @@ Item {
}
Loader {
id: aboutLoader
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 {
}

View File

@@ -34,7 +34,7 @@ DankModal {
objectName: "settingsModal"
width: 800
height: 750
height: 800
visible: false
onBackgroundClicked: () => {
return hide();
@@ -78,6 +78,7 @@ DankModal {
id: profileBrowser
allowStacking: true
parentModal: settingsModal
browserTitle: "Select Profile Image"
browserIcon: "person"
browserType: "profile"
@@ -87,12 +88,6 @@ DankModal {
close();
}
onDialogClosed: () => {
if (settingsModal) {
settingsModal.allowFocusOverride = false;
settingsModal.shouldHaveFocus = Qt.binding(() => {
return settingsModal.shouldBeVisible;
});
}
allowStacking = true;
}
}
@@ -101,6 +96,7 @@ DankModal {
id: wallpaperBrowser
allowStacking: true
parentModal: settingsModal
browserTitle: "Select Wallpaper"
browserIcon: "wallpaper"
browserType: "wallpaper"
@@ -115,7 +111,7 @@ DankModal {
}
settingsContent: Component {
Item {
FocusScope {
anchors.fill: parent
focus: true
@@ -144,7 +140,7 @@ DankModal {
}
StyledText {
text: "Settings"
text: qsTr("Settings")
font.pixelSize: Theme.fontSizeXLarge
color: Theme.surfaceText
font.weight: Font.Medium

View File

@@ -18,7 +18,7 @@ Rectangle {
"text": "Weather",
"icon": "cloud"
}, {
"text": "Top Bar",
"text": "Dank Bar",
"icon": "toolbar"
}, {
"text": "Widgets",
@@ -38,6 +38,9 @@ Rectangle {
}, {
"text": "Power",
"icon": "power_settings_new"
}, {
"text": "Plugins",
"icon": "extension"
}, {
"text": "About",
"icon": "info"
@@ -83,7 +86,7 @@ Rectangle {
width: parent.width - Theme.spacingS * 2
height: 44
radius: Theme.cornerRadius
color: isActive ? Theme.primaryContainer : tabMouseArea.containsMouse ? Theme.surfaceHover : "transparent"
color: isActive ? Theme.primary : tabMouseArea.containsMouse ? Theme.surfaceHover : "transparent"
Row {
anchors.left: parent.left
@@ -94,14 +97,14 @@ Rectangle {
DankIcon {
name: modelData.icon || ""
size: Theme.iconSize - 2
color: parent.parent.isActive ? Theme.surfaceText : Theme.surfaceText
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.surfaceText : Theme.surfaceText
color: parent.parent.isActive ? Theme.primaryText : Theme.surfaceText
font.weight: parent.parent.isActive ? Font.Medium : Font.Normal
anchors.verticalCenter: parent.verticalCenter
}

View File

@@ -13,8 +13,13 @@ Item {
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)
@@ -33,6 +38,46 @@ Item {
} 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
@@ -59,43 +104,22 @@ Item {
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
Rectangle {
width: parent.width
height: categorySelector.height + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.surfaceVariantAlpha
border.color: Theme.outlineMedium
border.width: 1
visible: appLauncher.categories.length > 1 || appLauncher.model.count > 0
CategorySelector {
id: categorySelector
anchors.centerIn: parent
width: parent.width - Theme.spacingM * 2
categories: appLauncher.categories
selectedCategory: appLauncher.selectedCategory
compact: false
onCategorySelected: category => {
appLauncher.setCategory(category)
}
}
}
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.spacingM
width: parent.width - 80 - Theme.spacingL
height: 56
cornerRadius: Theme.cornerRadius
backgroundColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.7)
backgroundColor: Theme.surfaceContainerHigh
normalBorderColor: Theme.outlineMedium
focusedBorderColor: Theme.primary
leftIconName: "search"
@@ -125,7 +149,7 @@ Item {
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_Return || event.key === Qt.Key_Enter) && text.length === 0)) {
} 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
}
}
@@ -141,8 +165,6 @@ Item {
height: 36
radius: Theme.cornerRadius
color: appLauncher.viewMode === "list" ? Theme.primaryHover : listViewArea.containsMouse ? Theme.surfaceHover : "transparent"
border.color: appLauncher.viewMode === "list" ? Theme.primarySelected : "transparent"
border.width: 1
DankIcon {
anchors.centerIn: parent
@@ -168,8 +190,6 @@ Item {
height: 36
radius: Theme.cornerRadius
color: appLauncher.viewMode === "grid" ? Theme.primaryHover : gridViewArea.containsMouse ? Theme.surfaceHover : "transparent"
border.color: appLauncher.viewMode === "grid" ? Theme.primarySelected : "transparent"
border.width: 1
DankIcon {
anchors.centerIn: parent
@@ -193,6 +213,7 @@ Item {
}
SpotlightResults {
id: resultsView
appLauncher: spotlightKeyHandler.appLauncher
contextMenu: contextMenu
}
@@ -210,7 +231,7 @@ Item {
visible: contextMenu.visible
z: 999
onClicked: () => {
contextMenu.close()
contextMenu.hide()
}
MouseArea {

View File

@@ -1,65 +1,72 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
Popup {
id: contextMenu
property var currentApp: null
property bool menuVisible: false
property var appLauncher: null
property var parentHandler: null
function show(x, y, app) {
currentApp = app
const menuWidth = 180
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2
let finalX = x + 8
let finalY = y + 8
if (parentHandler) {
if (finalX + menuWidth > parentHandler.width)
finalX = x - menuWidth - 8
if (finalY + menuHeight > parentHandler.height)
finalY = y - menuHeight - 8
finalX = Math.max(8, Math.min(finalX, parentHandler.width - menuWidth - 8))
finalY = Math.max(8, Math.min(finalY, parentHandler.height - menuHeight - 8))
}
contextMenu.x = finalX
contextMenu.y = finalY
contextMenu.visible = true
contextMenu.menuVisible = true
contextMenu.x = x + 4
contextMenu.y = y + 4
contextMenu.open()
}
function close() {
contextMenu.menuVisible = false
Qt.callLater(() => {
contextMenu.visible = false
})
function hide() {
contextMenu.close()
}
visible: false
width: 180
width: Math.max(180, Math.min(300, menuColumn.implicitWidth + Theme.spacingS * 2))
height: menuColumn.implicitHeight + Theme.spacingS * 2
radius: Theme.cornerRadius
color: Theme.popupBackground()
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
z: 1000
opacity: menuVisible ? 1 : 0
scale: menuVisible ? 1 : 0.85
padding: 0
closePolicy: Popup.CloseOnPressOutside
modal: false
dim: false
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: parent.z - 1
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 {
@@ -125,7 +132,7 @@ Rectangle {
SessionData.removePinnedApp(appId)
else
SessionData.addPinnedApp(appId)
contextMenu.close()
contextMenu.hide()
}
}
}
@@ -144,6 +151,82 @@ Rectangle {
}
}
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
@@ -165,7 +248,7 @@ Rectangle {
}
StyledText {
text: "Launch"
text: qsTr("Launch")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
@@ -183,23 +266,72 @@ Rectangle {
if (contextMenu.currentApp && appLauncher)
appLauncher.launchApp(contextMenu.currentApp)
contextMenu.close()
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: qsTr("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()
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}

View File

@@ -18,24 +18,32 @@ DankModal {
function show() {
spotlightOpen = true
open()
if (contentLoader.item && contentLoader.item.appLauncher) {
contentLoader.item.appLauncher.searchQuery = ""
}
Qt.callLater(() => {
if (contentLoader.item && contentLoader.item.searchField) {
contentLoader.item.searchField.forceActiveFocus()
}
})
if (contentLoader.item && contentLoader.item.searchField) {
contentLoader.item.searchField.forceActiveFocus()
}
})
}
function hide() {
spotlightOpen = false
close()
if (contentLoader.item && contentLoader.item.appLauncher) {
contentLoader.item.appLauncher.searchQuery = ""
contentLoader.item.appLauncher.selectedIndex = 0
contentLoader.item.appLauncher.setCategory("All")
}
onDialogClosed: {
if (contentLoader.item) {
if (contentLoader.item.appLauncher) {
contentLoader.item.appLauncher.searchQuery = ""
contentLoader.item.appLauncher.selectedIndex = 0
contentLoader.item.appLauncher.setCategory("All")
}
if (contentLoader.item.resetScroll) {
contentLoader.item.resetScroll()
}
if (contentLoader.item.searchField) {
contentLoader.item.searchField.text = ""
}
}
}
@@ -49,12 +57,13 @@ DankModal {
shouldBeVisible: spotlightOpen
width: 550
height: 600
height: 700
backgroundColor: Theme.popupBackground()
cornerRadius: Theme.cornerRadius
borderColor: Theme.outlineMedium
borderWidth: 1
enableShadow: true
keepContentLoaded: true
onVisibleChanged: () => {
if (visible && !spotlightOpen) {
show()

View File

@@ -10,12 +10,16 @@ Rectangle {
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: Theme.surfaceLight
border.color: Theme.outlineLight
border.width: 1
color: "transparent"
clip: false
DankListView {
id: resultsList
@@ -75,9 +79,7 @@ Rectangle {
width: ListView.view.width
height: resultsList.itemHeight
radius: Theme.cornerRadius
color: ListView.isCurrentItem ? Theme.primaryPressed : listMouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
border.color: ListView.isCurrentItem ? Theme.primarySelected : Theme.outlineMedium
border.width: ListView.isCurrentItem ? 2 : 1
color: ListView.isCurrentItem ? Theme.primaryPressed : listMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceContainerHigh
Row {
anchors.fill: parent
@@ -161,7 +163,8 @@ Rectangle {
if (mouse.button === Qt.LeftButton) {
resultsList.itemClicked(index, model)
} else if (mouse.button === Qt.RightButton) {
const modalPos = mapToItem(resultsContainer.parent, mouse.x, mouse.y)
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)
}
}
@@ -238,9 +241,7 @@ Rectangle {
width: resultsGrid.cellWidth - resultsGrid.cellPadding
height: resultsGrid.cellHeight - resultsGrid.cellPadding
radius: Theme.cornerRadius
color: resultsGrid.currentIndex === index ? Theme.primaryPressed : gridMouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
border.color: resultsGrid.currentIndex === index ? Theme.primarySelected : Theme.outlineMedium
border.width: resultsGrid.currentIndex === index ? 2 : 1
color: resultsGrid.currentIndex === index ? Theme.primaryPressed : gridMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceContainerHigh
Column {
anchors.centerIn: parent
@@ -314,7 +315,8 @@ Rectangle {
if (mouse.button === Qt.LeftButton) {
resultsGrid.itemClicked(index, model)
} else if (mouse.button === Qt.RightButton) {
const modalPos = mapToItem(resultsContainer.parent, mouse.x, mouse.y)
const globalPos = mapToItem(null, mouse.x, mouse.y)
const modalPos = resultsContainer.parent.mapFromItem(null, globalPos.x, globalPos.y)
resultsGrid.itemRightClicked(index, model, modalPos.x, modalPos.y)
}
}

View File

@@ -9,33 +9,53 @@ DankModal {
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 && contentLoader.item.passwordInput)
contentLoader.item.passwordInput.forceActiveFocus()
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: 230
height: requiresEnterprise ? 310 : 230
onShouldBeVisibleChanged: () => {
if (!shouldBeVisible)
wifiPasswordInput = ""
if (!shouldBeVisible) {
wifiPasswordInput = ""
wifiUsernameInput = ""
}
}
onOpened: {
Qt.callLater(() => {
if (contentLoader.item && contentLoader.item.passwordInput)
contentLoader.item.passwordInput.forceActiveFocus()
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 {
@@ -55,6 +75,7 @@ DankModal {
FocusScope {
id: wifiContent
property alias usernameInput: usernameInput
property alias passwordInput: passwordInput
anchors.fill: parent
@@ -62,6 +83,7 @@ DankModal {
Keys.onEscapePressed: event => {
close()
wifiPasswordInput = ""
wifiUsernameInput = ""
event.accepted = true
}
@@ -78,14 +100,14 @@ DankModal {
spacing: Theme.spacingXS
StyledText {
text: "Connect to Wi-Fi"
text: qsTr("Connect to Wi-Fi")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: `Enter password for "${wifiPasswordSSID}"`
text: requiresEnterprise ? `Enter credentials for "${wifiPasswordSSID}"` : `Enter password for "${wifiPasswordSSID}"`
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium
width: parent.width
@@ -100,10 +122,48 @@ DankModal {
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
@@ -127,21 +187,24 @@ DankModal {
textColor: Theme.surfaceText
text: wifiPasswordInput
echoMode: showPasswordCheckbox.checked ? TextInput.Normal : TextInput.Password
placeholderText: ""
placeholderText: requiresEnterprise ? "Password" : ""
backgroundColor: "transparent"
focus: true
focus: !requiresEnterprise
enabled: root.shouldBeVisible
onTextEdited: () => {
wifiPasswordInput = text
}
onAccepted: () => {
NetworkService.connectToWifi(wifiPasswordSSID, passwordInput.text)
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)
if (root.shouldBeVisible && !requiresEnterprise)
focusDelayTimer.start()
}
@@ -151,8 +214,13 @@ DankModal {
interval: 100
repeat: false
onTriggered: () => {
if (root.shouldBeVisible)
passwordInput.forceActiveFocus()
if (root.shouldBeVisible) {
if (requiresEnterprise && usernameInput) {
usernameInput.forceActiveFocus()
} else {
passwordInput.forceActiveFocus()
}
}
}
}
@@ -201,7 +269,7 @@ DankModal {
}
StyledText {
text: "Show password"
text: qsTr("Show password")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
@@ -229,7 +297,7 @@ DankModal {
id: cancelText
anchors.centerIn: parent
text: "Cancel"
text: qsTr("Cancel")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
@@ -244,6 +312,7 @@ DankModal {
onClicked: () => {
close()
wifiPasswordInput = ""
wifiUsernameInput = ""
}
}
}
@@ -253,14 +322,14 @@ DankModal {
height: 36
radius: Theme.cornerRadius
color: connectArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
enabled: passwordInput.text.length > 0
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: "Connect"
text: qsTr("Connect")
font.pixelSize: Theme.fontSizeMedium
color: Theme.background
font.weight: Font.Medium
@@ -274,10 +343,13 @@ DankModal {
cursorShape: Qt.PointingHandCursor
enabled: parent.enabled
onClicked: () => {
NetworkService.connectToWifi(wifiPasswordSSID, passwordInput.text)
const username = requiresEnterprise ? usernameInput.text : ""
NetworkService.connectToWifi(wifiPasswordSSID, passwordInput.text, username)
close()
wifiPasswordInput = ""
wifiUsernameInput = ""
passwordInput.text = ""
if (requiresEnterprise) usernameInput.text = ""
}
}

View File

@@ -1,4 +1,5 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Io
@@ -12,7 +13,6 @@ import qs.Widgets
DankPopout {
id: appDrawerPopout
property string triggerSection: "left"
property var triggerScreen: null
// Setting to Exclusive, so virtual keyboards can send input to app drawer
@@ -33,9 +33,9 @@ DankPopout {
popupWidth: 520
popupHeight: 600
triggerX: Theme.spacingL
triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingXS
triggerY: Math.max(26 + SettingsData.dankBarInnerPadding + 4, Theme.barHeight - 4 - (8 - SettingsData.dankBarInnerPadding)) + SettingsData.dankBarSpacing + SettingsData.dankBarBottomGap - 2
triggerWidth: 40
positioning: "center"
positioning: ""
screen: triggerScreen
onShouldBeVisibleChanged: {
@@ -95,7 +95,7 @@ DankPopout {
color: "transparent"
radius: parent.radius + Math.abs(modelData.margin)
border.color: modelData.color
border.width: 1
border.width: 0
z: modelData.z
}
}
@@ -128,6 +128,44 @@ DankPopout {
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
}
}
if (!searchField.activeFocus && event.text && /[a-zA-Z0-9\s]/.test(event.text)) {
searchField.forceActiveFocus()
searchField.insertText(event.text)
@@ -136,19 +174,20 @@ DankPopout {
}
Column {
width: parent.width - Theme.spacingL * 2
height: parent.height - Theme.spacingL * 2
x: Theme.spacingL
y: Theme.spacingL
spacing: Theme.spacingL
width: parent.width - Theme.spacingS * 2
height: parent.height - Theme.spacingS * 2
x: Theme.spacingS
y: Theme.spacingS
spacing: Theme.spacingS
Row {
width: parent.width
height: 40
leftPadding: Theme.spacingS
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: "Applications"
text: qsTr("Applications")
font.pixelSize: Theme.fontSizeLarge + 4
font.weight: Font.Bold
color: Theme.surfaceText
@@ -170,7 +209,8 @@ DankPopout {
DankTextField {
id: searchField
width: parent.width
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)
@@ -231,14 +271,18 @@ DankPopout {
height: 40
spacing: Theme.spacingM
visible: searchField.text.length === 0
leftPadding: Theme.spacingS
Item {
width: 200
height: 36
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
@@ -249,7 +293,7 @@ DankPopout {
}
Item {
width: parent.width - 300
width: parent.width - 290
height: 1
}
@@ -286,15 +330,13 @@ DankPopout {
Rectangle {
width: parent.width
height: {
let usedHeight = 40 + Theme.spacingL
usedHeight += 52 + Theme.spacingL
usedHeight += (searchField.text.length === 0 ? 40 + Theme.spacingL : 0)
let usedHeight = 40 + Theme.spacingS
usedHeight += 52 + Theme.spacingS
usedHeight += (searchField.text.length === 0 ? 40 : 0)
return parent.height - usedHeight
}
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.1)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05)
border.width: 1
color: "transparent"
DankListView {
id: appList
@@ -323,7 +365,9 @@ DankPopout {
}
anchors.fill: parent
anchors.margins: Theme.spacingS
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.bottomMargin: Theme.spacingS
visible: appLauncher.viewMode === "list"
model: appLauncher.model
currentIndex: appLauncher.selectedIndex
@@ -353,9 +397,7 @@ DankPopout {
width: ListView.view.width
height: appList.itemHeight
radius: Theme.cornerRadius
color: ListView.isCurrentItem ? Theme.primaryPressed : listMouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
border.color: ListView.isCurrentItem ? Theme.primarySelected : Theme.outlineMedium
border.width: ListView.isCurrentItem ? 2 : 1
color: ListView.isCurrentItem ? Theme.primaryPressed : listMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceContainerHigh
Row {
anchors.fill: parent
@@ -371,6 +413,7 @@ DankPopout {
id: listIconImg
anchors.fill: parent
anchors.margins: Theme.spacingXS
source: Quickshell.iconPath(model.icon, true)
smooth: true
asynchronous: true
@@ -379,10 +422,13 @@ DankPopout {
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: 1
border.width: 0
border.color: Theme.primarySelected
StyledText {
@@ -425,6 +471,9 @@ DankPopout {
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
@@ -440,7 +489,8 @@ DankPopout {
if (mouse.button === Qt.LeftButton) {
appList.itemClicked(index, model)
} else if (mouse.button === Qt.RightButton) {
var panelPos = mapToItem(contextMenu.parent, mouse.x, mouse.y)
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)
}
}
@@ -484,7 +534,9 @@ DankPopout {
}
anchors.fill: parent
anchors.margins: Theme.spacingS
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.bottomMargin: Theme.spacingS
visible: appLauncher.viewMode === "grid"
model: appLauncher.model
clip: true
@@ -516,9 +568,7 @@ DankPopout {
width: appGrid.cellWidth - appGrid.cellPadding
height: appGrid.cellHeight - appGrid.cellPadding
radius: Theme.cornerRadius
color: appGrid.currentIndex === index ? Theme.primaryPressed : gridMouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
border.color: appGrid.currentIndex === index ? Theme.primarySelected : Theme.outlineMedium
border.width: appGrid.currentIndex === index ? 2 : 1
color: appGrid.currentIndex === index ? Theme.primaryPressed : gridMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceContainerHigh
Column {
anchors.centerIn: parent
@@ -535,6 +585,9 @@ DankPopout {
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
@@ -543,10 +596,13 @@ DankPopout {
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: 1
border.width: 0
border.color: Theme.primarySelected
StyledText {
@@ -577,6 +633,9 @@ DankPopout {
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
@@ -592,7 +651,8 @@ DankPopout {
if (mouse.button === Qt.LeftButton) {
appGrid.itemClicked(index, model)
} else if (mouse.button === Qt.RightButton) {
var panelPos = mapToItem(contextMenu.parent, mouse.x, mouse.y)
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)
}
}
@@ -605,68 +665,68 @@ DankPopout {
}
}
Rectangle {
Popup {
id: contextMenu
property var currentApp: null
property bool menuVisible: false
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
const menuWidth = 180
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2
let finalX = x + 8
let finalY = y + 8
if (finalX + menuWidth > appDrawerPopout.popupWidth) {
finalX = x - menuWidth - 8
}
if (finalY + menuHeight > appDrawerPopout.popupHeight) {
finalY = y - menuHeight - 8
}
finalX = Math.max(8, Math.min(finalX, appDrawerPopout.popupWidth - menuWidth - 8))
finalY = Math.max(8, Math.min(finalY, appDrawerPopout.popupHeight - menuHeight - 8))
contextMenu.x = finalX
contextMenu.y = finalY
contextMenu.visible = true
contextMenu.menuVisible = true
contextMenu.x = x + 4
contextMenu.y = y + 4
contextMenu.open()
}
function close() {
contextMenu.menuVisible = false
Qt.callLater(() => {
contextMenu.visible = false
})
function hide() {
contextMenu.close()
}
visible: false
width: 180
height: menuColumn.implicitHeight + Theme.spacingS * 2
radius: Theme.cornerRadius
color: Theme.popupBackground()
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
z: 1000
opacity: menuVisible ? 1 : 0
scale: menuVisible ? 1 : 0.85
padding: 0
closePolicy: Popup.CloseOnPressOutside
modal: false
dim: false
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: parent.z - 1
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 {
@@ -721,7 +781,7 @@ DankPopout {
} else {
SessionData.addPinnedApp(contextMenu.appId)
}
contextMenu.close()
contextMenu.hide()
}
}
}
@@ -740,6 +800,76 @@ DankPopout {
}
}
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
@@ -761,7 +891,7 @@ DankPopout {
}
StyledText {
text: "Launch"
text: qsTr("Launch")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
@@ -779,23 +909,70 @@ DankPopout {
if (contextMenu.currentApp)
appLauncher.launchApp(contextMenu.currentApp)
contextMenu.close()
contextMenu.hide()
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
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)
}
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
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: qsTr("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()
}
}
}
}
}
@@ -805,7 +982,7 @@ DankPopout {
visible: contextMenu.visible
z: 999
onClicked: {
contextMenu.close()
contextMenu.hide()
}
MouseArea {

View File

@@ -15,10 +15,9 @@ Item {
readonly property int maxCompactItems: 8
readonly property int itemHeight: 36
readonly property color selectedBorderColor: "transparent"
readonly property color unselectedBorderColor: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
readonly property color unselectedBorderColor: "transparent"
function handleCategoryClick(category) {
selectedCategory = category
categorySelected(category)
}
@@ -42,8 +41,7 @@ Item {
height: root.itemHeight
width: root.getButtonWidth(itemCount, parent.width)
radius: Theme.cornerRadius
color: selectedCategory === modelData ? Theme.primary : "transparent"
border.color: selectedCategory === modelData ? selectedBorderColor : unselectedBorderColor
color: selectedCategory === modelData ? Theme.primary : Theme.surfaceContainerHigh
StyledText {
anchors.centerIn: parent
@@ -82,7 +80,7 @@ Item {
height: root.itemHeight
width: root.getButtonWidth(itemCount, parent.width)
radius: Theme.cornerRadius
color: selectedCategory === modelData ? Theme.primary : "transparent"
color: selectedCategory === modelData ? Theme.primary : Theme.surfaceContainerHigh
border.color: selectedCategory === modelData ? selectedBorderColor : unselectedBorderColor
StyledText {
@@ -118,7 +116,7 @@ Item {
height: root.itemHeight
width: root.getButtonWidth(itemCount, parent.width)
radius: Theme.cornerRadius
color: selectedCategory === modelData ? Theme.primary : "transparent"
color: selectedCategory === modelData ? Theme.primary : Theme.surfaceContainerHigh
border.color: selectedCategory === modelData ? selectedBorderColor : unselectedBorderColor
StyledText {

View File

@@ -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: qsTr("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: qsTr("No VPN profiles found")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: qsTr("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)
}
}
}
}
}
}
}
}
}

View File

@@ -25,8 +25,7 @@ Rectangle {
readonly property color _tileBgActive: Theme.primary
readonly property color _tileBgInactive:
Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b,
Theme.getContentBackgroundAlpha() * 0.60)
Theme.surfaceContainerHigh
readonly property color _tileRingActive:
Qt.rgba(Theme.primaryText.r, Theme.primaryText.g, Theme.primaryText.b, 0.22)
@@ -60,7 +59,7 @@ Rectangle {
DankIcon {
name: root.iconName
size: Theme.iconSize
color: isActive ? Theme.primaryContainer : Theme.primary
color: isActive ? Theme.primaryText : Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
@@ -78,7 +77,7 @@ Rectangle {
width: parent.width
text: root.text
style: Typography.Style.Body
color: isActive ? Theme.primaryContainer : Theme.surfaceText
color: isActive ? Theme.primaryText : Theme.surfaceText
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
@@ -87,7 +86,7 @@ Rectangle {
width: parent.width
text: root.secondaryText
style: Typography.Style.Caption
color: isActive ? Theme.primaryContainer : Theme.surfaceVariantText
color: isActive ? Theme.primaryText : Theme.surfaceVariantText
visible: text.length > 0
elide: Text.ElideRight
wrapMode: Text.NoWrap

View File

@@ -1,38 +1,107 @@
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: parent.height > 0
property string sectionKey: root.expandedSection
sourceComponent: {
switch (root.expandedSection) {
case "network":
case "wifi": return networkDetailComponent
case "bluetooth": return bluetoothDetailComponent
case "audioOutput": return audioOutputDetailComponent
case "audioInput": return audioInputDetailComponent
case "battery": return batteryDetailComponent
default:
if (root.expandedSection.startsWith("diskUsage_")) {
return diskUsageDetailComponent
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
}
return null
builtinInstance = widgetModel.vpnBuiltinInstance
}
if (!builtinInstance || !builtinInstance.ccDetailContent) {
return
}
pluginDetailLoader.sourceComponent = builtinInstance.ccDetailContent
pluginDetailLoader.active = parent.height > 0
return
}
onSectionKeyChanged: {
active = false
active = true
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 {
@@ -42,7 +111,17 @@ Item {
Component {
id: bluetoothDetailComponent
BluetoothDetail {}
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 {

View File

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

View File

@@ -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 qsTr("DMS_SOCKET not available")
}
return qsTr("NM not supported")
}
secondaryMessage: qsTr("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()
}
}
}
}
}

View File

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

View File

@@ -7,6 +7,7 @@ Row {
id: root
property var availableWidgets: []
property Item popoutContent: null
signal addWidget(string widgetId)
signal resetToDefault()
@@ -19,7 +20,9 @@ Row {
Popup {
id: addWidgetPopup
anchors.centerIn: parent
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
@@ -29,7 +32,7 @@ Row {
background: Rectangle {
color: Theme.surfaceContainer
border.color: Theme.primarySelected
border.width: 1
border.width: 0
radius: Theme.cornerRadius
}
@@ -52,7 +55,7 @@ Row {
}
Typography {
text: "Add Widget"
text: qsTr("Add Widget")
style: Typography.Style.Subtitle
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
@@ -72,9 +75,9 @@ Row {
width: 400 - Theme.spacingL * 2
height: 50
radius: Theme.cornerRadius
color: widgetMouseArea.containsMouse ? Theme.primaryHover : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
color: widgetMouseArea.containsMouse ? Theme.primaryHover : Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 1
border.width: 0
Row {
anchors.fill: parent
@@ -138,7 +141,7 @@ Row {
radius: Theme.cornerRadius
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
border.color: Theme.primary
border.width: 1
border.width: 0
Row {
anchors.centerIn: parent
@@ -152,7 +155,7 @@ Row {
}
Typography {
text: "Add Widget"
text: qsTr("Add Widget")
style: Typography.Style.Button
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
@@ -172,7 +175,7 @@ Row {
radius: Theme.cornerRadius
color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12)
border.color: Theme.warning
border.width: 1
border.width: 0
Row {
anchors.centerIn: parent
@@ -186,7 +189,7 @@ Row {
}
Typography {
text: "Defaults"
text: qsTr("Defaults")
style: Typography.Style.Button
color: Theme.warning
anchors.verticalCenter: parent.verticalCenter
@@ -206,7 +209,7 @@ Row {
radius: Theme.cornerRadius
color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
border.color: Theme.error
border.width: 1
border.width: 0
Row {
anchors.centerIn: parent
@@ -220,7 +223,7 @@ Row {
}
Typography {
text: "Reset"
text: qsTr("Reset")
style: Typography.Style.Button
color: Theme.error
anchors.verticalCenter: parent.verticalCenter

View File

@@ -1,241 +0,0 @@
import QtQuick
import qs.Common
import qs.Widgets
Item {
id: root
property bool editMode: false
property var widgetData: null
property int widgetIndex: -1
property bool showSizeControls: true
property bool isSlider: false
signal removeWidget(int index)
signal toggleWidgetSize(int index)
signal moveWidget(int fromIndex, int toIndex)
// Delete button in top-right
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
onClicked: root.removeWidget(widgetIndex)
}
}
// Size control buttons in bottom-right
Row {
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.margins: -8
spacing: 4
visible: editMode && showSizeControls
z: 10
Rectangle {
width: 24
height: 24
radius: 12
color: (widgetData?.width || 50) === 25 ? Theme.primary : Theme.primaryContainer
border.color: Theme.primary
border.width: 1
visible: !isSlider
StyledText {
anchors.centerIn: parent
text: "25"
font.pixelSize: 10
font.weight: Font.Medium
color: (widgetData?.width || 50) === 25 ? Theme.primaryText : Theme.primary
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
var widgets = SettingsData.controlCenterWidgets.slice()
if (widgetIndex >= 0 && widgetIndex < widgets.length) {
widgets[widgetIndex].width = 25
SettingsData.setControlCenterWidgets(widgets)
}
}
}
}
Rectangle {
width: 24
height: 24
radius: 12
color: (widgetData?.width || 50) === 50 ? Theme.primary : Theme.primaryContainer
border.color: Theme.primary
border.width: 1
StyledText {
anchors.centerIn: parent
text: "50"
font.pixelSize: 10
font.weight: Font.Medium
color: (widgetData?.width || 50) === 50 ? Theme.primaryText : Theme.primary
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
var widgets = SettingsData.controlCenterWidgets.slice()
if (widgetIndex >= 0 && widgetIndex < widgets.length) {
widgets[widgetIndex].width = 50
SettingsData.setControlCenterWidgets(widgets)
}
}
}
}
Rectangle {
width: 24
height: 24
radius: 12
color: (widgetData?.width || 50) === 75 ? Theme.primary : Theme.primaryContainer
border.color: Theme.primary
border.width: 1
visible: !isSlider
StyledText {
anchors.centerIn: parent
text: "75"
font.pixelSize: 10
font.weight: Font.Medium
color: (widgetData?.width || 50) === 75 ? Theme.primaryText : Theme.primary
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
var widgets = SettingsData.controlCenterWidgets.slice()
if (widgetIndex >= 0 && widgetIndex < widgets.length) {
widgets[widgetIndex].width = 75
SettingsData.setControlCenterWidgets(widgets)
}
}
}
}
Rectangle {
width: 24
height: 24
radius: 12
color: (widgetData?.width || 50) === 100 ? Theme.primary : Theme.primaryContainer
border.color: Theme.primary
border.width: 1
StyledText {
anchors.centerIn: parent
text: "100"
font.pixelSize: 9
font.weight: Font.Medium
color: (widgetData?.width || 50) === 100 ? Theme.primaryText : Theme.primary
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
var widgets = SettingsData.controlCenterWidgets.slice()
if (widgetIndex >= 0 && widgetIndex < widgets.length) {
widgets[widgetIndex].width = 100
SettingsData.setControlCenterWidgets(widgets)
}
}
}
}
}
// Arrow buttons for reordering in top-left
Row {
anchors.top: parent.top
anchors.left: parent.left
anchors.margins: 4
spacing: 2
visible: editMode
z: 20
Rectangle {
width: 16
height: 16
radius: 8
color: Theme.surfaceContainer
border.color: Theme.outline
border.width: 1
DankIcon {
anchors.centerIn: parent
name: "keyboard_arrow_left"
size: 12
color: Theme.surfaceText
}
MouseArea {
anchors.fill: parent
enabled: widgetIndex > 0
opacity: enabled ? 1.0 : 0.5
onClicked: root.moveWidget(widgetIndex, widgetIndex - 1)
}
}
Rectangle {
width: 16
height: 16
radius: 8
color: Theme.surfaceContainer
border.color: Theme.outline
border.width: 1
DankIcon {
anchors.centerIn: parent
name: "keyboard_arrow_right"
size: 12
color: Theme.surfaceText
}
MouseArea {
anchors.fill: parent
enabled: widgetIndex < ((SettingsData.controlCenterWidgets?.length ?? 0) - 1)
opacity: enabled ? 1.0 : 0.5
onClicked: root.moveWidget(widgetIndex, widgetIndex + 1)
}
}
}
// Border highlight
Rectangle {
anchors.fill: parent
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
radius: Theme.cornerRadius
border.color: Theme.primary
border.width: editMode ? 1 : 0
visible: editMode
z: -1
Behavior on border.width {
NumberAnimation { duration: Theme.shortDuration }
}
}
}

View File

@@ -6,22 +6,18 @@ import qs.Widgets
Rectangle {
id: root
property bool powerOptionsExpanded: false
property bool editMode: false
signal powerActionRequested(string action, string title, string message)
signal powerButtonClicked()
signal lockRequested()
signal editModeToggled()
implicitHeight: 70
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
Theme.getContentBackgroundAlpha() * 0.4)
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 1
border.width: 0
Row {
anchors.left: parent.left
@@ -86,13 +82,11 @@ Rectangle {
DankActionButton {
buttonSize: 36
iconName: root.powerOptionsExpanded ? "expand_less" : "power_settings_new"
iconName: "power_settings_new"
iconSize: Theme.iconSize - 4
iconColor: root.powerOptionsExpanded ? Theme.primary : Theme.surfaceText
iconColor: Theme.surfaceText
backgroundColor: "transparent"
onClicked: {
root.powerOptionsExpanded = !root.powerOptionsExpanded
}
onClicked: root.powerButtonClicked()
}
DankActionButton {

View File

@@ -1,73 +0,0 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property bool expanded: false
signal powerActionRequested(string action, string title, string message)
implicitHeight: expanded ? 60 : 0
height: implicitHeight
clip: true
Rectangle {
width: parent.width
height: 60
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
Theme.getContentBackgroundAlpha() * 0.4)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: root.expanded ? 1 : 0
opacity: root.expanded ? 1 : 0
clip: true
Row {
anchors.centerIn: parent
spacing: SessionService.hibernateSupported ? Theme.spacingS : Theme.spacingL
visible: root.expanded
PowerButton {
width: SessionService.hibernateSupported ? 85 : 100
iconName: "logout"
text: "Logout"
onPressed: root.powerActionRequested("logout", "Logout", "Are you sure you want to logout?")
}
PowerButton {
width: SessionService.hibernateSupported ? 85 : 100
iconName: "restart_alt"
text: "Restart"
onPressed: root.powerActionRequested("reboot", "Restart", "Are you sure you want to restart?")
}
PowerButton {
width: SessionService.hibernateSupported ? 85 : 100
iconName: "bedtime"
text: "Suspend"
onPressed: root.powerActionRequested("suspend", "Suspend", "Are you sure you want to suspend?")
}
PowerButton {
width: SessionService.hibernateSupported ? 85 : 100
iconName: "ac_unit"
text: "Hibernate"
visible: SessionService.hibernateSupported
onPressed: root.powerActionRequested("hibernate", "Hibernate", "Are you sure you want to hibernate?")
}
PowerButton {
width: SessionService.hibernateSupported ? 85 : 100
iconName: "power_settings_new"
text: "Shutdown"
onPressed: root.powerActionRequested("poweroff", "Shutdown", "Are you sure you want to shutdown?")
}
}
}
}

View File

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

View File

@@ -1,734 +0,0 @@
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
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
function calculateRowsAndWidgets() {
return LayoutUtils.calculateRowsAndWidgets(root, expandedSection, expandedWidgetIndex)
}
property var layoutResult: {
const dummy = [expandedSection, expandedWidgetIndex, model?.controlCenterWidgets]
return calculateRowsAndWidgets()
}
onLayoutResultChanged: {
expandedRowIndex = layoutResult.expandedRowIndex
}
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 : -12) : 0
bottomPadding: isSliderOnlyRow ? (root.editMode ? 4 : -12) : 0
Flow {
width: parent.width
spacing: Theme.spacingS
Repeater {
model: rowWidgets || []
Item {
property var 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: 60
Loader {
id: widgetLoader
anchors.fill: parent
property var widgetData: parent.widgetData
property int widgetIndex: parent.globalWidgetIndex
property int globalWidgetIndex: parent.globalWidgetIndex
property int widgetWidth: parent.widgetWidth
sourceComponent: {
const id = modelData.id || ""
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 {
return widgetWidth <= 25 ? smallToggleComponent : toggleButtonComponent
}
}
}
}
}
}
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
}
}
}
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) {
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
}
}
EditModeOverlay {
anchors.fill: parent
editMode: root.editMode
widgetData: parent.widgetData
widgetIndex: parent.widgetIndex
showSizeControls: true
isSlider: false
onRemoveWidget: (index) => root.removeWidget(index)
onToggleWidgetSize: (index) => root.toggleWidgetSize(index)
onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex)
}
}
}
Component {
id: audioSliderComponent
Item {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
property var widgetDef: root.model?.getWidgetForId(widgetData.id || "")
width: parent.width
height: 16
AudioSliderRow {
anchors.centerIn: parent
width: parent.width
height: 14
property color sliderTrackColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.60)
}
EditModeOverlay {
anchors.fill: parent
editMode: root.editMode
widgetData: parent.widgetData
widgetIndex: parent.widgetIndex
showSizeControls: true
isSlider: true
onRemoveWidget: (index) => root.removeWidget(index)
onToggleWidgetSize: (index) => root.toggleWidgetSize(index)
onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex)
}
}
}
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: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.60)
}
EditModeOverlay {
anchors.fill: parent
editMode: root.editMode
widgetData: parent.widgetData
widgetIndex: parent.widgetIndex
showSizeControls: true
isSlider: true
onRemoveWidget: (index) => root.removeWidget(index)
onToggleWidgetSize: (index) => root.toggleWidgetSize(index)
onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex)
}
}
}
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: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.60)
}
EditModeOverlay {
anchors.fill: parent
editMode: root.editMode
widgetData: parent.widgetData
widgetIndex: parent.widgetIndex
showSizeControls: true
isSlider: true
onRemoveWidget: (index) => root.removeWidget(index)
onToggleWidgetSize: (index) => root.toggleWidgetSize(index)
onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex)
}
}
}
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)
}
}
EditModeOverlay {
anchors.fill: parent
editMode: root.editMode
widgetData: parent.widgetData
widgetIndex: parent.widgetIndex
showSizeControls: true
isSlider: false
onRemoveWidget: (index) => root.removeWidget(index)
onToggleWidgetSize: (index) => root.toggleWidgetSize(index)
onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex)
}
}
}
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)
}
}
EditModeOverlay {
anchors.fill: parent
editMode: root.editMode
widgetData: parent.widgetData
widgetIndex: parent.widgetIndex
showSizeControls: true
isSlider: false
onRemoveWidget: (index) => root.removeWidget(index)
onToggleWidgetSize: (index) => root.toggleWidgetSize(index)
onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex)
}
}
}
Component {
id: toggleButtonComponent
ToggleButton {
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 "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 widgetDef?.icon || "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 widgetDef?.text || "Unknown"
}
}
secondaryText: ""
iconRotation: widgetData.id === "darkMode" && 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: (widgetDef?.enabled ?? true) && !root.editMode
onClicked: {
switch (widgetData.id || "") {
case "nightMode": {
if (DisplayService.automationAvailable) {
DisplayService.toggleNightMode()
}
break
}
case "darkMode": {
Theme.toggleLightMode()
break
}
case "doNotDisturb": {
SessionData.setDoNotDisturb(!SessionData.doNotDisturb)
break
}
case "idleInhibitor": {
SessionService.toggleIdleInhibit()
break
}
}
}
EditModeOverlay {
anchors.fill: parent
editMode: root.editMode
widgetData: parent.widgetData
widgetIndex: parent.widgetIndex
showSizeControls: true
isSlider: false
onRemoveWidget: (index) => root.removeWidget(index)
onToggleWidgetSize: (index) => root.toggleWidgetSize(index)
onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex)
}
}
}
Component {
id: smallToggleComponent
SmallToggleButton {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
property var widgetDef: root.model?.getWidgetForId(widgetData.id || "")
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 widgetDef?.icon || "help"
}
}
iconRotation: widgetData.id === "darkMode" && 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: (widgetDef?.enabled ?? true) && !root.editMode
onClicked: {
switch (widgetData.id || "") {
case "nightMode": {
if (DisplayService.automationAvailable) {
DisplayService.toggleNightMode()
}
break
}
case "darkMode": {
Theme.toggleLightMode()
break
}
case "doNotDisturb": {
SessionData.setDoNotDisturb(!SessionData.doNotDisturb)
break
}
case "idleInhibitor": {
SessionService.toggleIdleInhibit()
break
}
}
}
EditModeOverlay {
anchors.fill: parent
editMode: root.editMode
widgetData: parent.widgetData
widgetIndex: parent.widgetIndex
showSizeControls: true
isSlider: false
onRemoveWidget: (index) => root.removeWidget(index)
onToggleWidgetSize: (index) => root.toggleWidgetSize(index)
onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex)
}
}
}
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)
}
}
EditModeOverlay {
anchors.fill: parent
editMode: root.editMode
widgetData: parent.widgetData
widgetIndex: parent.widgetIndex
showSizeControls: true
isSlider: false
onRemoveWidget: (index) => root.removeWidget(index)
onToggleWidgetSize: (index) => root.toggleWidgetSize(index)
onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex)
}
}
}
}

View File

@@ -10,7 +10,7 @@ import qs.Common
import qs.Modules.ControlCenter
import qs.Modules.ControlCenter.Widgets
import qs.Modules.ControlCenter.Details
import qs.Modules.TopBar
import qs.Modules.DankBar
import qs.Services
import qs.Widgets
import qs.Modules.ControlCenter.Components
@@ -21,14 +21,11 @@ DankPopout {
id: root
property string expandedSection: ""
property bool powerOptionsExpanded: false
property string triggerSection: "right"
property var triggerScreen: null
property bool editMode: false
property int expandedWidgetIndex: -1
property var expandedWidgetData: null
signal powerActionRequested(string action, string title, string message)
signal lockRequested
function collapseAll() {
@@ -49,7 +46,7 @@ DankPopout {
}
}
readonly property color _containerBg: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.60)
readonly property color _containerBg: Theme.surfaceContainerHigh
function setTriggerPosition(x, y, width, section, screen) {
StateUtils.setTriggerPosition(root, x, y, width, section, screen)
@@ -66,9 +63,9 @@ DankPopout {
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.topBarSpacing + Theme.spacingXS
triggerY: Theme.barHeight - 4 + SettingsData.dankBarSpacing
triggerWidth: 80
positioning: "center"
positioning: ""
screen: triggerScreen
shouldBeVisible: false
visible: shouldBeVisible
@@ -76,13 +73,17 @@ DankPopout {
onShouldBeVisibleChanged: {
if (shouldBeVisible) {
Qt.callLater(() => {
NetworkService.autoRefreshEnabled = NetworkService.wifiEnabled
if (NetworkService.activeService) {
NetworkService.activeService.autoRefreshEnabled = NetworkService.wifiEnabled
}
if (UserInfoService)
UserInfoService.getUptime()
})
} else {
Qt.callLater(() => {
NetworkService.autoRefreshEnabled = false
if (NetworkService.activeService) {
NetworkService.activeService.autoRefreshEnabled = false
}
if (BluetoothService.adapter && BluetoothService.adapter.discovering)
BluetoothService.adapter.discovering = false
editMode = false
@@ -102,14 +103,14 @@ DankPopout {
property alias bluetoothCodecSelector: bluetoothCodecSelector
color: {
const transparency = Theme.popupTransparency || 0.92
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: 1
border.width: 0
antialiasing: true
smooth: true
@@ -123,29 +124,25 @@ DankPopout {
HeaderPane {
id: headerPane
width: parent.width
powerOptionsExpanded: root.powerOptionsExpanded
editMode: root.editMode
onPowerOptionsExpandedChanged: root.powerOptionsExpanded = powerOptionsExpanded
onEditModeToggled: root.editMode = !root.editMode
onPowerActionRequested: (action, title, message) => root.powerActionRequested(action, title, message)
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()
}
}
PowerOptionsPane {
id: powerOptionsPane
width: parent.width
expanded: root.powerOptionsExpanded
onPowerActionRequested: (action, title, message) => {
root.powerOptionsExpanded = false
root.close()
root.powerActionRequested(action, title, message)
}
}
WidgetGrid {
DragDropGrid {
id: widgetGrid
width: parent.width
editMode: root.editMode
@@ -153,6 +150,8 @@ DankPopout {
expandedWidgetIndex: root.expandedWidgetIndex
expandedWidgetData: root.expandedWidgetData
model: widgetModel
bluetoothCodecSelector: bluetoothCodecSelector
colorPickerModal: root.colorPickerModal
onExpandClicked: (widgetData, globalIndex) => {
root.expandedWidgetIndex = globalIndex
root.expandedWidgetData = widgetData
@@ -170,9 +169,12 @@ DankPopout {
EditControls {
width: parent.width
visible: editMode
popoutContent: controlContent
availableWidgets: {
if (!editMode) return []
const existingIds = (SettingsData.controlCenterWidgets || []).map(w => w.id)
return widgetModel.baseWidgetDefinitions.filter(w => w.allowMultiple || !existingIds.includes(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()
@@ -222,4 +224,7 @@ DankPopout {
id: batteryDetailComponent
BatteryDetail {}
}
property var colorPickerModal: null
property var powerMenuModalLoader: null
}

View File

@@ -14,9 +14,9 @@ Rectangle {
implicitHeight: headerRow.height + (hasInputVolumeSliderInCC ? 0 : volumeSlider.height) + audioContent.height + Theme.spacingM
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
border.width: 0
Row {
id: headerRow
@@ -30,7 +30,7 @@ Rectangle {
StyledText {
id: headerText
text: "Input Devices"
text: qsTr("Input Devices")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
@@ -57,10 +57,6 @@ Rectangle {
radius: (Theme.iconSize + Theme.spacingS * 2) / 2
color: iconArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
}
MouseArea {
id: iconArea
anchors.fill: parent
@@ -138,9 +134,9 @@ Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: deviceMouseArea.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, index % 2 === 0 ? 0.3 : 0.2)
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: modelData === AudioService.source ? 2 : 1
border.width: 0
Row {
anchors.left: parent.left
@@ -198,14 +194,6 @@ Rectangle {
}
}
}
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
}
Behavior on border.color {
ColorAnimation { duration: Theme.shortDuration }
}
}
}
}

View File

@@ -14,9 +14,9 @@ Rectangle {
implicitHeight: headerRow.height + (!hasVolumeSliderInCC ? volumeSlider.height : 0) + audioContent.height + Theme.spacingM
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
border.width: 0
Row {
id: headerRow
@@ -30,7 +30,7 @@ Rectangle {
StyledText {
id: headerText
text: "Audio Devices"
text: qsTr("Audio Devices")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
@@ -57,10 +57,6 @@ Rectangle {
radius: (Theme.iconSize + Theme.spacingS * 2) / 2
color: iconArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
}
MouseArea {
id: iconArea
anchors.fill: parent
@@ -143,9 +139,9 @@ Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: deviceMouseArea.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, index % 2 === 0 ? 0.3 : 0.2)
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: modelData === AudioService.sink ? 2 : 1
border.width: 0
Row {
anchors.left: parent.left
@@ -205,14 +201,6 @@ Rectangle {
}
}
}
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
}
Behavior on border.color {
ColorAnimation { duration: Theme.shortDuration }
}
}
}
}

View File

@@ -9,9 +9,9 @@ import qs.Widgets
Rectangle {
implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
border.width: 0
function isActiveProfile(profile) {
if (typeof PowerProfiles === "undefined") {
@@ -125,16 +125,15 @@ Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: 64
radius: Theme.cornerRadius
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
border.width: 1
color: Theme.surfaceContainerHighest
border.width: 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
StyledText {
text: "Health"
text: qsTr("Health")
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
font.weight: Font.Medium
@@ -161,16 +160,15 @@ Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: 64
radius: Theme.cornerRadius
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
border.width: 1
color: Theme.surfaceContainerHighest
border.width: 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
StyledText {
text: "Capacity"
text: qsTr("Capacity")
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
font.weight: Font.Medium
@@ -211,7 +209,7 @@ Rectangle {
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: 1
border.width: 0
visible: (typeof PowerProfiles !== "undefined") && PowerProfiles.degradationReason !== PerformanceDegradationReason.None
Column {
@@ -239,7 +237,7 @@ Rectangle {
width: parent.width - Theme.iconSize - Theme.spacingM
StyledText {
text: "Power Profile Degradation"
text: qsTr("Power Profile Degradation")
font.pixelSize: Theme.fontSizeLarge
color: Theme.error
font.weight: Font.Medium

View File

@@ -124,7 +124,7 @@ Item {
radius: Theme.cornerRadius
color: Theme.surfaceContainer
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
border.width: 0
opacity: modalVisible ? 1 : 0
scale: modalVisible ? 1 : 0.9
@@ -170,7 +170,7 @@ Item {
}
StyledText {
text: "Audio Codec Selection"
text: qsTr("Audio Codec Selection")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
}
@@ -206,14 +206,14 @@ Item {
radius: Theme.cornerRadius
color: {
if (modelData.name === currentCodec)
return Theme.surfaceContainerHigh;
return Theme.surfaceContainerHighest;
else if (codecMouseArea.containsMouse)
return Theme.surfaceHover;
else
return "transparent";
}
border.color: "transparent"
border.width: 1
border.width: 0
Row {
anchors.left: parent.left
@@ -272,12 +272,6 @@ Item {
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}

View File

@@ -9,9 +9,9 @@ import qs.Widgets
Rectangle {
implicitHeight: BluetoothService.adapter && BluetoothService.adapter.enabled ? headerRow.height + bluetoothContent.height + Theme.spacingM : headerRow.height
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
border.width: 0
property var bluetoothCodecModalRef: null
@@ -39,7 +39,7 @@ Rectangle {
StyledText {
id: headerText
text: "Bluetooth Settings"
text: qsTr("Bluetooth Settings")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
@@ -58,11 +58,11 @@ Rectangle {
radius: 18
color: {
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled)
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
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: 1
border.width: 0
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
Row {
@@ -96,13 +96,6 @@ Rectangle {
BluetoothService.adapter.discovering = !BluetoothService.adapter.discovering
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
@@ -159,7 +152,7 @@ Rectangle {
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 Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, index % 2 === 0 ? 0.3 : 0.2)
return Theme.surfaceContainerHighest
}
border.color: {
if (modelData.state === BluetoothDeviceState.Connecting)
@@ -168,7 +161,7 @@ Rectangle {
return Theme.primary
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
}
border.width: (modelData.connected || modelData.state === BluetoothDeviceState.Connecting) ? 2 : 1
border.width: 0
Row {
anchors.left: parent.left
@@ -284,14 +277,6 @@ Rectangle {
}
}
}
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
}
Behavior on border.color {
ColorAnimation { duration: Theme.shortDuration }
}
}
}
@@ -347,9 +332,9 @@ Rectangle {
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) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.15)
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: 1
border.width: 0
opacity: canConnect ? 1 : 0.6
Row {
@@ -427,9 +412,6 @@ Rectangle {
}
}
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
}
}
}
@@ -440,7 +422,7 @@ Rectangle {
StyledText {
anchors.centerIn: parent
text: "No Bluetooth adapter found"
text: qsTr("No Bluetooth adapter found")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
}
@@ -458,7 +440,7 @@ Rectangle {
background: Rectangle {
color: Theme.popupBackground()
radius: Theme.cornerRadius
border.width: 1
border.width: 0
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
}
@@ -491,7 +473,7 @@ Rectangle {
}
MenuItem {
text: "Audio Codec"
text: qsTr("Audio Codec")
height: bluetoothContextMenu.currentDevice && BluetoothService.isAudioDevice(bluetoothContextMenu.currentDevice) && bluetoothContextMenu.currentDevice.connected ? 32 : 0
visible: bluetoothContextMenu.currentDevice && BluetoothService.isAudioDevice(bluetoothContextMenu.currentDevice) && bluetoothContextMenu.currentDevice.connected
@@ -516,7 +498,7 @@ Rectangle {
}
MenuItem {
text: "Forget Device"
text: qsTr("Forget Device")
height: 32
contentItem: StyledText {

View File

@@ -6,6 +6,8 @@ import qs.Services
import qs.Widgets
Rectangle {
id: root
property string currentMountPath: "/"
property string instanceId: ""
@@ -13,9 +15,9 @@ Rectangle {
implicitHeight: diskContent.height + Theme.spacingM
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
border.width: 0
Component.onCompleted: {
DgopService.addRef(["diskmounts"])
@@ -76,9 +78,9 @@ Rectangle {
width: parent.width
height: 80
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, index % 2 === 0 ? 0.3 : 0.2)
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 : 1
border.width: modelData.mount === currentMountPath ? 2 : 0
Row {
anchors.left: parent.left
@@ -152,16 +154,11 @@ Rectangle {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData.mount !== currentMountPath) {
currentMountPath = modelData.mount
mountPathChanged(modelData.mount)
}
currentMountPath = modelData.mount
mountPathChanged(modelData.mount)
}
}
Behavior on border.color {
ColorAnimation { duration: Theme.shortDuration }
}
}
}
}

View File

@@ -17,30 +17,18 @@ Rectangle {
return headerRow.height + wifiOffContent.height + Theme.spacingM
}
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
border.width: 0
Component.onCompleted: {
NetworkService.addRef()
if (NetworkService.wifiEnabled) {
NetworkService.scanWifi()
}
}
Component.onDestruction: {
NetworkService.removeRef()
}
property var wifiPasswordModalRef: {
wifiPasswordModalLoader.active = true
return wifiPasswordModalLoader.item
}
property var networkInfoModalRef: {
networkInfoModalLoader.active = true
return networkInfoModalLoader.item
}
Row {
id: headerRow
anchors.left: parent.left
@@ -53,7 +41,7 @@ Rectangle {
StyledText {
id: headerText
text: "Network Settings"
text: qsTr("Network Settings")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
@@ -68,15 +56,30 @@ Rectangle {
DankButtonGroup {
id: preferenceControls
anchors.verticalCenter: parent.verticalCenter
visible: NetworkService.ethernetConnected && NetworkService.wifiConnected
visible: NetworkService.ethernetConnected
property int currentPreferenceIndex: NetworkService.userPreference === "ethernet" ? 0 : 1
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")
}
}
@@ -91,17 +94,17 @@ Rectangle {
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
@@ -110,7 +113,7 @@ Rectangle {
duration: 1000
}
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: NetworkService.wifiEnabled ? "Disabling WiFi..." : "Enabling WiFi..."
@@ -145,7 +148,7 @@ Rectangle {
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: "WiFi is off"
text: qsTr("WiFi is off")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
@@ -158,12 +161,12 @@ Rectangle {
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: 1
border.width: 0
border.color: Theme.primary
StyledText {
anchors.centerIn: parent
text: "Enable WiFi"
text: qsTr("Enable WiFi")
color: Theme.primary
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
@@ -177,12 +180,6 @@ Rectangle {
onClicked: NetworkService.toggleWifiRadio()
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
@@ -207,16 +204,16 @@ Rectangle {
Item {
width: parent.width
height: 200
visible: NetworkService.wifiInterface && NetworkService.wifiNetworks?.length < 1 && !NetworkService.wifiToggling
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: true
running: NetworkService.isScanning
loops: Animation.Infinite
from: 0
to: 360
@@ -226,14 +223,18 @@ Rectangle {
}
Repeater {
model: {
let networks = [...NetworkService.wifiNetworks]
networks.sort((a, b) => {
if (a.ssid === NetworkService.currentWifiSSID) return -1
if (b.ssid === NetworkService.currentWifiSSID) return 1
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 networks
return sorted
}
delegate: Rectangle {
required property var modelData
@@ -242,9 +243,9 @@ Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: networkMouseArea.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, index % 2 === 0 ? 0.3 : 0.2)
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: modelData.ssid === NetworkService.currentWifiSSID ? 2 : 1
border.width: 0
Row {
anchors.left: parent.left
@@ -276,10 +277,10 @@ Rectangle {
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
@@ -332,9 +333,7 @@ Rectangle {
onClicked: function(event) {
if (modelData.ssid !== NetworkService.currentWifiSSID) {
if (modelData.secured && !modelData.saved) {
if (wifiPasswordModalRef) {
wifiPasswordModalRef.show(modelData.ssid)
}
wifiPasswordModal.show(modelData.ssid)
} else {
NetworkService.connectToWifi(modelData.ssid)
}
@@ -343,13 +342,6 @@ Rectangle {
}
}
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
}
Behavior on border.color {
ColorAnimation { duration: Theme.shortDuration }
}
}
}
}
@@ -369,7 +361,7 @@ Rectangle {
background: Rectangle {
color: Theme.popupBackground()
radius: Theme.cornerRadius
border.width: 1
border.width: 0
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
}
@@ -395,9 +387,7 @@ Rectangle {
NetworkService.disconnectWifi()
} else {
if (networkContextMenu.currentSecured && !networkContextMenu.currentSaved) {
if (wifiPasswordModalRef) {
wifiPasswordModalRef.show(networkContextMenu.currentSSID)
}
wifiPasswordModal.show(networkContextMenu.currentSSID)
} else {
NetworkService.connectToWifi(networkContextMenu.currentSSID)
}
@@ -406,7 +396,7 @@ Rectangle {
}
MenuItem {
text: "Network Info"
text: qsTr("Network Info")
height: 32
contentItem: StyledText {
@@ -423,15 +413,13 @@ Rectangle {
}
onTriggered: {
if (networkInfoModalRef) {
let networkData = NetworkService.getNetworkInfo(networkContextMenu.currentSSID)
networkInfoModalRef.showNetworkInfo(networkContextMenu.currentSSID, networkData)
}
let networkData = NetworkService.getNetworkInfo(networkContextMenu.currentSSID)
networkInfoModal.showNetworkInfo(networkContextMenu.currentSSID, networkData)
}
}
MenuItem {
text: "Forget Network"
text: qsTr("Forget Network")
height: networkContextMenu.currentSaved || networkContextMenu.currentConnected ? 32 : 0
visible: networkContextMenu.currentSaved || networkContextMenu.currentConnected
@@ -454,22 +442,12 @@ Rectangle {
}
}
LazyLoader {
id: wifiPasswordModalLoader
active: false
WifiPasswordModal {
id: wifiPasswordModal
}
WifiPasswordModal {
id: wifiPasswordModal
}
LazyLoader {
id: networkInfoModalLoader
active: false
NetworkInfoModal {
id: networkInfoModal
}
NetworkInfoModal {
id: networkInfoModal
}

View File

@@ -1,13 +1,38 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Modules.ControlCenter.BuiltinPlugins
import "../utils/widgets.js" as WidgetUtils
QtObject {
id: root
readonly property var baseWidgetDefinitions: [
{
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",
@@ -15,32 +40,28 @@ QtObject {
"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",
@@ -48,8 +69,7 @@ QtObject {
"type": "connection",
"enabled": NetworkService.wifiAvailable,
"warning": !NetworkService.wifiAvailable ? "Wi-Fi not available" : undefined
},
{
}, {
"id": "bluetooth",
"text": "Bluetooth",
"description": "Device connections",
@@ -57,32 +77,28 @@ QtObject {
"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",
@@ -90,24 +106,21 @@ QtObject {
"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",
@@ -116,8 +129,68 @@ QtObject {
"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)
@@ -146,4 +219,4 @@ QtObject {
function clearAll() {
WidgetUtils.clearAll()
}
}
}

View File

@@ -44,7 +44,7 @@ PanelWindow {
radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 1
border.width: 0
opacity: powerMenuVisible ? 1 : 0
scale: powerMenuVisible ? 1 : 0.85
@@ -65,7 +65,7 @@ PanelWindow {
width: parent.width
StyledText {
text: "Power Options"
text: qsTr("Power Options")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
@@ -118,7 +118,7 @@ PanelWindow {
}
StyledText {
text: "Log Out"
text: qsTr("Log Out")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
@@ -168,7 +168,7 @@ PanelWindow {
}
StyledText {
text: "Suspend"
text: qsTr("Suspend")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
@@ -218,7 +218,7 @@ PanelWindow {
}
StyledText {
text: "Reboot"
text: qsTr("Reboot")
font.pixelSize: Theme.fontSizeMedium
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
font.weight: Font.Medium
@@ -268,7 +268,7 @@ PanelWindow {
}
StyledText {
text: "Power Off"
text: qsTr("Power Off")
font.pixelSize: Theme.fontSizeMedium
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
font.weight: Font.Medium

View File

@@ -20,11 +20,7 @@ Row {
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"
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
}
color: iconArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.primary, 0)
MouseArea {
id: iconArea
@@ -34,7 +30,9 @@ Row {
cursorShape: Qt.PointingHandCursor
onClicked: {
if (defaultSink) {
AudioService.suppressOSD = true
defaultSink.audio.muted = !defaultSink.audio.muted
AudioService.suppressOSD = false
}
}
}
@@ -70,7 +68,7 @@ Row {
unit: "%"
valueOverride: actualVolumePercent
thumbOutlineColor: Theme.surfaceContainer
trackColor: root.sliderTrackColor.a > 0 ? root.sliderTrackColor : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.60)
trackColor: root.sliderTrackColor.a > 0 ? root.sliderTrackColor : Theme.surfaceContainerHigh
onSliderValueChanged: function(newValue) {
if (defaultSink) {
defaultSink.audio.volume = newValue / 100.0

View File

@@ -30,9 +30,6 @@ CompoundPill {
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled) {
return "bluetooth_disabled"
}
if (primaryDevice) {
return BluetoothService.getDeviceIcon(primaryDevice)
}
return "bluetooth"
}

View File

@@ -18,17 +18,13 @@ Row {
radius: (Theme.iconSize + Theme.spacingS * 2) / 2
color: iconArea.containsMouse
? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
: "transparent"
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
}
: Theme.withAlpha(Theme.primary, 0)
MouseArea {
id: iconArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
cursorShape: DisplayService.devices.length > 1 ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: function(event) {
if (DisplayService.devices.length > 1) {
@@ -41,6 +37,22 @@ Row {
}
}
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: {
@@ -80,7 +92,7 @@ Row {
}
}
thumbOutlineColor: Theme.surfaceContainer
trackColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.60)
trackColor: Theme.surfaceContainerHigh
}
Menu {
@@ -91,7 +103,7 @@ Row {
background: Rectangle {
color: Theme.popupBackground()
radius: Theme.cornerRadius
border.width: 1
border.width: 0
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
}
@@ -141,4 +153,10 @@ Row {
onObjectRemoved: (index, object) => deviceMenu.removeItem(object)
}
}
Loader {
id: tooltipLoader
active: false
sourceComponent: DankTooltip {}
}
}

View File

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

View File

@@ -20,9 +20,9 @@ Rectangle {
width: parent ? parent.width : 200
height: 60
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
border.width: 0
opacity: enabled ? 1.0 : 0.6
Row {

View File

@@ -27,20 +27,21 @@ Rectangle {
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor)
}
readonly property color _containerBg:
Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b,
Theme.getContentBackgroundAlpha() * 0.60)
readonly property color _containerBg: Theme.surfaceContainerHigh
color: _containerBg
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: 1
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 || 0.92
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)
}
@@ -48,7 +49,7 @@ Rectangle {
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.primaryContainer
readonly property color _tileIconActive: Theme.primaryText
readonly property color _tileIconInactive: Theme.primary
property int _padH: Theme.spacingS

View File

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

View File

@@ -20,11 +20,7 @@ Row {
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"
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
}
color: iconArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.primary, 0)
MouseArea {
id: iconArea
@@ -34,7 +30,9 @@ Row {
cursorShape: Qt.PointingHandCursor
onClicked: {
if (defaultSource) {
AudioService.suppressOSD = true
defaultSource.audio.muted = !defaultSource.audio.muted
AudioService.suppressOSD = false
}
}
}
@@ -68,7 +66,10 @@ Row {
unit: "%"
valueOverride: actualVolumePercent
thumbOutlineColor: Theme.surfaceContainer
trackColor: root.sliderTrackColor.a > 0 ? root.sliderTrackColor : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.60)
trackColor: root.sliderTrackColor.a > 0 ? root.sliderTrackColor : Theme.surfaceContainerHigh
onIsDraggingChanged: {
AudioService.suppressOSD = isDragging
}
onSliderValueChanged: function(newValue) {
if (defaultSource) {
defaultSource.audio.volume = newValue / 100.0

View File

@@ -1,78 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.ControlCenter.Widgets
CompoundPill {
id: root
isActive: {
if (NetworkService.wifiToggling) {
return false
}
if (NetworkService.networkStatus === "ethernet") {
return true
}
if (NetworkService.networkStatus === "wifi") {
return true
}
return NetworkService.wifiEnabled
}
iconName: {
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"
}
primaryText: {
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"
}
secondaryText: {
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 ""
}
onToggled: {
if (NetworkService.networkStatus !== "ethernet" && !NetworkService.wifiToggling) {
NetworkService.toggleWifiRadio()
}
}
}

View File

@@ -25,15 +25,17 @@ Rectangle {
}
readonly property color _tileBgActive: Theme.primary
readonly property color _tileBgInactive:
Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b,
Theme.getContentBackgroundAlpha() * 0.60)
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.primaryContainer
readonly property color _tileIconActive: Theme.primaryText
readonly property color _tileIconInactive: Theme.primary
color: isActive ? _tileBgActive : _tileBgInactive
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
@@ -89,13 +91,6 @@ Rectangle {
onClicked: root.clicked()
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on radius {
NumberAnimation {
duration: Theme.shortDuration

View File

@@ -12,6 +12,7 @@ Rectangle {
property real iconRotation: 0
signal clicked()
signal iconRotationCompleted()
width: parent ? ((parent.width - parent.spacing * 3) / 4) : 48
height: 48
@@ -26,15 +27,17 @@ Rectangle {
}
readonly property color _tileBgActive: Theme.primary
readonly property color _tileBgInactive:
Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b,
Theme.getContentBackgroundAlpha() * 0.60)
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.primaryContainer
readonly property color _tileIconActive: Theme.primaryText
readonly property color _tileIconInactive: Theme.primary
color: isActive ? _tileBgActive : _tileBgInactive
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
@@ -56,6 +59,7 @@ Rectangle {
size: Theme.iconSize
color: isActive ? _tileIconActive : _tileIconInactive
rotation: iconRotation
onRotationCompleted: root.iconRotationCompleted()
}
MouseArea {
@@ -67,13 +71,6 @@ Rectangle {
onClicked: root.clicked()
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on radius {
NumberAnimation {
duration: Theme.shortDuration

View File

@@ -12,8 +12,9 @@ Rectangle {
property bool enabled: true
property string secondaryText: ""
property real iconRotation: 0
signal clicked()
signal iconRotationCompleted()
width: parent ? parent.width : 200
height: 60
@@ -23,15 +24,17 @@ Rectangle {
}
readonly property color _tileBgActive: Theme.primary
readonly property color _tileBgInactive:
Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b,
Theme.getContentBackgroundAlpha() * 0.60)
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
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: isActive ? 1 : 1
border.width: 0
opacity: enabled ? 1.0 : 0.6
function hoverTint(base) {
@@ -39,14 +42,12 @@ Rectangle {
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor)
}
readonly property color _containerBg:
Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b,
Theme.getContentBackgroundAlpha() * 0.60)
readonly property color _containerBg: Theme.surfaceContainerHigh
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: mouseArea.containsMouse ? hoverTint(_containerBg) : "transparent"
color: mouseArea.containsMouse ? hoverTint(_containerBg) : Theme.withAlpha(_containerBg, 0)
opacity: mouseArea.containsMouse ? 0.08 : 0.0
Behavior on opacity {
@@ -63,9 +64,10 @@ Rectangle {
DankIcon {
name: root.iconName
size: Theme.iconSize
color: isActive ? Theme.primaryContainer : Theme.primary
color: isActive ? Theme.primaryText : Theme.primary
anchors.verticalCenter: parent.verticalCenter
rotation: root.iconRotation
onRotationCompleted: root.iconRotationCompleted()
}
Item {
@@ -82,7 +84,7 @@ Rectangle {
width: parent.width
text: root.text
font.pixelSize: Theme.fontSizeMedium
color: isActive ? Theme.primaryContainer : Theme.surfaceText
color: isActive ? Theme.primaryText : Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
wrapMode: Text.NoWrap
@@ -92,7 +94,7 @@ Rectangle {
width: parent.width
text: root.secondaryText
font.pixelSize: Theme.fontSizeSmall
color: isActive ? Theme.primaryContainer : Theme.surfaceVariantText
color: isActive ? Theme.primaryText : Theme.surfaceVariantText
visible: text.length > 0
elide: Text.ElideRight
wrapMode: Text.NoWrap
@@ -110,13 +112,6 @@ Rectangle {
onClicked: root.clicked()
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on radius {
NumberAnimation {
duration: Theme.shortDuration

View File

@@ -0,0 +1,179 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Services
Item {
id: root
required property var barWindow
required property var axis
required property var appDrawerLoader
required property var dankDashPopoutLoader
required property var processListPopoutLoader
required property var notificationCenterLoader
required property var batteryPopoutLoader
required property var vpnPopoutLoader
required property var controlCenterLoader
required property var clipboardHistoryModalPopup
required property var systemUpdateLoader
required property var notepadInstance
property alias reveal: core.reveal
property alias autoHide: core.autoHide
property alias backgroundTransparency: core.backgroundTransparency
property alias hasActivePopout: core.hasActivePopout
property alias mouseArea: topBarMouseArea
Item {
id: inputMask
readonly property int barThickness: barWindow.px(barWindow.effectiveBarThickness + SettingsData.dankBarSpacing)
readonly property bool showing: SettingsData.dankBarVisible && (core.reveal
|| (CompositorService.isNiri && NiriService.inOverview && SettingsData.dankBarOpenOnOverview)
|| !core.autoHide)
readonly property int maskThickness: showing ? barThickness : 1
x: {
if (!axis.isVertical) {
return 0
} else {
switch (SettingsData.dankBarPosition) {
case SettingsData.Position.Left: return 0
case SettingsData.Position.Right: return parent.width - maskThickness
default: return 0
}
}
}
y: {
if (axis.isVertical) {
return 0
} else {
switch (SettingsData.dankBarPosition) {
case SettingsData.Position.Top: return 0
case SettingsData.Position.Bottom: return parent.height - maskThickness
default: return 0
}
}
}
width: axis.isVertical ? maskThickness : parent.width
height: axis.isVertical ? parent.height : maskThickness
}
Region {
id: mask
item: inputMask
}
property alias maskRegion: mask
QtObject {
id: core
property real backgroundTransparency: SettingsData.dankBarTransparency
property bool autoHide: SettingsData.dankBarAutoHide
property bool revealSticky: false
property bool notepadInstanceVisible: notepadInstance?.isVisible ?? false
readonly property bool hasActivePopout: {
const loaders = [{
"loader": appDrawerLoader,
"prop": "shouldBeVisible"
}, {
"loader": dankDashPopoutLoader,
"prop": "shouldBeVisible"
}, {
"loader": processListPopoutLoader,
"prop": "shouldBeVisible"
}, {
"loader": notificationCenterLoader,
"prop": "shouldBeVisible"
}, {
"loader": batteryPopoutLoader,
"prop": "shouldBeVisible"
}, {
"loader": vpnPopoutLoader,
"prop": "shouldBeVisible"
}, {
"loader": controlCenterLoader,
"prop": "shouldBeVisible"
}, {
"loader": clipboardHistoryModalPopup,
"prop": "visible"
}, {
"loader": systemUpdateLoader,
"prop": "shouldBeVisible"
}]
return notepadInstanceVisible || loaders.some(item => {
if (item.loader) {
return item.loader?.item?.[item.prop]
}
return false
})
}
property bool reveal: {
if (CompositorService.isNiri && NiriService.inOverview) {
return SettingsData.dankBarOpenOnOverview
}
return SettingsData.dankBarVisible && (!autoHide || topBarMouseArea.containsMouse || hasActivePopout || revealSticky)
}
onHasActivePopoutChanged: {
if (!hasActivePopout && autoHide && !topBarMouseArea.containsMouse) {
revealSticky = true
revealHold.restart()
}
}
}
Timer {
id: revealHold
interval: 250
repeat: false
onTriggered: core.revealSticky = false
}
Connections {
function onDankBarTransparencyChanged() {
core.backgroundTransparency = SettingsData.dankBarTransparency
}
target: SettingsData
}
Connections {
target: topBarMouseArea
function onContainsMouseChanged() {
if (topBarMouseArea.containsMouse) {
core.revealSticky = true
revealHold.stop()
} else {
if (core.autoHide && !core.hasActivePopout) {
revealHold.restart()
}
}
}
}
MouseArea {
id: topBarMouseArea
y: !barWindow.isVertical ? (SettingsData.dankBarPosition === SettingsData.Position.Bottom ? parent.height - height : 0) : 0
x: barWindow.isVertical ? (SettingsData.dankBarPosition === SettingsData.Position.Right ? parent.width - width : 0) : 0
height: !barWindow.isVertical ? barWindow.px(barWindow.effectiveBarThickness + SettingsData.dankBarSpacing) : undefined
width: barWindow.isVertical ? barWindow.px(barWindow.effectiveBarThickness + SettingsData.dankBarSpacing) : undefined
anchors {
left: !barWindow.isVertical ? parent.left : (SettingsData.dankBarPosition === SettingsData.Position.Left ? parent.left : undefined)
right: !barWindow.isVertical ? parent.right : (SettingsData.dankBarPosition === SettingsData.Position.Right ? parent.right : undefined)
top: barWindow.isVertical ? parent.top : undefined
bottom: barWindow.isVertical ? parent.bottom : undefined
}
hoverEnabled: SettingsData.dankBarAutoHide && !core.reveal
acceptedButtons: Qt.NoButton
enabled: SettingsData.dankBarAutoHide && !core.reveal
}
}

View File

@@ -0,0 +1,57 @@
import QtQuick
QtObject {
id: root
property string edge: "top"
readonly property string orientation: isVertical ? "vertical" : "horizontal"
readonly property bool isVertical: edge === "left" || edge === "right"
readonly property bool isHorizontal: !isVertical
function primarySize(item) {
return isVertical ? item.height : item.width
}
function crossSize(item) {
return isVertical ? item.width : item.height
}
function setPrimaryPos(item, value) {
if (isVertical) {
item.y = value
} else {
item.x = value
}
}
function getPrimaryPos(item) {
return isVertical ? item.y : item.x
}
function primaryAnchor(anchors) {
return isVertical ? anchors.verticalCenter : anchors.horizontalCenter
}
function crossAnchor(anchors) {
return isVertical ? anchors.horizontalCenter : anchors.verticalCenter
}
function outerVisualEdge() {
if (edge === "bottom") return "bottom"
if (edge === "left") return "right"
if (edge === "right") return "left"
if (edge === "top") return "top"
return "bottom"
}
signal axisEdgeChanged()
signal axisOrientationChanged()
signal changed() // Single coalesced signal
onEdgeChanged: {
axisEdgeChanged()
axisOrientationChanged()
changed()
}
}

View File

@@ -0,0 +1,327 @@
import QtQuick
import qs.Common
Item {
id: root
required property var barWindow
required property var axis
anchors.fill: parent
anchors.left: parent.left
anchors.top: parent.top
anchors.leftMargin: -(SettingsData.dankBarGothCornersEnabled && axis.isVertical && axis.edge === "right" ? barWindow._wingR : 0)
anchors.rightMargin: -(SettingsData.dankBarGothCornersEnabled && axis.isVertical && axis.edge === "left" ? barWindow._wingR : 0)
anchors.topMargin: -(SettingsData.dankBarGothCornersEnabled && !axis.isVertical && axis.edge === "bottom" ? barWindow._wingR : 0)
anchors.bottomMargin: -(SettingsData.dankBarGothCornersEnabled && !axis.isVertical && axis.edge === "top" ? barWindow._wingR : 0)
Canvas {
id: barShape
anchors.fill: parent
antialiasing: true
renderTarget: Canvas.FramebufferObject
renderStrategy: Canvas.Cooperative
readonly property real correctWidth: root.width
readonly property real correctHeight: root.height
canvasSize: Qt.size(barWindow.px(correctWidth), barWindow.px(correctHeight))
property real wing: SettingsData.dankBarGothCornersEnabled ? barWindow._wingR : 0
property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.cornerRadius
onWingChanged: requestPaint()
onRtChanged: requestPaint()
onCorrectWidthChanged: requestPaint()
onCorrectHeightChanged: requestPaint()
onVisibleChanged: if (visible) requestPaint()
Component.onCompleted: requestPaint()
Connections {
target: barWindow
function on_BgColorChanged() { barShape.requestPaint() }
function on_DprChanged() { barShape.requestPaint() }
}
Connections {
target: Theme
function onIsLightModeChanged() { barShape.requestPaint() }
}
onPaint: {
const ctx = getContext("2d")
const scale = barWindow._dpr
const W = barWindow.px(barWindow.isVertical ? correctHeight : correctWidth)
const H_raw = barWindow.px(barWindow.isVertical ? correctWidth : correctHeight)
const R = barWindow.px(wing)
const RT = barWindow.px(rt)
const H = H_raw - (R > 0 ? R : 0)
const isTop = SettingsData.dankBarPosition === SettingsData.Position.Top
const isBottom = SettingsData.dankBarPosition === SettingsData.Position.Bottom
const isLeft = SettingsData.dankBarPosition === SettingsData.Position.Left
const isRight = SettingsData.dankBarPosition === SettingsData.Position.Right
ctx.scale(scale, scale)
function drawTopPath() {
ctx.beginPath()
ctx.moveTo(RT, 0)
ctx.lineTo(W - RT, 0)
ctx.arcTo(W, 0, W, RT, RT)
ctx.lineTo(W, H)
if (R > 0) {
ctx.lineTo(W, H + R)
ctx.arc(W - R, H + R, R, 0, -Math.PI / 2, true)
ctx.lineTo(R, H)
ctx.arc(R, H + R, R, -Math.PI / 2, -Math.PI, true)
ctx.lineTo(0, H + R)
} else {
ctx.lineTo(W, H - RT)
ctx.arcTo(W, H, W - RT, H, RT)
ctx.lineTo(RT, H)
ctx.arcTo(0, H, 0, H - RT, RT)
}
ctx.lineTo(0, RT)
ctx.arcTo(0, 0, RT, 0, RT)
ctx.closePath()
}
ctx.reset()
ctx.clearRect(0, 0, W, H_raw)
ctx.save()
if (isBottom) {
ctx.translate(W, H_raw)
ctx.rotate(Math.PI)
} else if (isLeft) {
ctx.translate(0, W)
ctx.rotate(-Math.PI / 2)
} else if (isRight) {
ctx.translate(H_raw, 0)
ctx.rotate(Math.PI / 2)
}
drawTopPath()
ctx.restore()
ctx.fillStyle = barWindow._bgColor
ctx.fill()
}
}
Canvas {
id: barTint
anchors.fill: parent
antialiasing: true
renderTarget: Canvas.FramebufferObject
renderStrategy: Canvas.Cooperative
readonly property real correctWidth: root.width
readonly property real correctHeight: root.height
canvasSize: Qt.size(barWindow.px(correctWidth), barWindow.px(correctHeight))
property real wing: SettingsData.dankBarGothCornersEnabled ? barWindow._wingR : 0
property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.cornerRadius
property real alphaTint: (barWindow._bgColor?.a ?? 1) < 0.99 ? (Theme.stateLayerOpacity ?? 0) : 0
onWingChanged: requestPaint()
onRtChanged: requestPaint()
onAlphaTintChanged: requestPaint()
onCorrectWidthChanged: requestPaint()
onCorrectHeightChanged: requestPaint()
onVisibleChanged: if (visible) requestPaint()
Component.onCompleted: requestPaint()
Connections {
target: barWindow
function on_BgColorChanged() { barTint.requestPaint() }
function on_DprChanged() { barTint.requestPaint() }
}
Connections {
target: Theme
function onIsLightModeChanged() { barTint.requestPaint() }
}
onPaint: {
const ctx = getContext("2d")
const scale = barWindow._dpr
const W = barWindow.px(barWindow.isVertical ? correctHeight : correctWidth)
const H_raw = barWindow.px(barWindow.isVertical ? correctWidth : correctHeight)
const R = barWindow.px(wing)
const RT = barWindow.px(rt)
const H = H_raw - (R > 0 ? R : 0)
const isTop = SettingsData.dankBarPosition === SettingsData.Position.Top
const isBottom = SettingsData.dankBarPosition === SettingsData.Position.Bottom
const isLeft = SettingsData.dankBarPosition === SettingsData.Position.Left
const isRight = SettingsData.dankBarPosition === SettingsData.Position.Right
ctx.scale(scale, scale)
function drawTopPath() {
ctx.beginPath()
ctx.moveTo(RT, 0)
ctx.lineTo(W - RT, 0)
ctx.arcTo(W, 0, W, RT, RT)
ctx.lineTo(W, H)
if (R > 0) {
ctx.lineTo(W, H + R)
ctx.arc(W - R, H + R, R, 0, -Math.PI / 2, true)
ctx.lineTo(R, H)
ctx.arc(R, H + R, R, -Math.PI / 2, -Math.PI, true)
ctx.lineTo(0, H + R)
} else {
ctx.lineTo(W, H - RT)
ctx.arcTo(W, H, W - RT, H, RT)
ctx.lineTo(RT, H)
ctx.arcTo(0, H, 0, H - RT, RT)
}
ctx.lineTo(0, RT)
ctx.arcTo(0, 0, RT, 0, RT)
ctx.closePath()
}
ctx.reset()
ctx.clearRect(0, 0, W, H_raw)
ctx.save()
if (isBottom) {
ctx.translate(W, H_raw)
ctx.rotate(Math.PI)
} else if (isLeft) {
ctx.translate(0, W)
ctx.rotate(-Math.PI / 2)
} else if (isRight) {
ctx.translate(H_raw, 0)
ctx.rotate(Math.PI / 2)
}
drawTopPath()
ctx.restore()
ctx.fillStyle = Qt.rgba(Theme.surface.r, Theme.surface.g, Theme.surface.b, alphaTint)
ctx.fill()
}
}
Canvas {
id: barBorder
anchors.fill: parent
antialiasing: true
visible: SettingsData.dankBarBorderEnabled
renderTarget: Canvas.FramebufferObject
renderStrategy: Canvas.Cooperative
readonly property real correctWidth: root.width
readonly property real correctHeight: root.height
canvasSize: Qt.size(barWindow.px(correctWidth), barWindow.px(correctHeight))
property real wing: SettingsData.dankBarGothCornersEnabled ? barWindow._wingR : 0
property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.cornerRadius
property bool borderEnabled: SettingsData.dankBarBorderEnabled
onWingChanged: requestPaint()
onRtChanged: requestPaint()
onBorderEnabledChanged: requestPaint()
onCorrectWidthChanged: requestPaint()
onCorrectHeightChanged: requestPaint()
onVisibleChanged: if (visible) requestPaint()
Component.onCompleted: requestPaint()
Connections {
target: barWindow
function on_DprChanged() { barBorder.requestPaint() }
}
Connections {
target: Theme
function onSecondaryChanged() { barBorder.requestPaint() }
}
Connections {
target: SettingsData
function onDankBarSpacingChanged() { barBorder.requestPaint() }
function onDankBarSquareCornersChanged() { barBorder.requestPaint() }
function onCornerRadiusChanged() { barBorder.requestPaint() }
}
onPaint: {
if (!borderEnabled) return
const ctx = getContext("2d")
const scale = barWindow._dpr
const W = barWindow.px(barWindow.isVertical ? correctHeight : correctWidth)
const H_raw = barWindow.px(barWindow.isVertical ? correctWidth : correctHeight)
const R = barWindow.px(wing)
const RT = barWindow.px(rt)
const H = H_raw - (R > 0 ? R : 0)
const isTop = SettingsData.dankBarPosition === SettingsData.Position.Top
const isBottom = SettingsData.dankBarPosition === SettingsData.Position.Bottom
const isLeft = SettingsData.dankBarPosition === SettingsData.Position.Left
const isRight = SettingsData.dankBarPosition === SettingsData.Position.Right
const spacing = SettingsData.dankBarSpacing
const hasEdgeGap = spacing > 0 || RT > 0
ctx.scale(scale, scale)
function drawTopBorder() {
ctx.beginPath()
if (!hasEdgeGap) {
ctx.moveTo(0, H)
ctx.lineTo(W, H)
} else {
ctx.moveTo(RT, 0)
ctx.lineTo(W - RT, 0)
ctx.arcTo(W, 0, W, RT, RT)
ctx.lineTo(W, H)
if (R > 0) {
ctx.lineTo(W, H + R)
ctx.arc(W - R, H + R, R, 0, -Math.PI / 2, true)
ctx.lineTo(R, H)
ctx.arc(R, H + R, R, -Math.PI / 2, -Math.PI, true)
ctx.lineTo(0, H + R)
} else {
ctx.lineTo(W, H - RT)
ctx.arcTo(W, H, W - RT, H, RT)
ctx.lineTo(RT, H)
ctx.arcTo(0, H, 0, H - RT, RT)
}
ctx.lineTo(0, RT)
ctx.arcTo(0, 0, RT, 0, RT)
}
ctx.closePath()
}
ctx.reset()
ctx.clearRect(0, 0, W, H_raw)
ctx.save()
if (isBottom) {
ctx.translate(W, H_raw)
ctx.rotate(Math.PI)
} else if (isLeft) {
ctx.translate(0, W)
ctx.rotate(-Math.PI / 2)
} else if (isRight) {
ctx.translate(H_raw, 0)
ctx.rotate(Math.PI / 2)
}
drawTopBorder()
ctx.restore()
ctx.lineWidth = 1
ctx.strokeStyle = Theme.secondary
ctx.stroke()
}
}
}

View File

@@ -0,0 +1,460 @@
import QtQuick
import qs.Common
import qs.Services
Item {
id: root
property var widgetsModel: null
property var components: null
property bool noBackground: false
required property var axis
property string section: "center"
property var parentScreen: null
property real widgetThickness: 30
property real barThickness: 48
readonly property bool isVertical: axis?.isVertical ?? false
readonly property real spacing: noBackground ? 2 : Theme.spacingXS
property var centerWidgets: []
property int totalWidgets: 0
property real totalSize: 0
function updateLayout() {
const containerSize = isVertical ? height : width
if (containerSize <= 0 || !visible) {
return
}
centerWidgets = []
totalWidgets = 0
totalSize = 0
let configuredWidgets = 0
for (var i = 0; i < centerRepeater.count; i++) {
const item = centerRepeater.itemAt(i)
if (item && getWidgetVisible(item.widgetId)) {
configuredWidgets++
if (item.active && item.item) {
centerWidgets.push(item.item)
totalWidgets++
totalSize += isVertical ? item.item.height : item.item.width
}
}
}
if (totalWidgets > 1) {
totalSize += spacing * (totalWidgets - 1)
}
positionWidgets(configuredWidgets)
}
function positionWidgets(configuredWidgets) {
if (totalWidgets === 0 || (isVertical ? height : width) <= 0) {
return
}
const parentCenter = (isVertical ? height : width) / 2
const isOdd = configuredWidgets % 2 === 1
centerWidgets.forEach(widget => {
if (isVertical) {
widget.anchors.verticalCenter = undefined
} else {
widget.anchors.horizontalCenter = undefined
}
})
if (isOdd) {
const middleIndex = Math.floor(configuredWidgets / 2)
let currentActiveIndex = 0
let middleWidget = null
for (var i = 0; i < centerRepeater.count; i++) {
const item = centerRepeater.itemAt(i)
if (item && getWidgetVisible(item.widgetId)) {
if (currentActiveIndex === middleIndex && item.active && item.item) {
middleWidget = item.item
break
}
currentActiveIndex++
}
}
if (middleWidget) {
const middleSize = isVertical ? middleWidget.height : middleWidget.width
if (isVertical) {
middleWidget.y = parentCenter - (middleSize / 2)
} else {
middleWidget.x = parentCenter - (middleSize / 2)
}
let leftWidgets = []
let rightWidgets = []
let foundMiddle = false
for (var i = 0; i < centerWidgets.length; i++) {
if (centerWidgets[i] === middleWidget) {
foundMiddle = true
continue
}
if (!foundMiddle) {
leftWidgets.push(centerWidgets[i])
} else {
rightWidgets.push(centerWidgets[i])
}
}
let currentPos = isVertical ? middleWidget.y : middleWidget.x
for (var i = leftWidgets.length - 1; i >= 0; i--) {
const size = isVertical ? leftWidgets[i].height : leftWidgets[i].width
currentPos -= (spacing + size)
if (isVertical) {
leftWidgets[i].y = currentPos
} else {
leftWidgets[i].x = currentPos
}
}
currentPos = (isVertical ? middleWidget.y : middleWidget.x) + middleSize
for (var i = 0; i < rightWidgets.length; i++) {
currentPos += spacing
if (isVertical) {
rightWidgets[i].y = currentPos
} else {
rightWidgets[i].x = currentPos
}
currentPos += isVertical ? rightWidgets[i].height : rightWidgets[i].width
}
}
} else {
let configuredLeftIndex = (configuredWidgets / 2) - 1
let configuredRightIndex = configuredWidgets / 2
const halfSpacing = spacing / 2
let leftWidget = null
let rightWidget = null
let leftWidgets = []
let rightWidgets = []
let currentConfigIndex = 0
for (var i = 0; i < centerRepeater.count; i++) {
const item = centerRepeater.itemAt(i)
if (item && getWidgetVisible(item.widgetId)) {
if (item.active && item.item) {
if (currentConfigIndex < configuredLeftIndex) {
leftWidgets.push(item.item)
} else if (currentConfigIndex === configuredLeftIndex) {
leftWidget = item.item
} else if (currentConfigIndex === configuredRightIndex) {
rightWidget = item.item
} else {
rightWidgets.push(item.item)
}
}
currentConfigIndex++
}
}
if (leftWidget && rightWidget) {
const leftSize = isVertical ? leftWidget.height : leftWidget.width
if (isVertical) {
leftWidget.y = parentCenter - halfSpacing - leftSize
rightWidget.y = parentCenter + halfSpacing
} else {
leftWidget.x = parentCenter - halfSpacing - leftSize
rightWidget.x = parentCenter + halfSpacing
}
let currentPos = isVertical ? leftWidget.y : leftWidget.x
for (var i = leftWidgets.length - 1; i >= 0; i--) {
const size = isVertical ? leftWidgets[i].height : leftWidgets[i].width
currentPos -= (spacing + size)
if (isVertical) {
leftWidgets[i].y = currentPos
} else {
leftWidgets[i].x = currentPos
}
}
currentPos = (isVertical ? rightWidget.y + rightWidget.height : rightWidget.x + rightWidget.width)
for (var i = 0; i < rightWidgets.length; i++) {
currentPos += spacing
if (isVertical) {
rightWidgets[i].y = currentPos
} else {
rightWidgets[i].x = currentPos
}
currentPos += isVertical ? rightWidgets[i].height : rightWidgets[i].width
}
} else if (leftWidget && !rightWidget) {
const leftSize = isVertical ? leftWidget.height : leftWidget.width
if (isVertical) {
leftWidget.y = parentCenter - halfSpacing - leftSize
} else {
leftWidget.x = parentCenter - halfSpacing - leftSize
}
let currentPos = isVertical ? leftWidget.y : leftWidget.x
for (var i = leftWidgets.length - 1; i >= 0; i--) {
const size = isVertical ? leftWidgets[i].height : leftWidgets[i].width
currentPos -= (spacing + size)
if (isVertical) {
leftWidgets[i].y = currentPos
} else {
leftWidgets[i].x = currentPos
}
}
currentPos = (isVertical ? leftWidget.y + leftWidget.height : leftWidget.x + leftWidget.width) + spacing
for (var i = 0; i < rightWidgets.length; i++) {
currentPos += spacing
if (isVertical) {
rightWidgets[i].y = currentPos
} else {
rightWidgets[i].x = currentPos
}
currentPos += isVertical ? rightWidgets[i].height : rightWidgets[i].width
}
} else if (!leftWidget && rightWidget) {
if (isVertical) {
rightWidget.y = parentCenter + halfSpacing
} else {
rightWidget.x = parentCenter + halfSpacing
}
let currentPos = (isVertical ? rightWidget.y : rightWidget.x) - spacing
for (var i = leftWidgets.length - 1; i >= 0; i--) {
const size = isVertical ? leftWidgets[i].height : leftWidgets[i].width
currentPos -= size
if (isVertical) {
leftWidgets[i].y = currentPos
} else {
leftWidgets[i].x = currentPos
}
currentPos -= spacing
}
currentPos = (isVertical ? rightWidget.y + rightWidget.height : rightWidget.x + rightWidget.width)
for (var i = 0; i < rightWidgets.length; i++) {
currentPos += spacing
if (isVertical) {
rightWidgets[i].y = currentPos
} else {
rightWidgets[i].x = currentPos
}
currentPos += isVertical ? rightWidgets[i].height : rightWidgets[i].width
}
} else if (totalWidgets === 1 && centerWidgets[0]) {
const size = isVertical ? centerWidgets[0].height : centerWidgets[0].width
if (isVertical) {
centerWidgets[0].y = parentCenter - (size / 2)
} else {
centerWidgets[0].x = parentCenter - (size / 2)
}
}
}
}
function getWidgetVisible(widgetId) {
const widgetVisibility = {
"cpuUsage": DgopService.dgopAvailable,
"memUsage": DgopService.dgopAvailable,
"cpuTemp": DgopService.dgopAvailable,
"gpuTemp": DgopService.dgopAvailable,
"network_speed_monitor": DgopService.dgopAvailable
}
return widgetVisibility[widgetId] ?? true
}
function getWidgetComponent(widgetId) {
// Build dynamic component map including plugins
let baseMap = {
"launcherButton": "launcherButtonComponent",
"workspaceSwitcher": "workspaceSwitcherComponent",
"focusedWindow": "focusedWindowComponent",
"runningApps": "runningAppsComponent",
"clock": "clockComponent",
"music": "mediaComponent",
"weather": "weatherComponent",
"systemTray": "systemTrayComponent",
"privacyIndicator": "privacyIndicatorComponent",
"clipboard": "clipboardComponent",
"cpuUsage": "cpuUsageComponent",
"memUsage": "memUsageComponent",
"diskUsage": "diskUsageComponent",
"cpuTemp": "cpuTempComponent",
"gpuTemp": "gpuTempComponent",
"notificationButton": "notificationButtonComponent",
"battery": "batteryComponent",
"controlCenterButton": "controlCenterButtonComponent",
"idleInhibitor": "idleInhibitorComponent",
"spacer": "spacerComponent",
"separator": "separatorComponent",
"network_speed_monitor": "networkComponent",
"keyboard_layout_name": "keyboardLayoutNameComponent",
"vpn": "vpnComponent",
"notepadButton": "notepadButtonComponent",
"colorPicker": "colorPickerComponent",
"systemUpdate": "systemUpdateComponent"
}
// For built-in components, get from components property
const componentKey = baseMap[widgetId]
if (componentKey && root.components[componentKey]) {
return root.components[componentKey]
}
// For plugin components, get from PluginService
var parts = widgetId.split(":")
var pluginId = parts[0]
let pluginComponents = PluginService.getWidgetComponents()
return pluginComponents[pluginId] || null
}
height: parent.height
width: parent.width
anchors.centerIn: parent
Timer {
id: layoutTimer
interval: 0
repeat: false
onTriggered: root.updateLayout()
}
Component.onCompleted: {
layoutTimer.restart()
}
onWidthChanged: {
if (width > 0) {
layoutTimer.restart()
}
}
onHeightChanged: {
if (height > 0) {
layoutTimer.restart()
}
}
onVisibleChanged: {
if (visible && (isVertical ? height : width) > 0) {
layoutTimer.restart()
}
}
Repeater {
id: centerRepeater
model: root.widgetsModel
Loader {
property string widgetId: model.widgetId
property var widgetData: model
property int spacerSize: model.size || 20
anchors.verticalCenter: !root.isVertical ? parent.verticalCenter : undefined
anchors.horizontalCenter: root.isVertical ? parent.horizontalCenter : undefined
active: root.getWidgetVisible(model.widgetId) && (model.widgetId !== "music" || MprisController.activePlayer !== null)
sourceComponent: root.getWidgetComponent(model.widgetId)
opacity: (model.enabled !== false) ? 1 : 0
asynchronous: false
onLoaded: {
if (!item) {
return
}
item.widthChanged.connect(() => layoutTimer.restart())
item.heightChanged.connect(() => layoutTimer.restart())
if (model.widgetId === "spacer") {
item.spacerSize = Qt.binding(() => model.size || 20)
}
if (root.axis && "axis" in item) {
item.axis = Qt.binding(() => root.axis)
}
if (root.axis && "isVertical" in item) {
try {
item.isVertical = Qt.binding(() => root.axis.isVertical)
} catch (e) {
}
}
// Inject properties for plugin widgets
if ("section" in item) {
item.section = root.section
}
if ("parentScreen" in item) {
item.parentScreen = Qt.binding(() => root.parentScreen)
}
if ("widgetThickness" in item) {
item.widgetThickness = Qt.binding(() => root.widgetThickness)
}
if ("barThickness" in item) {
item.barThickness = Qt.binding(() => root.barThickness)
}
// Inject PluginService for plugin widgets
if (item.pluginService !== undefined) {
var parts = model.widgetId.split(":")
var pluginId = parts[0]
var variantId = parts.length > 1 ? parts[1] : null
if (item.pluginId !== undefined) {
item.pluginId = pluginId
}
if (item.variantId !== undefined) {
item.variantId = variantId
}
if (item.variantData !== undefined && variantId) {
item.variantData = PluginService.getPluginVariantData(pluginId, variantId)
}
item.pluginService = PluginService
}
if (item.popoutService !== undefined) {
item.popoutService = PopoutService
}
layoutTimer.restart()
}
onActiveChanged: {
layoutTimer.restart()
}
}
}
Connections {
target: widgetsModel
function onCountChanged() {
layoutTimer.restart()
}
}
// Listen for plugin changes and refresh components
Connections {
target: PluginService
function onPluginLoaded(pluginId) {
// Force refresh of component lookups
for (var i = 0; i < centerRepeater.count; i++) {
var item = centerRepeater.itemAt(i)
if (item && item.widgetId.startsWith(pluginId)) {
item.sourceComponent = root.getWidgetComponent(item.widgetId)
}
}
}
function onPluginUnloaded(pluginId) {
// Force refresh of component lookups
for (var i = 0; i < centerRepeater.count; i++) {
var item = centerRepeater.itemAt(i)
if (item && item.widgetId.startsWith(pluginId)) {
item.sourceComponent = root.getWidgetComponent(item.widgetId)
}
}
}
}
}

1125
Modules/DankBar/DankBar.qml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,82 @@
import QtQuick
import qs.Common
Item {
id: root
property var widgetsModel: null
property var components: null
property bool noBackground: false
required property var axis
property var parentScreen: null
property real widgetThickness: 30
property real barThickness: 48
readonly property bool isVertical: axis?.isVertical ?? false
implicitHeight: layoutLoader.item ? (layoutLoader.item.implicitHeight || layoutLoader.item.height) : 0
implicitWidth: layoutLoader.item ? (layoutLoader.item.implicitWidth || layoutLoader.item.width) : 0
Loader {
id: layoutLoader
anchors.fill: parent
sourceComponent: root.isVertical ? columnComp : rowComp
}
Component {
id: rowComp
Row {
spacing: noBackground ? 2 : Theme.spacingXS
Repeater {
model: root.widgetsModel
Item {
width: widgetLoader.item ? widgetLoader.item.width : 0
height: widgetLoader.item ? widgetLoader.item.height : 0
WidgetHost {
id: widgetLoader
anchors.verticalCenter: parent.verticalCenter
widgetId: model.widgetId
widgetData: model
spacerSize: model.size || 20
components: root.components
isInColumn: false
axis: root.axis
section: "left"
parentScreen: root.parentScreen
widgetThickness: root.widgetThickness
barThickness: root.barThickness
}
}
}
}
}
Component {
id: columnComp
Column {
width: Math.max(parent.width, 200)
spacing: noBackground ? 2 : Theme.spacingXS
Repeater {
model: root.widgetsModel
Item {
width: parent.width
height: widgetLoader.item ? widgetLoader.item.height : 0
WidgetHost {
id: widgetLoader
anchors.horizontalCenter: parent.horizontalCenter
widgetId: model.widgetId
widgetData: model
spacerSize: model.size || 20
components: root.components
isInColumn: true
axis: root.axis
section: "left"
parentScreen: root.parentScreen
widgetThickness: root.widgetThickness
barThickness: root.barThickness
}
}
}
}
}
}

View File

@@ -11,7 +11,6 @@ import qs.Widgets
DankPopout {
id: root
property string triggerSection: "right"
property var triggerScreen: null
function setTriggerPosition(x, y, width, section, screen) {
@@ -45,9 +44,9 @@ DankPopout {
popupWidth: 400
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 400
triggerX: Screen.width - 380 - Theme.spacingL
triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingS
triggerY: Theme.barHeight - 4 + SettingsData.dankBarSpacing
triggerWidth: 70
positioning: "center"
positioning: ""
screen: triggerScreen
shouldBeVisible: false
visible: shouldBeVisible
@@ -60,7 +59,7 @@ DankPopout {
color: Theme.popupBackground()
radius: Theme.cornerRadius
border.color: Theme.outlineMedium
border.width: 1
border.width: 0
antialiasing: true
smooth: true
focus: true
@@ -96,7 +95,7 @@ DankPopout {
color: "transparent"
radius: parent.radius + 3
border.color: Qt.rgba(0, 0, 0, 0.05)
border.width: 1
border.width: 0
z: -3
}
@@ -106,7 +105,7 @@ DankPopout {
color: "transparent"
radius: parent.radius + 2
border.color: Theme.shadowMedium
border.width: 1
border.width: 0
z: -2
}
@@ -114,7 +113,7 @@ DankPopout {
anchors.fill: parent
color: "transparent"
border.color: Theme.outlineStrong
border.width: 1
border.width: 0
radius: parent.radius
z: -1
}
@@ -274,7 +273,7 @@ DankPopout {
height: 32
radius: 16
color: closeBatteryArea.containsMouse ? Theme.errorHover : "transparent"
anchors.verticalCenter: parent.verticalCenter
anchors.top: parent.top
DankIcon {
anchors.centerIn: parent
@@ -304,16 +303,15 @@ DankPopout {
width: (parent.width - Theme.spacingM) / 2
height: 64
radius: Theme.cornerRadius
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
border.width: 1
color: Theme.surfaceContainerHigh
border.width: 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
StyledText {
text: "Health"
text: qsTr("Health")
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
font.weight: Font.Medium
@@ -340,16 +338,15 @@ DankPopout {
width: (parent.width - Theme.spacingM) / 2
height: 64
radius: Theme.cornerRadius
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
border.width: 1
color: Theme.surfaceContainerHigh
border.width: 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
StyledText {
text: "Capacity"
text: qsTr("Capacity")
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
font.weight: Font.Medium
@@ -390,7 +387,7 @@ DankPopout {
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: 1
border.width: 0
visible: (typeof PowerProfiles !== "undefined") && PowerProfiles.degradationReason !== PerformanceDegradationReason.None
Column {
@@ -418,7 +415,7 @@ DankPopout {
width: parent.width - Theme.iconSize - Theme.spacingM
StyledText {
text: "Power Profile Degradation"
text: qsTr("Power Profile Degradation")
font.pixelSize: Theme.fontSizeLarge
color: Theme.error
font.weight: Font.Medium

View File

@@ -13,7 +13,10 @@ import qs.Widgets
DankPopout {
id: root
property string triggerSection: "right"
Ref {
service: VpnService
}
property var triggerScreen: null
function setTriggerPosition(x, y, width, section, screen) {
@@ -27,9 +30,9 @@ DankPopout {
popupWidth: 360
popupHeight: Math.min(Screen.height - 100, contentLoader.item ? contentLoader.item.implicitHeight : 260)
triggerX: Screen.width - 380 - Theme.spacingL
triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingS
triggerY: Theme.barHeight - 4 + SettingsData.dankBarSpacing
triggerWidth: 70
positioning: "center"
positioning: ""
screen: triggerScreen
shouldBeVisible: false
visible: shouldBeVisible
@@ -42,7 +45,7 @@ DankPopout {
color: Theme.popupBackground()
radius: Theme.cornerRadius
border.color: Theme.outlineMedium
border.width: 1
border.width: 0
antialiasing: true
smooth: true
focus: true
@@ -60,7 +63,7 @@ DankPopout {
color: "transparent"
radius: parent.radius + 3
border.color: Qt.rgba(0, 0, 0, 0.05)
border.width: 1
border.width: 0
z: -3
}
@@ -70,7 +73,7 @@ DankPopout {
color: "transparent"
radius: parent.radius + 2
border.color: Theme.shadowMedium
border.width: 1
border.width: 0
z: -2
}
@@ -78,7 +81,7 @@ DankPopout {
anchors.fill: parent
color: "transparent"
border.color: Theme.outlineStrong
border.width: 1
border.width: 0
radius: parent.radius
z: -1
}
@@ -97,7 +100,7 @@ DankPopout {
height: 32
StyledText {
text: "VPN Connections"
text: qsTr("VPN Connections")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
@@ -142,7 +145,7 @@ DankPopout {
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainerHigh.r, Theme.surfaceContainerHigh.g, Theme.surfaceContainerHigh.b, Theme.getContentBackgroundAlpha() * 0.6)
border.color: Theme.outlineStrong
border.width: 1
border.width: 0
clip: true
Column {
@@ -193,7 +196,7 @@ DankPopout {
visible: VpnService.connected
width: 130
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
border.width: 1
border.width: 0
border.color: Theme.outlineLight
Row {
@@ -207,7 +210,7 @@ DankPopout {
}
StyledText {
text: "Disconnect"
text: qsTr("Disconnect")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
@@ -263,14 +266,14 @@ DankPopout {
}
StyledText {
text: "No VPN profiles found"
text: qsTr("No VPN profiles found")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: "Add a VPN in NetworkManager"
text: qsTr("Add a VPN in NetworkManager")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter

View File

@@ -0,0 +1,84 @@
import QtQuick
import qs.Common
Item {
id: root
property var widgetsModel: null
property var components: null
property bool noBackground: false
required property var axis
property var parentScreen: null
property real widgetThickness: 30
property real barThickness: 48
readonly property bool isVertical: axis?.isVertical ?? false
implicitHeight: layoutLoader.item ? layoutLoader.item.implicitHeight : 0
implicitWidth: layoutLoader.item ? layoutLoader.item.implicitWidth : 0
Loader {
id: layoutLoader
width: parent.width
height: parent.height
sourceComponent: root.isVertical ? columnComp : rowComp
}
Component {
id: rowComp
Row {
spacing: noBackground ? 2 : Theme.spacingXS
anchors.right: parent ? parent.right : undefined
Repeater {
model: root.widgetsModel
Item {
width: widgetLoader.item ? widgetLoader.item.width : 0
height: widgetLoader.item ? widgetLoader.item.height : 0
WidgetHost {
id: widgetLoader
anchors.verticalCenter: parent.verticalCenter
widgetId: model.widgetId
widgetData: model
spacerSize: model.size || 20
components: root.components
isInColumn: false
axis: root.axis
section: "right"
parentScreen: root.parentScreen
widgetThickness: root.widgetThickness
barThickness: root.barThickness
}
}
}
}
}
Component {
id: columnComp
Column {
width: parent ? parent.width : 0
spacing: noBackground ? 2 : Theme.spacingXS
Repeater {
model: root.widgetsModel
Item {
width: parent.width
height: widgetLoader.item ? widgetLoader.item.height : 0
WidgetHost {
id: widgetLoader
anchors.horizontalCenter: parent.horizontalCenter
widgetId: model.widgetId
widgetData: model
spacerSize: model.size || 20
components: root.components
isInColumn: true
axis: root.axis
section: "right"
parentScreen: root.parentScreen
widgetThickness: root.widgetThickness
barThickness: root.barThickness
}
}
}
}
}
}

View File

@@ -0,0 +1,169 @@
import QtQuick
import Quickshell.Services.Mpris
import qs.Services
Loader {
id: root
property string widgetId: ""
property var widgetData: null
property int spacerSize: 20
property var components: null
property bool isInColumn: false
property var axis: null
property string section: "center"
property var parentScreen: null
property real widgetThickness: 30
property real barThickness: 48
asynchronous: false
active: getWidgetVisible(widgetId, DgopService.dgopAvailable) &&
(widgetId !== "music" || MprisController.activePlayer !== null)
sourceComponent: getWidgetComponent(widgetId, components)
opacity: getWidgetEnabled(widgetData?.enabled) ? 1 : 0
signal contentItemReady(var item)
Binding {
target: root.item
when: root.item && "parentScreen" in root.item
property: "parentScreen"
value: root.parentScreen
restoreMode: Binding.RestoreNone
}
Binding {
target: root.item
when: root.item && "section" in root.item
property: "section"
value: root.section
restoreMode: Binding.RestoreNone
}
Binding {
target: root.item
when: root.item && "widgetThickness" in root.item
property: "widgetThickness"
value: root.widgetThickness
restoreMode: Binding.RestoreNone
}
Binding {
target: root.item
when: root.item && "barThickness" in root.item
property: "barThickness"
value: root.barThickness
restoreMode: Binding.RestoreNone
}
Binding {
target: root.item
when: root.item && "axis" in root.item
property: "axis"
value: root.axis
restoreMode: Binding.RestoreNone
}
Binding {
target: root.item
when: root.item && "widgetData" in root.item
property: "widgetData"
value: root.widgetData
restoreMode: Binding.RestoreNone
}
onLoaded: {
if (item) {
contentItemReady(item)
if (widgetId === "spacer") {
item.spacerSize = Qt.binding(() => spacerSize)
}
if (axis && "isVertical" in item) {
try {
item.isVertical = axis.isVertical
} catch (e) {
}
}
if (item.pluginService !== undefined) {
var parts = widgetId.split(":")
var pluginId = parts[0]
var variantId = parts.length > 1 ? parts[1] : null
if (item.pluginId !== undefined) {
item.pluginId = pluginId
}
if (item.variantId !== undefined) {
item.variantId = variantId
}
if (item.variantData !== undefined && variantId) {
item.variantData = PluginService.getPluginVariantData(pluginId, variantId)
}
item.pluginService = PluginService
}
if (item.popoutService !== undefined) {
item.popoutService = PopoutService
}
}
}
function getWidgetComponent(widgetId, components) {
const componentMap = {
"launcherButton": components.launcherButtonComponent,
"workspaceSwitcher": components.workspaceSwitcherComponent,
"focusedWindow": components.focusedWindowComponent,
"runningApps": components.runningAppsComponent,
"clock": components.clockComponent,
"music": components.mediaComponent,
"weather": components.weatherComponent,
"systemTray": components.systemTrayComponent,
"privacyIndicator": components.privacyIndicatorComponent,
"clipboard": components.clipboardComponent,
"cpuUsage": components.cpuUsageComponent,
"memUsage": components.memUsageComponent,
"diskUsage": components.diskUsageComponent,
"cpuTemp": components.cpuTempComponent,
"gpuTemp": components.gpuTempComponent,
"notificationButton": components.notificationButtonComponent,
"battery": components.batteryComponent,
"controlCenterButton": components.controlCenterButtonComponent,
"idleInhibitor": components.idleInhibitorComponent,
"spacer": components.spacerComponent,
"separator": components.separatorComponent,
"network_speed_monitor": components.networkComponent,
"keyboard_layout_name": components.keyboardLayoutNameComponent,
"vpn": components.vpnComponent,
"notepadButton": components.notepadButtonComponent,
"colorPicker": components.colorPickerComponent,
"systemUpdate": components.systemUpdateComponent
}
if (componentMap[widgetId]) {
return componentMap[widgetId]
}
var parts = widgetId.split(":")
var pluginId = parts[0]
let pluginMap = PluginService.getWidgetComponents()
return pluginMap[pluginId] || null
}
function getWidgetVisible(widgetId, dgopAvailable) {
const widgetVisibility = {
"cpuUsage": dgopAvailable,
"memUsage": dgopAvailable,
"cpuTemp": dgopAvailable,
"gpuTemp": dgopAvailable,
"network_speed_monitor": dgopAvailable
}
return widgetVisibility[widgetId] ?? true
}
function getWidgetEnabled(enabled) {
return enabled !== false
}
}

View File

@@ -0,0 +1,126 @@
import QtQuick
import Quickshell.Services.UPower
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: battery
property bool isVertical: axis?.isVertical ?? false
property var axis: null
property bool batteryPopupVisible: false
property string section: "right"
property var popupTarget: null
property var parentScreen: null
property real widgetThickness: 30
property real barThickness: 48
readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetThickness / 30))
signal toggleBatteryPopup()
width: isVertical ? widgetThickness : (batteryContent.implicitWidth + horizontalPadding * 2)
height: isVertical ? (batteryColumn.implicitHeight + horizontalPadding * 2) : widgetThickness
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
color: {
if (SettingsData.dankBarNoBackground) {
return "transparent";
}
const baseColor = batteryArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor;
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
}
visible: true
Column {
id: batteryColumn
visible: battery.isVertical
anchors.centerIn: parent
spacing: 1
DankIcon {
name: BatteryService.getBatteryIcon()
size: Theme.barIconSize(barThickness)
color: {
if (!BatteryService.batteryAvailable) {
return Theme.surfaceText
}
if (BatteryService.isLowBattery && !BatteryService.isCharging) {
return Theme.error
}
if (BatteryService.isCharging || BatteryService.isPluggedIn) {
return Theme.primary
}
return Theme.surfaceText
}
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: BatteryService.batteryLevel.toString()
font.pixelSize: Theme.barTextSize(barThickness)
font.weight: Font.Medium
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
visible: BatteryService.batteryAvailable
}
}
Row {
id: batteryContent
visible: !battery.isVertical
anchors.centerIn: parent
spacing: SettingsData.dankBarNoBackground ? 1 : 2
DankIcon {
name: BatteryService.getBatteryIcon()
size: Theme.barIconSize(barThickness, -4)
color: {
if (!BatteryService.batteryAvailable) {
return Theme.surfaceText;
}
if (BatteryService.isLowBattery && !BatteryService.isCharging) {
return Theme.error;
}
if (BatteryService.isCharging || BatteryService.isPluggedIn) {
return Theme.primary;
}
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: `${BatteryService.batteryLevel}%`
font.pixelSize: Theme.barTextSize(barThickness)
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
visible: BatteryService.batteryAvailable
}
}
MouseArea {
id: batteryArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: {
if (popupTarget && popupTarget.setTriggerPosition) {
const globalPos = mapToGlobal(0, 0)
const currentScreen = parentScreen || Screen
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, width)
popupTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen)
}
toggleBatteryPopup();
}
}
}

View File

@@ -0,0 +1,57 @@
import QtQuick
import qs.Common
import qs.Widgets
Item {
id: root
property bool isActive: false
property bool isVertical: axis?.isVertical ?? false
property var axis: null
property string section: "right"
property var clipboardHistoryModal: null
property var parentScreen: null
property real widgetThickness: 30
property real barThickness: 48
readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetThickness / 30))
signal clicked()
width: widgetThickness
height: widgetThickness
MouseArea {
id: clipboardArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton
onPressed: {
root.clicked()
}
}
Rectangle {
id: clipboardContent
anchors.fill: parent
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
color: {
if (SettingsData.dankBarNoBackground) {
return "transparent"
}
const baseColor = clipboardArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency)
}
DankIcon {
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
name: "content_paste"
size: Theme.barIconSize(barThickness)
color: Theme.surfaceText
}
}
}

View File

@@ -0,0 +1,247 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Widgets
Rectangle {
id: root
property bool isVertical: axis?.isVertical ?? false
property var axis: null
property bool compactMode: false
property string section: "center"
property var popupTarget: null
property var parentScreen: null
property real barThickness: 48
property real widgetThickness: 30
readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 2 : Theme.spacingS
signal clockClicked
width: isVertical ? widgetThickness : (clockRow.implicitWidth + horizontalPadding * 2)
height: isVertical ? (clockColumn.implicitHeight + horizontalPadding * 2) : widgetThickness
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
color: {
if (SettingsData.dankBarNoBackground) {
return "transparent";
}
const baseColor = clockMouseArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor;
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
}
Column {
id: clockColumn
visible: root.isVertical
anchors.centerIn: parent
spacing: -2
Row {
spacing: 0
anchors.horizontalCenter: parent.horizontalCenter
StyledText {
text: {
if (SettingsData.use24HourClock) {
return String(systemClock?.date?.getHours()).padStart(2, '0').charAt(0)
} else {
const hours = systemClock?.date?.getHours()
const display = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours
return String(display).padStart(2, '0').charAt(0)
}
}
font.pixelSize: Theme.barTextSize(barThickness)
color: Theme.surfaceText
font.weight: Font.Normal
width: 9
horizontalAlignment: Text.AlignHCenter
}
StyledText {
text: {
if (SettingsData.use24HourClock) {
return String(systemClock?.date?.getHours()).padStart(2, '0').charAt(1)
} else {
const hours = systemClock?.date?.getHours()
const display = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours
return String(display).padStart(2, '0').charAt(1)
}
}
font.pixelSize: Theme.barTextSize(barThickness)
color: Theme.surfaceText
font.weight: Font.Normal
width: 9
horizontalAlignment: Text.AlignHCenter
}
}
Row {
spacing: 0
anchors.horizontalCenter: parent.horizontalCenter
StyledText {
text: String(systemClock?.date?.getMinutes()).padStart(2, '0').charAt(0)
font.pixelSize: Theme.barTextSize(barThickness)
color: Theme.surfaceText
font.weight: Font.Normal
width: 9
horizontalAlignment: Text.AlignHCenter
}
StyledText {
text: String(systemClock?.date?.getMinutes()).padStart(2, '0').charAt(1)
font.pixelSize: Theme.barTextSize(barThickness)
color: Theme.surfaceText
font.weight: Font.Normal
width: 9
horizontalAlignment: Text.AlignHCenter
}
}
Item {
width: 12
height: Theme.spacingM
anchors.horizontalCenter: parent.horizontalCenter
Rectangle {
width: 12
height: 1
color: Theme.outlineButton
anchors.centerIn: parent
}
}
Row {
spacing: 0
anchors.horizontalCenter: parent.horizontalCenter
StyledText {
text: {
const locale = Qt.locale()
const dateFormatShort = locale.dateFormat(Locale.ShortFormat)
const dayFirst = dateFormatShort.indexOf('d') < dateFormatShort.indexOf('M')
const value = dayFirst ? String(systemClock?.date?.getDate()).padStart(2, '0') : String(systemClock?.date?.getMonth() + 1).padStart(2, '0')
return value.charAt(0)
}
font.pixelSize: Theme.barTextSize(barThickness)
color: Theme.primary
font.weight: Font.Light
width: 9
horizontalAlignment: Text.AlignHCenter
}
StyledText {
text: {
const locale = Qt.locale()
const dateFormatShort = locale.dateFormat(Locale.ShortFormat)
const dayFirst = dateFormatShort.indexOf('d') < dateFormatShort.indexOf('M')
const value = dayFirst ? String(systemClock?.date?.getDate()).padStart(2, '0') : String(systemClock?.date?.getMonth() + 1).padStart(2, '0')
return value.charAt(1)
}
font.pixelSize: Theme.barTextSize(barThickness)
color: Theme.primary
font.weight: Font.Light
width: 9
horizontalAlignment: Text.AlignHCenter
}
}
Row {
spacing: 0
anchors.horizontalCenter: parent.horizontalCenter
StyledText {
text: {
const locale = Qt.locale()
const dateFormatShort = locale.dateFormat(Locale.ShortFormat)
const dayFirst = dateFormatShort.indexOf('d') < dateFormatShort.indexOf('M')
const value = dayFirst ? String(systemClock?.date?.getMonth() + 1).padStart(2, '0') : String(systemClock?.date?.getDate()).padStart(2, '0')
return value.charAt(0)
}
font.pixelSize: Theme.barTextSize(barThickness)
color: Theme.primary
font.weight: Font.Light
width: 9
horizontalAlignment: Text.AlignHCenter
}
StyledText {
text: {
const locale = Qt.locale()
const dateFormatShort = locale.dateFormat(Locale.ShortFormat)
const dayFirst = dateFormatShort.indexOf('d') < dateFormatShort.indexOf('M')
const value = dayFirst ? String(systemClock?.date?.getMonth() + 1).padStart(2, '0') : String(systemClock?.date?.getDate()).padStart(2, '0')
return value.charAt(1)
}
font.pixelSize: Theme.barTextSize(barThickness)
color: Theme.primary
font.weight: Font.Light
width: 9
horizontalAlignment: Text.AlignHCenter
}
}
}
Row {
id: clockRow
visible: !root.isVertical
anchors.centerIn: parent
spacing: Theme.spacingS
StyledText {
text: {
const format = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP"
return systemClock?.date?.toLocaleTimeString(Qt.locale(), format)
}
font.pixelSize: Theme.barTextSize(barThickness)
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "•"
font.pixelSize: Theme.fontSizeSmall
color: Theme.outlineButton
anchors.verticalCenter: parent.verticalCenter
visible: !SettingsData.clockCompactMode
}
StyledText {
text: {
if (SettingsData.clockDateFormat && SettingsData.clockDateFormat.length > 0) {
return systemClock?.date?.toLocaleDateString(Qt.locale(), SettingsData.clockDateFormat)
}
return systemClock?.date?.toLocaleDateString(Qt.locale(), "ddd d")
}
font.pixelSize: Theme.barTextSize(barThickness)
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
visible: !SettingsData.clockCompactMode
}
}
SystemClock {
id: systemClock
precision: SystemClock.Seconds
}
MouseArea {
id: clockMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: {
if (popupTarget && popupTarget.setTriggerPosition) {
const globalPos = mapToGlobal(0, 0)
const currentScreen = parentScreen || Screen
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, width)
popupTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen)
}
root.clockClicked()
}
}
}

View File

@@ -5,21 +5,23 @@ import qs.Widgets
Rectangle {
id: root
property bool isVertical: axis?.isVertical ?? false
property var axis: null
property bool isActive: false
property string section: "right"
property var popupTarget: null
property var parentScreen: null
property real widgetHeight: 30
property real barHeight: 48
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
property real widgetThickness: 30
property real barThickness: 48
readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetThickness / 30))
signal clicked()
width: colorPickerIcon.width + horizontalPadding * 2
height: widgetHeight
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
width: isVertical ? widgetThickness : (colorPickerIcon.width + horizontalPadding * 2)
height: isVertical ? (colorPickerIcon.height + horizontalPadding * 2) : widgetThickness
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
color: {
if (SettingsData.topBarNoBackground) {
if (SettingsData.dankBarNoBackground) {
return "transparent";
}
@@ -32,7 +34,7 @@ Rectangle {
anchors.centerIn: parent
name: "palette"
size: Theme.iconSize - 6
size: Theme.barIconSize(barThickness, -4)
color: colorPickerArea.containsMouse || root.isActive ? Theme.primary : Theme.surfaceText
}
@@ -43,12 +45,10 @@ Rectangle {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: {
console.log("Color picker button clicked!")
root.colorPickerRequested();
}
}
// Signal to notify TopBar to open color picker
signal colorPickerRequested()
}

View File

@@ -0,0 +1,268 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
property bool isVertical: axis?.isVertical ?? false
property var axis: null
property bool isActive: false
property string section: "right"
property var popupTarget: null
property var parentScreen: null
property var widgetData: null
property bool showNetworkIcon: SettingsData.controlCenterShowNetworkIcon
property bool showBluetoothIcon: SettingsData.controlCenterShowBluetoothIcon
property bool showAudioIcon: SettingsData.controlCenterShowAudioIcon
property real widgetThickness: 30
property real barThickness: 48
readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetThickness / 30))
signal clicked()
width: isVertical ? widgetThickness : (controlIndicators.implicitWidth + horizontalPadding * 2)
height: isVertical ? (controlColumn.implicitHeight + horizontalPadding * 2) : widgetThickness
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
color: {
if (SettingsData.dankBarNoBackground) {
return "transparent";
}
const baseColor = controlCenterArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor;
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
}
Column {
id: controlColumn
visible: root.isVertical
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: {
if (NetworkService.wifiToggling) {
return "sync"
}
if (NetworkService.networkStatus === "ethernet") {
return "lan"
}
return NetworkService.wifiSignalIcon
}
size: Theme.barIconSize(barThickness)
color: {
if (NetworkService.wifiToggling) {
return Theme.primary
}
return NetworkService.networkStatus !== "disconnected" ? Theme.primary : Theme.outlineButton
}
anchors.horizontalCenter: parent.horizontalCenter
visible: root.showNetworkIcon && NetworkService.networkAvailable
}
DankIcon {
name: "bluetooth"
size: Theme.barIconSize(barThickness)
color: BluetoothService.enabled ? Theme.primary : Theme.outlineButton
anchors.horizontalCenter: parent.horizontalCenter
visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled
}
Rectangle {
width: audioIconV.implicitWidth + 4
height: audioIconV.implicitHeight + 4
color: "transparent"
anchors.horizontalCenter: parent.horizontalCenter
visible: root.showAudioIcon
DankIcon {
id: audioIconV
name: {
if (AudioService.sink && AudioService.sink.audio) {
if (AudioService.sink.audio.muted || AudioService.sink.audio.volume === 0) {
return "volume_off"
} else if (AudioService.sink.audio.volume * 100 < 33) {
return "volume_down"
} else {
return "volume_up"
}
}
return "volume_up"
}
size: Theme.barIconSize(barThickness)
color: Theme.surfaceText
anchors.centerIn: parent
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
onWheel: function(wheelEvent) {
let delta = wheelEvent.angleDelta.y
let currentVolume = (AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.volume * 100) || 0
let newVolume
if (delta > 0) {
newVolume = Math.min(100, currentVolume + 5)
} else {
newVolume = Math.max(0, currentVolume - 5)
}
if (AudioService.sink && AudioService.sink.audio) {
AudioService.sink.audio.muted = false
AudioService.sink.audio.volume = newVolume / 100
AudioService.volumeChanged()
}
wheelEvent.accepted = true
}
}
}
DankIcon {
name: "settings"
size: Theme.barIconSize(barThickness)
color: controlCenterArea.containsMouse || root.isActive ? Theme.primary : Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
visible: !root.showNetworkIcon && !root.showBluetoothIcon && !root.showAudioIcon
}
}
Row {
id: controlIndicators
visible: !root.isVertical
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
id: networkIcon
name: {
if (NetworkService.wifiToggling) {
return "sync";
}
if (NetworkService.networkStatus === "ethernet") {
return "lan";
}
return NetworkService.wifiSignalIcon;
}
size: Theme.barIconSize(barThickness)
color: {
if (NetworkService.wifiToggling) {
return Theme.primary;
}
return NetworkService.networkStatus !== "disconnected" ? Theme.primary : Theme.outlineButton;
}
anchors.verticalCenter: parent.verticalCenter
visible: root.showNetworkIcon && NetworkService.networkAvailable
}
DankIcon {
id: bluetoothIcon
name: "bluetooth"
size: Theme.barIconSize(barThickness)
color: BluetoothService.enabled ? Theme.primary : Theme.outlineButton
anchors.verticalCenter: parent.verticalCenter
visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled
}
Rectangle {
width: audioIcon.implicitWidth + 4
height: audioIcon.implicitHeight + 4
color: "transparent"
anchors.verticalCenter: parent.verticalCenter
visible: root.showAudioIcon
DankIcon {
id: audioIcon
name: {
if (AudioService.sink && AudioService.sink.audio) {
if (AudioService.sink.audio.muted || AudioService.sink.audio.volume === 0) {
return "volume_off";
} else if (AudioService.sink.audio.volume * 100 < 33) {
return "volume_down";
} else {
return "volume_up";
}
}
return "volume_up";
}
size: Theme.barIconSize(barThickness)
color: Theme.surfaceText
anchors.centerIn: parent
}
MouseArea {
id: audioWheelArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
onWheel: function(wheelEvent) {
let delta = wheelEvent.angleDelta.y;
let currentVolume = (AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.volume * 100) || 0;
let newVolume;
if (delta > 0) {
newVolume = Math.min(100, currentVolume + 5);
} else {
newVolume = Math.max(0, currentVolume - 5);
}
if (AudioService.sink && AudioService.sink.audio) {
AudioService.sink.audio.muted = false;
AudioService.sink.audio.volume = newVolume / 100;
AudioService.volumeChanged();
}
wheelEvent.accepted = true;
}
}
}
DankIcon {
name: "mic"
size: Theme.barIconSize(barThickness)
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
visible: false // TODO: Add mic detection
}
// Fallback settings icon when all other icons are hidden
DankIcon {
name: "settings"
size: Theme.barIconSize(barThickness)
color: controlCenterArea.containsMouse || root.isActive ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
visible: !root.showNetworkIcon && !root.showBluetoothIcon && !root.showAudioIcon
}
}
MouseArea {
id: controlCenterArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: {
if (popupTarget && popupTarget.setTriggerPosition) {
const globalPos = mapToGlobal(0, 0)
const currentScreen = parentScreen || Screen
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, width)
popupTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen)
}
root.clicked();
}
}
}

View File

@@ -7,21 +7,25 @@ import qs.Widgets
Rectangle {
id: root
property bool isVertical: axis?.isVertical ?? false
property var axis: null
property bool showPercentage: true
property bool showIcon: true
property var toggleProcessList
property string section: "right"
property var popupTarget: null
property var parentScreen: null
property real barHeight: 48
property real widgetHeight: 30
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
property real barThickness: 48
property real widgetThickness: 30
property var widgetData: null
property bool minimumWidth: (widgetData && widgetData.minimumWidth !== undefined) ? widgetData.minimumWidth : true
readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetThickness / 30))
width: cpuContent.implicitWidth + horizontalPadding * 2
height: widgetHeight
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
width: isVertical ? widgetThickness : (cpuContent.implicitWidth + horizontalPadding * 2)
height: isVertical ? (cpuColumn.implicitHeight + horizontalPadding * 2) : widgetThickness
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
color: {
if (SettingsData.topBarNoBackground) {
if (SettingsData.dankBarNoBackground) {
return "transparent";
}
@@ -43,11 +47,10 @@ Rectangle {
cursorShape: Qt.PointingHandCursor
onPressed: {
if (popupTarget && popupTarget.setTriggerPosition) {
const globalPos = mapToGlobal(0, 0);
const currentScreen = parentScreen || Screen;
const screenX = currentScreen.x || 0;
const relativeX = globalPos.x - screenX;
popupTarget.setTriggerPosition(relativeX, barHeight + Theme.spacingXS, width, section, currentScreen);
const globalPos = mapToGlobal(0, 0)
const currentScreen = parentScreen || Screen
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, width)
popupTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen)
}
DgopService.setSortBy("cpu");
if (root.toggleProcessList) {
@@ -57,15 +60,53 @@ Rectangle {
}
}
Column {
id: cpuColumn
visible: root.isVertical
anchors.centerIn: parent
spacing: 1
DankIcon {
name: "memory"
size: Theme.barIconSize(barThickness)
color: {
if (DgopService.cpuUsage > 80) {
return Theme.tempDanger;
}
if (DgopService.cpuUsage > 60) {
return Theme.tempWarning;
}
return Theme.surfaceText;
}
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: {
if (DgopService.cpuUsage === undefined || DgopService.cpuUsage === null || DgopService.cpuUsage === 0) {
return "--";
}
return DgopService.cpuUsage.toFixed(0);
}
font.pixelSize: Theme.barTextSize(barThickness)
font.weight: Font.Medium
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
}
Row {
id: cpuContent
visible: !root.isVertical
anchors.centerIn: parent
spacing: 3
DankIcon {
name: "memory"
size: Theme.iconSize - 8
size: Theme.barIconSize(barThickness)
color: {
if (DgopService.cpuUsage > 80) {
return Theme.tempDanger;
@@ -88,7 +129,7 @@ Rectangle {
return DgopService.cpuUsage.toFixed(0) + "%";
}
font.pixelSize: Theme.fontSizeSmall
font.pixelSize: Theme.barTextSize(barThickness)
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
@@ -97,12 +138,12 @@ Rectangle {
StyledTextMetrics {
id: cpuBaseline
font.pixelSize: Theme.fontSizeSmall
font.pixelSize: Theme.barTextSize(barThickness)
font.weight: Font.Medium
text: "100%"
}
width: Math.max(cpuBaseline.width, paintedWidth)
width: root.minimumWidth ? Math.max(cpuBaseline.width, paintedWidth) : paintedWidth
Behavior on width {
NumberAnimation {

View File

@@ -0,0 +1,159 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
property bool isVertical: axis?.isVertical ?? false
property var axis: null
property bool showPercentage: true
property bool showIcon: true
property var toggleProcessList
property string section: "right"
property var popupTarget: null
property var parentScreen: null
property real barThickness: 48
property real widgetThickness: 30
property var widgetData: null
property bool minimumWidth: (widgetData && widgetData.minimumWidth !== undefined) ? widgetData.minimumWidth : true
readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetThickness / 30))
width: isVertical ? widgetThickness : (cpuTempContent.implicitWidth + horizontalPadding * 2)
height: isVertical ? (cpuTempColumn.implicitHeight + horizontalPadding * 2) : widgetThickness
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
color: {
if (SettingsData.dankBarNoBackground) {
return "transparent";
}
const baseColor = cpuTempArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor;
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
}
Component.onCompleted: {
DgopService.addRef(["cpu"]);
}
Component.onDestruction: {
DgopService.removeRef(["cpu"]);
}
MouseArea {
id: cpuTempArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: {
if (popupTarget && popupTarget.setTriggerPosition) {
const globalPos = mapToGlobal(0, 0)
const currentScreen = parentScreen || Screen
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, width)
popupTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen)
}
DgopService.setSortBy("cpu");
if (root.toggleProcessList) {
root.toggleProcessList();
}
}
}
Column {
id: cpuTempColumn
visible: root.isVertical
anchors.centerIn: parent
spacing: 1
DankIcon {
name: "device_thermostat"
size: Theme.barIconSize(barThickness)
color: {
if (DgopService.cpuTemperature > 85) {
return Theme.tempDanger;
}
if (DgopService.cpuTemperature > 69) {
return Theme.tempWarning;
}
return Theme.surfaceText;
}
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: {
if (DgopService.cpuTemperature === undefined || DgopService.cpuTemperature === null || DgopService.cpuTemperature < 0) {
return "--";
}
return Math.round(DgopService.cpuTemperature).toString();
}
font.pixelSize: Theme.barTextSize(barThickness)
font.weight: Font.Medium
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
}
Row {
id: cpuTempContent
visible: !root.isVertical
anchors.centerIn: parent
spacing: 3
DankIcon {
name: "device_thermostat"
size: Theme.barIconSize(barThickness)
color: {
if (DgopService.cpuTemperature > 85) {
return Theme.tempDanger;
}
if (DgopService.cpuTemperature > 69) {
return Theme.tempWarning;
}
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: {
if (DgopService.cpuTemperature === undefined || DgopService.cpuTemperature === null || DgopService.cpuTemperature < 0) {
return "--°";
}
return Math.round(DgopService.cpuTemperature) + "°";
}
font.pixelSize: Theme.barTextSize(barThickness)
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignLeft
elide: Text.ElideNone
StyledTextMetrics {
id: tempBaseline
font.pixelSize: Theme.barTextSize(barThickness)
font.weight: Font.Medium
text: "100°"
}
width: root.minimumWidth ? Math.max(tempBaseline.width, paintedWidth) : paintedWidth
Behavior on width {
NumberAnimation {
duration: 120
easing.type: Easing.OutCubic
}
}
}
}
}

View File

@@ -7,10 +7,14 @@ import qs.Widgets
Rectangle {
id: root
property bool isVertical: axis?.isVertical ?? false
property var axis: null
property var widgetData: null
property real widgetHeight: 30
property var parentScreen: null
property real widgetThickness: 30
property real barThickness: 48
property string mountPath: (widgetData && widgetData.mountPath !== undefined) ? widgetData.mountPath : "/"
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetThickness / 30))
property var selectedMount: {
if (!DgopService.diskMounts || DgopService.diskMounts.length === 0) {
@@ -46,11 +50,11 @@ Rectangle {
return parseFloat(percentStr) || 0
}
width: diskContent.implicitWidth + horizontalPadding * 2
height: widgetHeight
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
width: isVertical ? widgetThickness : (diskContent.implicitWidth + horizontalPadding * 2)
height: isVertical ? (diskColumn.implicitHeight + horizontalPadding * 2) : widgetThickness
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
color: {
if (SettingsData.topBarNoBackground) {
if (SettingsData.dankBarNoBackground) {
return "transparent"
}
@@ -100,16 +104,83 @@ Rectangle {
target: SettingsData
}
Loader {
id: tooltipLoader
active: false
sourceComponent: DankTooltip {}
}
MouseArea {
id: diskArea
anchors.fill: parent
hoverEnabled: root.isVertical
onEntered: {
if (root.isVertical && root.selectedMount) {
tooltipLoader.active = true
if (tooltipLoader.item) {
const globalPos = mapToGlobal(width / 2, height / 2)
const currentScreen = root.parentScreen || Screen
const screenX = currentScreen ? currentScreen.x : 0
const screenY = currentScreen ? currentScreen.y : 0
const relativeY = globalPos.y - screenY
const tooltipX = root.axis?.edge === "left" ? (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS) : (currentScreen.width - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS)
const isLeft = root.axis?.edge === "left"
tooltipLoader.item.show(root.selectedMount.mount, screenX + tooltipX, relativeY, currentScreen, isLeft, !isLeft)
}
}
}
onExited: {
if (tooltipLoader.item) {
tooltipLoader.item.hide()
}
tooltipLoader.active = false
}
}
Column {
id: diskColumn
visible: root.isVertical
anchors.centerIn: parent
spacing: 1
DankIcon {
name: "storage"
size: Theme.barIconSize(barThickness)
color: {
if (root.diskUsagePercent > 90) {
return Theme.tempDanger
}
if (root.diskUsagePercent > 75) {
return Theme.tempWarning
}
return Theme.surfaceText
}
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: {
if (root.diskUsagePercent === undefined || root.diskUsagePercent === null || root.diskUsagePercent === 0) {
return "--"
}
return root.diskUsagePercent.toFixed(0)
}
font.pixelSize: Theme.barTextSize(barThickness)
font.weight: Font.Medium
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
}
Row {
id: diskContent
visible: !root.isVertical
anchors.centerIn: parent
spacing: 3
DankIcon {
name: "storage"
size: Theme.iconSize - 8
size: Theme.barIconSize(barThickness)
color: {
if (root.diskUsagePercent > 90) {
return Theme.tempDanger
@@ -129,7 +200,7 @@ Rectangle {
}
return root.selectedMount.mount
}
font.pixelSize: Theme.fontSizeSmall
font.pixelSize: Theme.barTextSize(barThickness)
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
@@ -144,7 +215,7 @@ Rectangle {
}
return root.diskUsagePercent.toFixed(0) + "%"
}
font.pixelSize: Theme.fontSizeSmall
font.pixelSize: Theme.barTextSize(barThickness)
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
@@ -153,7 +224,7 @@ Rectangle {
StyledTextMetrics {
id: diskBaseline
font.pixelSize: Theme.fontSizeSmall
font.pixelSize: Theme.barTextSize(barThickness)
font.weight: Font.Medium
text: "100%"
}

View File

@@ -0,0 +1,273 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import Quickshell.Hyprland
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
property bool isVertical: axis?.isVertical ?? false
property var axis: null
property var parentScreen
property bool compactMode: SettingsData.focusedWindowCompactMode
property int availableWidth: 400
property real widgetThickness: 30
property real barThickness: 48
readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 2 : Theme.spacingS
readonly property int baseWidth: contentRow.implicitWidth + horizontalPadding * 2
readonly property int maxNormalWidth: 456
readonly property int maxCompactWidth: 288
readonly property Toplevel activeWindow: ToplevelManager.activeToplevel
property var activeDesktopEntry: null
Component.onCompleted: {
updateDesktopEntry()
}
Connections {
target: DesktopEntries
function onApplicationsChanged() {
root.updateDesktopEntry()
}
}
Connections {
target: root
function onActiveWindowChanged() {
root.updateDesktopEntry()
}
}
function updateDesktopEntry() {
if (activeWindow && activeWindow.appId) {
const moddedId = Paths.moddedAppId(activeWindow.appId)
activeDesktopEntry = DesktopEntries.heuristicLookup(moddedId)
} else {
activeDesktopEntry = null
}
}
readonly property bool hasWindowsOnCurrentWorkspace: {
if (CompositorService.isNiri) {
let currentWorkspaceId = null
for (var i = 0; i < NiriService.allWorkspaces.length; i++) {
const ws = NiriService.allWorkspaces[i]
if (ws.is_focused) {
currentWorkspaceId = ws.id
break
}
}
if (!currentWorkspaceId) {
return false
}
const workspaceWindows = NiriService.windows.filter(w => w.workspace_id === currentWorkspaceId)
return workspaceWindows.length > 0 && activeWindow && activeWindow.title
}
if (CompositorService.isHyprland) {
if (!Hyprland.focusedWorkspace || !activeWindow || !activeWindow.title) {
return false
}
const hyprlandToplevels = Array.from(Hyprland.toplevels.values)
const activeHyprToplevel = hyprlandToplevels.find(t => t.wayland === activeWindow)
if (!activeHyprToplevel || !activeHyprToplevel.workspace) {
return false
}
return activeHyprToplevel.workspace.id === Hyprland.focusedWorkspace.id
}
return activeWindow && activeWindow.title
}
width: !hasWindowsOnCurrentWorkspace ? 0 : (isVertical ? widgetThickness : (compactMode ? Math.min(baseWidth, maxCompactWidth) : Math.min(baseWidth, maxNormalWidth)))
height: !hasWindowsOnCurrentWorkspace ? 0 : (isVertical ? widgetThickness : widgetThickness)
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
color: {
if (!activeWindow || !activeWindow.title) {
return "transparent";
}
if (SettingsData.dankBarNoBackground) {
return "transparent";
}
const baseColor = mouseArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor;
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
}
clip: true
visible: hasWindowsOnCurrentWorkspace
IconImage {
id: appIcon
anchors.centerIn: parent
width: 18
height: 18
visible: root.isVertical && activeWindow && status === Image.Ready
source: {
if (!activeWindow || !activeWindow.appId) return ""
const moddedId = Paths.moddedAppId(activeWindow.appId)
if (moddedId.toLowerCase().includes("steam_app")) return ""
return Quickshell.iconPath(activeDesktopEntry?.icon, true)
}
smooth: true
mipmap: true
asynchronous: true
}
DankIcon {
anchors.centerIn: parent
size: 18
name: "sports_esports"
color: Theme.surfaceText
visible: {
if (!root.isVertical || !activeWindow || !activeWindow.appId) return false
const moddedId = Paths.moddedAppId(activeWindow.appId)
return moddedId.toLowerCase().includes("steam_app")
}
}
Text {
anchors.centerIn: parent
visible: {
if (!root.isVertical || !activeWindow || !activeWindow.appId) return false
if (appIcon.status === Image.Ready) return false
const moddedId = Paths.moddedAppId(activeWindow.appId)
return !moddedId.toLowerCase().includes("steam_app")
}
text: {
if (!activeWindow || !activeWindow.appId) return "?"
if (activeDesktopEntry && activeDesktopEntry.name) {
return activeDesktopEntry.name.charAt(0).toUpperCase()
}
return activeWindow.appId.charAt(0).toUpperCase()
}
font.pixelSize: 10
color: Theme.surfaceText
font.weight: Font.Medium
}
Row {
id: contentRow
anchors.centerIn: parent
spacing: Theme.spacingS
visible: !root.isVertical
StyledText {
id: appText
text: {
if (!activeWindow || !activeWindow.appId) {
return "";
}
const desktopEntry = DesktopEntries.heuristicLookup(activeWindow.appId);
return desktopEntry && desktopEntry.name ? desktopEntry.name : activeWindow.appId;
}
font.pixelSize: Theme.barTextSize(barThickness)
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
maximumLineCount: 1
width: Math.min(implicitWidth, compactMode ? 80 : 180)
visible: !compactMode && text.length > 0
}
StyledText {
text: "•"
font.pixelSize: Theme.barTextSize(barThickness)
color: Theme.outlineButton
anchors.verticalCenter: parent.verticalCenter
visible: !compactMode && appText.text && titleText.text
}
StyledText {
id: titleText
text: {
const title = activeWindow && activeWindow.title ? activeWindow.title : "";
const appName = appText.text;
if (!title || !appName) {
return title;
}
if (title.endsWith(" - " + appName)) {
return title.substring(0, title.length - (" - " + appName).length);
}
if (title.endsWith(appName)) {
return title.substring(0, title.length - appName.length).replace(/ - $/, "");
}
return title;
}
font.pixelSize: Theme.barTextSize(barThickness)
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
maximumLineCount: 1
width: Math.min(implicitWidth, compactMode ? 280 : 250)
visible: text.length > 0
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: root.isVertical
onEntered: {
if (root.isVertical && activeWindow && activeWindow.appId && root.parentScreen) {
tooltipLoader.active = true
if (tooltipLoader.item) {
const globalPos = mapToGlobal(width / 2, height / 2)
const currentScreen = root.parentScreen
const screenX = currentScreen ? currentScreen.x : 0
const screenY = currentScreen ? currentScreen.y : 0
const relativeY = globalPos.y - screenY
const tooltipX = root.axis?.edge === "left" ? (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS) : (currentScreen.width - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS)
const appName = activeDesktopEntry && activeDesktopEntry.name ? activeDesktopEntry.name : activeWindow.appId
const title = activeWindow.title || ""
const tooltipText = appName + (title ? " • " + title : "")
const isLeft = root.axis?.edge === "left"
tooltipLoader.item.show(tooltipText, screenX + tooltipX, relativeY, currentScreen, isLeft, !isLeft)
}
}
}
onExited: {
if (tooltipLoader.item) {
tooltipLoader.item.hide()
}
tooltipLoader.active = false
}
}
Loader {
id: tooltipLoader
active: false
sourceComponent: DankTooltip {}
}
Behavior on width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}

View File

@@ -7,6 +7,8 @@ import qs.Widgets
Rectangle {
id: root
property bool isVertical: axis?.isVertical ?? false
property var axis: null
property bool showPercentage: true
property bool showIcon: true
property var toggleProcessList
@@ -14,10 +16,11 @@ Rectangle {
property var popupTarget: null
property var parentScreen: null
property var widgetData: null
property real barHeight: 48
property real widgetHeight: 30
property real barThickness: 48
property real widgetThickness: 30
property int selectedGpuIndex: (widgetData && widgetData.selectedGpuIndex !== undefined) ? widgetData.selectedGpuIndex : 0
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
property bool minimumWidth: (widgetData && widgetData.minimumWidth !== undefined) ? widgetData.minimumWidth : true
readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetThickness / 30))
property real displayTemp: {
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
return 0;
@@ -37,11 +40,11 @@ Rectangle {
const sectionId = sections[s];
let widgets = [];
if (sectionId === "left") {
widgets = SettingsData.topBarLeftWidgets.slice();
widgets = SettingsData.dankBarLeftWidgets.slice();
} else if (sectionId === "center") {
widgets = SettingsData.topBarCenterWidgets.slice();
widgets = SettingsData.dankBarCenterWidgets.slice();
} else if (sectionId === "right") {
widgets = SettingsData.topBarRightWidgets.slice();
widgets = SettingsData.dankBarRightWidgets.slice();
}
for (let i = 0; i < widgets.length; i++) {
const widget = widgets[i];
@@ -53,11 +56,11 @@ Rectangle {
"pciId": pciId
};
if (sectionId === "left") {
SettingsData.setTopBarLeftWidgets(widgets);
SettingsData.setDankBarLeftWidgets(widgets);
} else if (sectionId === "center") {
SettingsData.setTopBarCenterWidgets(widgets);
SettingsData.setDankBarCenterWidgets(widgets);
} else if (sectionId === "right") {
SettingsData.setTopBarRightWidgets(widgets);
SettingsData.setDankBarRightWidgets(widgets);
}
return ;
}
@@ -65,11 +68,11 @@ Rectangle {
}
}
width: gpuTempContent.implicitWidth + horizontalPadding * 2
height: widgetHeight
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
width: isVertical ? widgetThickness : (gpuTempContent.implicitWidth + horizontalPadding * 2)
height: isVertical ? (gpuTempColumn.implicitHeight + horizontalPadding * 2) : widgetThickness
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
color: {
if (SettingsData.topBarNoBackground) {
if (SettingsData.dankBarNoBackground) {
return "transparent";
}
@@ -78,20 +81,14 @@ Rectangle {
}
Component.onCompleted: {
DgopService.addRef(["gpu"]);
console.log("GpuTemperature widget - pciId:", widgetData ? widgetData.pciId : "no widgetData", "selectedGpuIndex:", widgetData ? widgetData.selectedGpuIndex : "no widgetData");
// Add this widget's PCI ID to the service
if (widgetData && widgetData.pciId) {
console.log("Adding GPU PCI ID to service:", widgetData.pciId);
DgopService.addGpuPciId(widgetData.pciId);
} else {
console.log("No PCI ID in widget data, starting auto-detection");
// No PCI ID saved, auto-detect and save the first GPU
autoSaveTimer.running = true;
}
}
Component.onDestruction: {
DgopService.removeRef(["gpu"]);
// Remove this widget's PCI ID from the service
if (widgetData && widgetData.pciId) {
DgopService.removeGpuPciId(widgetData.pciId);
}
@@ -117,11 +114,10 @@ Rectangle {
cursorShape: Qt.PointingHandCursor
onPressed: {
if (popupTarget && popupTarget.setTriggerPosition) {
const globalPos = mapToGlobal(0, 0);
const currentScreen = parentScreen || Screen;
const screenX = currentScreen.x || 0;
const relativeX = globalPos.x - screenX;
popupTarget.setTriggerPosition(relativeX, barHeight + Theme.spacingXS, width, section, currentScreen);
const globalPos = mapToGlobal(0, 0)
const currentScreen = parentScreen || Screen
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, width)
popupTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen)
}
DgopService.setSortBy("cpu");
if (root.toggleProcessList) {
@@ -131,15 +127,53 @@ Rectangle {
}
}
Column {
id: gpuTempColumn
visible: root.isVertical
anchors.centerIn: parent
spacing: 1
DankIcon {
name: "auto_awesome_mosaic"
size: Theme.barIconSize(barThickness)
color: {
if (root.displayTemp > 80) {
return Theme.tempDanger;
}
if (root.displayTemp > 65) {
return Theme.tempWarning;
}
return Theme.surfaceText;
}
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: {
if (root.displayTemp === undefined || root.displayTemp === null || root.displayTemp === 0) {
return "--";
}
return Math.round(root.displayTemp).toString();
}
font.pixelSize: Theme.barTextSize(barThickness)
font.weight: Font.Medium
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
}
Row {
id: gpuTempContent
visible: !root.isVertical
anchors.centerIn: parent
spacing: 3
DankIcon {
name: "auto_awesome_mosaic"
size: Theme.iconSize - 8
size: Theme.barIconSize(barThickness)
color: {
if (root.displayTemp > 80) {
return Theme.tempDanger;
@@ -162,7 +196,7 @@ Rectangle {
return Math.round(root.displayTemp) + "°";
}
font.pixelSize: Theme.fontSizeSmall
font.pixelSize: Theme.barTextSize(barThickness)
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
@@ -171,12 +205,12 @@ Rectangle {
StyledTextMetrics {
id: gpuTempBaseline
font.pixelSize: Theme.fontSizeSmall
font.pixelSize: Theme.barTextSize(barThickness)
font.weight: Font.Medium
text: "100°"
}
width: Math.max(gpuTempBaseline.width, paintedWidth)
width: root.minimumWidth ? Math.max(gpuTempBaseline.width, paintedWidth) : paintedWidth
Behavior on width {
NumberAnimation {

View File

@@ -8,17 +8,19 @@ import qs.Widgets
Rectangle {
id: root
property bool isVertical: axis?.isVertical ?? false
property var axis: null
property string section: "right"
property var popupTarget: null
property var parentScreen: null
property real widgetHeight: 30
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
property real widgetThickness: 30
readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetThickness / 30))
width: idleIcon.width + horizontalPadding * 2
height: widgetHeight
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
width: isVertical ? widgetThickness : (idleIcon.width + horizontalPadding * 2)
height: isVertical ? (idleIcon.height + horizontalPadding * 2) : widgetThickness
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
color: {
if (SettingsData.topBarNoBackground) {
if (SettingsData.dankBarNoBackground) {
return "transparent";
}
@@ -31,7 +33,7 @@ Rectangle {
anchors.centerIn: parent
name: SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle"
size: Theme.iconSize - 6
size: Theme.barIconSize(barThickness, -4)
color: Theme.surfaceText
}

View File

@@ -10,15 +10,19 @@ import qs.Widgets
Rectangle {
id: root
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
property bool isVertical: axis?.isVertical ?? false
property var axis: null
property real widgetThickness: 30
property real barThickness: 48
readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetThickness / 30))
property string currentLayout: ""
property string hyprlandKeyboard: ""
width: contentRow.implicitWidth + horizontalPadding * 2
height: widgetHeight
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
width: isVertical ? widgetThickness : (contentRow.implicitWidth + horizontalPadding * 2)
height: isVertical ? (contentColumn.implicitHeight + horizontalPadding * 2) : widgetThickness
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
color: {
if (SettingsData.topBarNoBackground) {
if (SettingsData.dankBarNoBackground) {
return "transparent";
}
@@ -47,15 +51,46 @@ Rectangle {
}
}
Column {
id: contentColumn
anchors.centerIn: parent
spacing: 1
visible: root.isVertical
DankIcon {
name: "keyboard"
size: Theme.barIconSize(barThickness)
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: {
if (!currentLayout) return ""
const parts = currentLayout.split(" ")
if (parts.length > 0) {
return parts[0].substring(0, 2).toUpperCase()
}
return currentLayout.substring(0, 2).toUpperCase()
}
font.pixelSize: Theme.barTextSize(barThickness)
font.weight: Font.Medium
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
}
Row {
id: contentRow
anchors.centerIn: parent
spacing: Theme.spacingS
visible: !root.isVertical
StyledText {
text: currentLayout
font.pixelSize: Theme.fontSizeSmall
font.pixelSize: Theme.barTextSize(barThickness)
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}

View File

@@ -0,0 +1,118 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property bool isActive: false
property bool isVertical: axis?.isVertical ?? false
property var axis: null
property string section: "left"
property var popupTarget: null
property var parentScreen: null
property real widgetThickness: 30
property real barThickness: 48
readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetThickness / 30))
signal clicked()
width: widgetThickness
height: widgetThickness
MouseArea {
id: launcherArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton
onPressed: {
root.clicked();
if (popupTarget && popupTarget.setTriggerPosition) {
const globalPos = mapToGlobal(0, 0);
const currentScreen = parentScreen || Screen;
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, width);
popupTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen);
}
}
}
Rectangle {
id: launcherContent
anchors.fill: parent
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
color: {
if (SettingsData.dankBarNoBackground) {
return "transparent";
}
const baseColor = launcherArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor;
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
}
DankIcon {
visible: SettingsData.launcherLogoMode === "apps"
anchors.centerIn: parent
name: "apps"
size: Theme.barIconSize(barThickness, -4)
color: Theme.surfaceText
}
SystemLogo {
visible: SettingsData.launcherLogoMode === "os"
anchors.centerIn: parent
width: Theme.barIconSize(barThickness, SettingsData.launcherLogoSizeOffset)
height: Theme.barIconSize(barThickness, SettingsData.launcherLogoSizeOffset)
colorOverride: Theme.effectiveLogoColor
brightnessOverride: SettingsData.launcherLogoBrightness
contrastOverride: SettingsData.launcherLogoContrast
}
IconImage {
visible: SettingsData.launcherLogoMode === "compositor"
anchors.centerIn: parent
width: Theme.barIconSize(barThickness, SettingsData.launcherLogoSizeOffset)
height: Theme.barIconSize(barThickness, SettingsData.launcherLogoSizeOffset)
smooth: true
asynchronous: true
source: {
if (CompositorService.isNiri) {
return "file://" + Theme.shellDir + "/assets/niri.svg"
} else if (CompositorService.isHyprland) {
return "file://" + Theme.shellDir + "/assets/hyprland.svg"
}
return ""
}
layer.enabled: Theme.effectiveLogoColor !== ""
layer.effect: MultiEffect {
colorization: 1
colorizationColor: Theme.effectiveLogoColor
brightness: SettingsData.launcherLogoBrightness
contrast: SettingsData.launcherLogoContrast
}
}
IconImage {
visible: SettingsData.launcherLogoMode === "custom" && SettingsData.launcherLogoCustomPath !== ""
anchors.centerIn: parent
width: Theme.barIconSize(barThickness, SettingsData.launcherLogoSizeOffset)
height: Theme.barIconSize(barThickness, SettingsData.launcherLogoSizeOffset)
smooth: true
asynchronous: true
source: SettingsData.launcherLogoCustomPath ? "file://" + SettingsData.launcherLogoCustomPath.replace("file://", "") : ""
layer.enabled: Theme.effectiveLogoColor !== ""
layer.effect: MultiEffect {
colorization: 1
colorizationColor: Theme.effectiveLogoColor
brightness: SettingsData.launcherLogoBrightness
contrast: SettingsData.launcherLogoContrast
}
}
}
}

View File

@@ -7,6 +7,8 @@ import qs.Widgets
Rectangle {
id: root
property bool isVertical: axis?.isVertical ?? false
property var axis: null
readonly property MprisPlayer activePlayer: MprisController.activePlayer
readonly property bool playerAvailable: activePlayer !== null
property bool compactMode: false
@@ -21,27 +23,36 @@ Rectangle {
}
}
readonly property int currentContentWidth: {
// Calculate actual content width:
// AudioViz (20) + spacing + [text + spacing] + controls (prev:20 + spacing + play:24 + spacing + next:20) + padding
if (isVertical) {
return widgetThickness;
}
const controlsWidth = 20 + Theme.spacingXS + 24 + Theme.spacingXS + 20;
// ~72px total
const audioVizWidth = 20;
const contentWidth = audioVizWidth + Theme.spacingXS + controlsWidth;
return contentWidth + (textWidth > 0 ? textWidth + Theme.spacingXS : 0) + horizontalPadding * 2;
}
readonly property int currentContentHeight: {
if (!isVertical) {
return widgetThickness;
}
const audioVizHeight = 20;
const playButtonHeight = 24;
return audioVizHeight + Theme.spacingXS + playButtonHeight + horizontalPadding * 2;
}
property string section: "center"
property var popupTarget: null
property var parentScreen: null
property real barHeight: 48
property real widgetHeight: 30
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
property real barThickness: 48
property real widgetThickness: 30
readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetThickness / 30))
signal clicked()
height: widgetHeight
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
width: currentContentWidth
height: currentContentHeight
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
color: {
if (SettingsData.topBarNoBackground) {
if (SettingsData.dankBarNoBackground) {
return "transparent";
}
@@ -57,6 +68,7 @@ Rectangle {
target: root
opacity: 1
width: currentContentWidth
height: currentContentHeight
}
},
@@ -67,7 +79,8 @@ Rectangle {
PropertyChanges {
target: root
opacity: 0
width: 0
width: isVertical ? widgetThickness : 0
height: isVertical ? 0 : widgetThickness
}
}
@@ -83,7 +96,7 @@ Rectangle {
}
NumberAnimation {
properties: "opacity,width"
properties: isVertical ? "opacity,height" : "opacity,width"
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
@@ -96,7 +109,7 @@ Rectangle {
to: "shown"
NumberAnimation {
properties: "opacity,width"
properties: isVertical ? "opacity,height" : "opacity,width"
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
@@ -104,9 +117,107 @@ Rectangle {
}
]
Column {
id: verticalLayout
visible: root.isVertical
anchors.centerIn: parent
spacing: Theme.spacingXS
AudioVisualization {
anchors.horizontalCenter: parent.horizontalCenter
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.popupTarget && root.popupTarget.setTriggerPosition) {
const globalPos = parent.mapToGlobal(0, 0)
const currentScreen = root.parentScreen || Screen
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, root.barThickness, parent.width)
root.popupTarget.setTriggerPosition(pos.x, pos.y, pos.width, root.section, currentScreen)
}
root.clicked()
}
onEntered: {
tooltipLoader.active = true
if (tooltipLoader.item && activePlayer) {
const globalPos = parent.mapToGlobal(parent.width / 2, parent.height / 2)
const screenX = root.parentScreen ? root.parentScreen.x : 0
const screenY = root.parentScreen ? root.parentScreen.y : 0
const relativeY = globalPos.y - screenY
const tooltipX = root.axis?.edge === "left" ? (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS) : (root.parentScreen.width - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS)
let identity = activePlayer.identity || ""
let isWebMedia = identity.toLowerCase().includes("firefox") || identity.toLowerCase().includes("chrome") || identity.toLowerCase().includes("chromium")
let title = activePlayer.trackTitle || "Unknown Track"
let subtitle = ""
if (isWebMedia && activePlayer.trackTitle) {
subtitle = activePlayer.trackArtist || identity
} else {
subtitle = activePlayer.trackArtist || ""
}
let tooltipText = subtitle.length > 0 ? title + " • " + subtitle : title
const isLeft = root.axis?.edge === "left"
tooltipLoader.item.show(tooltipText, screenX + tooltipX, relativeY, root.parentScreen, isLeft, !isLeft)
}
}
onExited: {
if (tooltipLoader.item) {
tooltipLoader.item.hide()
}
tooltipLoader.active = false
}
}
}
Rectangle {
width: 24
height: 24
radius: 12
anchors.horizontalCenter: parent.horizontalCenter
color: activePlayer && activePlayer.playbackState === 1 ? Theme.primary : Theme.primaryHover
visible: root.playerAvailable
opacity: activePlayer ? 1 : 0.3
DankIcon {
anchors.centerIn: parent
name: activePlayer && activePlayer.playbackState === 1 ? "pause" : "play_arrow"
size: 14
color: activePlayer && activePlayer.playbackState === 1 ? Theme.background : Theme.primary
}
MouseArea {
anchors.fill: parent
enabled: root.playerAvailable
hoverEnabled: enabled
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
onClicked: (mouse) => {
if (!activePlayer) return
if (mouse.button === Qt.LeftButton) {
activePlayer.togglePlaying()
} else if (mouse.button === Qt.MiddleButton) {
activePlayer.previous()
} else if (mouse.button === Qt.RightButton) {
activePlayer.next()
}
}
}
}
}
Loader {
id: tooltipLoader
active: false
sourceComponent: DankTooltip {}
}
Row {
id: mediaRow
visible: !root.isVertical
anchors.centerIn: parent
spacing: Theme.spacingXS
@@ -156,7 +267,7 @@ Rectangle {
anchors.verticalCenter: parent.verticalCenter
text: textContainer.displayText
font.pixelSize: Theme.fontSizeSmall
font.pixelSize: Theme.barTextSize(barThickness)
color: Theme.surfaceText
font.weight: Font.Medium
wrapMode: Text.NoWrap
@@ -208,13 +319,12 @@ Rectangle {
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onPressed: {
if (root.popupTarget && root.popupTarget.setTriggerPosition) {
const globalPos = mapToGlobal(0, 0);
const currentScreen = root.parentScreen || Screen;
const screenX = currentScreen.x || 0;
const relativeX = globalPos.x - screenX;
root.popupTarget.setTriggerPosition(relativeX, barHeight + Theme.spacingXS, root.width, root.section, currentScreen);
const globalPos = mapToGlobal(0, 0)
const currentScreen = root.parentScreen || Screen
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, root.width)
root.popupTarget.setTriggerPosition(pos.x, pos.y, pos.width, root.section, currentScreen)
}
root.clicked();
root.clicked()
}
}
@@ -330,7 +440,13 @@ Rectangle {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}

View File

@@ -8,10 +8,14 @@ import qs.Widgets
Rectangle {
id: root
property bool isVertical: axis?.isVertical ?? false
property var axis: null
property int availableWidth: 400
readonly property int baseWidth: contentRow.implicitWidth + Theme.spacingS * 2
readonly property int maxNormalWidth: 456
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
property real widgetThickness: 30
property real barThickness: 48
readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetThickness / 30))
function formatNetworkSpeed(bytesPerSec) {
if (bytesPerSec < 1024) {
@@ -25,11 +29,11 @@ Rectangle {
}
}
width: contentRow.implicitWidth + horizontalPadding * 2
height: widgetHeight
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
width: isVertical ? widgetThickness : (contentRow.implicitWidth + horizontalPadding * 2)
height: isVertical ? (contentColumn.implicitHeight + horizontalPadding * 2) : widgetThickness
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
color: {
if (SettingsData.topBarNoBackground) {
if (SettingsData.dankBarNoBackground) {
return "transparent";
}
@@ -51,15 +55,57 @@ Rectangle {
cursorShape: Qt.PointingHandCursor
}
Column {
id: contentColumn
anchors.centerIn: parent
spacing: 2
visible: root.isVertical
DankIcon {
name: "network_check"
size: Theme.barIconSize(barThickness)
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: {
const rate = DgopService.networkRxRate
if (rate < 1024) return rate.toFixed(0)
if (rate < 1024 * 1024) return (rate / 1024).toFixed(0) + "K"
return (rate / (1024 * 1024)).toFixed(0) + "M"
}
font.pixelSize: Theme.barTextSize(barThickness)
font.weight: Font.Medium
color: Theme.info
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: {
const rate = DgopService.networkTxRate
if (rate < 1024) return rate.toFixed(0)
if (rate < 1024 * 1024) return (rate / 1024).toFixed(0) + "K"
return (rate / (1024 * 1024)).toFixed(0) + "M"
}
font.pixelSize: Theme.barTextSize(barThickness)
font.weight: Font.Medium
color: Theme.error
anchors.horizontalCenter: parent.horizontalCenter
}
}
Row {
id: contentRow
anchors.centerIn: parent
spacing: Theme.spacingS
visible: !root.isVertical
DankIcon {
name: "network_check"
size: Theme.iconSize - 8
size: Theme.barIconSize(barThickness)
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
@@ -70,13 +116,13 @@ Rectangle {
StyledText {
text: "↓"
font.pixelSize: Theme.fontSizeSmall
font.pixelSize: Theme.barTextSize(barThickness)
color: Theme.info
}
StyledText {
text: DgopService.networkRxRate > 0 ? formatNetworkSpeed(DgopService.networkRxRate) : "0 B/s"
font.pixelSize: Theme.fontSizeSmall
font.pixelSize: Theme.barTextSize(barThickness)
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
@@ -86,7 +132,7 @@ Rectangle {
StyledTextMetrics {
id: rxBaseline
font.pixelSize: Theme.fontSizeSmall
font.pixelSize: Theme.barTextSize(barThickness)
font.weight: Font.Medium
text: "88.8 MB/s"
}
@@ -109,13 +155,13 @@ Rectangle {
StyledText {
text: "↑"
font.pixelSize: Theme.fontSizeSmall
font.pixelSize: Theme.barTextSize(barThickness)
color: Theme.error
}
StyledText {
text: DgopService.networkTxRate > 0 ? formatNetworkSpeed(DgopService.networkTxRate) : "0 B/s"
font.pixelSize: Theme.fontSizeSmall
font.pixelSize: Theme.barTextSize(barThickness)
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
@@ -125,7 +171,7 @@ Rectangle {
StyledTextMetrics {
id: txBaseline
font.pixelSize: Theme.fontSizeSmall
font.pixelSize: Theme.barTextSize(barThickness)
font.weight: Font.Medium
text: "88.8 MB/s"
}

View File

@@ -0,0 +1,96 @@
import QtQuick
import Quickshell.Hyprland
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
property bool isVertical: axis?.isVertical ?? false
property var axis: null
property string section: "right"
property var parentScreen: null
property real widgetThickness: 30
property real barThickness: 48
readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetThickness / 30))
signal clicked()
readonly property string focusedScreenName: (
CompositorService.isHyprland && typeof Hyprland !== "undefined" && Hyprland.focusedWorkspace && Hyprland.focusedWorkspace.monitor ? (Hyprland.focusedWorkspace.monitor.name || "") :
CompositorService.isNiri && typeof NiriService !== "undefined" && NiriService.currentOutput ? NiriService.currentOutput : ""
)
function resolveNotepadInstance() {
if (typeof notepadSlideoutVariants === "undefined" || !notepadSlideoutVariants || !notepadSlideoutVariants.instances) {
return null
}
const targetScreen = focusedScreenName
if (targetScreen) {
for (var i = 0; i < notepadSlideoutVariants.instances.length; i++) {
var slideout = notepadSlideoutVariants.instances[i]
if (slideout.modelData && slideout.modelData.name === targetScreen) {
return slideout
}
}
}
return notepadSlideoutVariants.instances.length > 0 ? notepadSlideoutVariants.instances[0] : null
}
readonly property var notepadInstance: resolveNotepadInstance()
readonly property bool isActive: notepadInstance?.isVisible ?? false
width: isVertical ? widgetThickness : (notepadIcon.width + horizontalPadding * 2)
height: isVertical ? (notepadIcon.height + horizontalPadding * 2) : widgetThickness
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
color: {
if (SettingsData.dankBarNoBackground) {
return "transparent";
}
const baseColor = notepadArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor;
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
}
DankIcon {
id: notepadIcon
anchors.centerIn: parent
name: "assignment"
size: Theme.barIconSize(barThickness, -4)
color: notepadArea.containsMouse || root.isActive ? Theme.primary : Theme.surfaceText
}
Rectangle {
width: 6
height: 6
radius: 3
color: Theme.primary
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: SettingsData.dankBarNoBackground ? 0 : 4
anchors.topMargin: SettingsData.dankBarNoBackground ? 0 : 4
visible: NotepadStorageService.tabs && NotepadStorageService.tabs.length > 0
opacity: 0.8
}
MouseArea {
id: notepadArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: {
const inst = root.notepadInstance
if (inst) {
inst.toggle()
}
root.clicked()
}
}
}

View File

@@ -0,0 +1,76 @@
import QtQuick
import qs.Common
import qs.Widgets
Item {
id: root
property bool hasUnread: false
property bool isActive: false
property bool isVertical: axis?.isVertical ?? false
property var axis: null
property string section: "right"
property var popupTarget: null
property var parentScreen: null
property real widgetThickness: 30
property real barThickness: 48
readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetThickness / 30))
signal clicked()
width: widgetThickness
height: widgetThickness
MouseArea {
id: notificationArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton
onPressed: {
if (popupTarget && popupTarget.setTriggerPosition) {
const globalPos = mapToGlobal(0, 0)
const currentScreen = parentScreen || Screen
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, width)
popupTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen)
}
root.clicked()
}
}
Rectangle {
id: notificationContent
anchors.fill: parent
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
color: {
if (SettingsData.dankBarNoBackground) {
return "transparent"
}
const baseColor = notificationArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency)
}
DankIcon {
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
name: SessionData.doNotDisturb ? "notifications_off" : "notifications"
size: Theme.barIconSize(barThickness, -4)
color: SessionData.doNotDisturb ? Theme.error : (notificationArea.containsMouse || root.isActive ? Theme.primary : Theme.surfaceText)
}
Rectangle {
width: 8
height: 8
radius: 4
color: Theme.error
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: SettingsData.dankBarNoBackground ? 0 : 6
anchors.topMargin: SettingsData.dankBarNoBackground ? 0 : 6
visible: root.hasUnread
}
}
}

View File

@@ -7,23 +7,27 @@ import qs.Widgets
Rectangle {
id: root
property bool isVertical: axis?.isVertical ?? false
property var axis: null
property string section: "right"
property var popupTarget: null
property var parentScreen: null
property real widgetHeight: 30
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 2 : Theme.spacingS
property real widgetThickness: 30
property real barThickness: 48
readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 2 : Theme.spacingS
readonly property bool hasActivePrivacy: PrivacyService.anyPrivacyActive
readonly property int activeCount: PrivacyService.microphoneActive + PrivacyService.cameraActive + PrivacyService.screensharingActive
readonly property real contentWidth: hasActivePrivacy ? (activeCount * 18 + (activeCount - 1) * Theme.spacingXS) : 0
readonly property real contentHeight: hasActivePrivacy ? (activeCount * 18 + (activeCount - 1) * Theme.spacingXS) : 0
width: hasActivePrivacy ? (contentWidth + horizontalPadding * 2) : 0
height: hasActivePrivacy ? widgetHeight : 0
radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius
width: isVertical ? widgetThickness : (hasActivePrivacy ? (contentWidth + horizontalPadding * 2) : 0)
height: isVertical ? (hasActivePrivacy ? (contentHeight + horizontalPadding * 2) : 0) : (hasActivePrivacy ? widgetThickness : 0)
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
visible: hasActivePrivacy
opacity: hasActivePrivacy ? 1 : 0
enabled: hasActivePrivacy
color: {
if (SettingsData.topBarNoBackground) {
if (SettingsData.dankBarNoBackground) {
return "transparent";
}
@@ -43,10 +47,76 @@ Rectangle {
}
}
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
visible: root.isVertical && hasActivePrivacy
Item {
width: 18
height: 18
visible: PrivacyService.microphoneActive
anchors.horizontalCenter: parent.horizontalCenter
DankIcon {
name: "mic"
size: Theme.iconSizeSmall
color: Theme.error
filled: true
anchors.centerIn: parent
}
}
Item {
width: 18
height: 18
visible: PrivacyService.cameraActive
anchors.horizontalCenter: parent.horizontalCenter
DankIcon {
name: "camera_video"
size: Theme.iconSizeSmall
color: Theme.surfaceText
filled: true
anchors.centerIn: parent
}
Rectangle {
width: 6
height: 6
radius: 3
color: Theme.error
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: -2
anchors.topMargin: -1
}
}
Item {
width: 18
height: 18
visible: PrivacyService.screensharingActive
anchors.horizontalCenter: parent.horizontalCenter
DankIcon {
name: "screen_share"
size: Theme.iconSizeSmall
color: Theme.warning
filled: true
anchors.centerIn: parent
}
}
}
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
visible: hasActivePrivacy
visible: !root.isVertical && hasActivePrivacy
Item {
width: 18
@@ -129,7 +199,7 @@ Rectangle {
anchors.centerIn: parent
text: PrivacyService.getPrivacySummary()
font.pixelSize: Theme.fontSizeSmall
font.pixelSize: Theme.barTextSize(barThickness)
color: Theme.surfaceText
}
@@ -158,7 +228,17 @@ Rectangle {
}
Behavior on width {
enabled: hasActivePrivacy && visible
enabled: hasActivePrivacy && visible && !isVertical
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on height {
enabled: hasActivePrivacy && visible && isVertical
NumberAnimation {
duration: Theme.mediumDuration

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