1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-08-02 19:48:27 -04:00

Compare commits

..

294 Commits

Author SHA1 Message Date
bbedward 63243e3ae5 readme updoot 2025-08-04 21:33:55 -04:00
bbedward 739f3eff01 small fix 2025-08-04 19:13:10 -04:00
bbedward 436c7e2234 dock: create an initial basic dock 2025-08-04 19:10:20 -04:00
bbedward 9dbb6b094f small fix to calendar sizing 2025-08-02 22:17:10 -04:00
bbedward 7e5e3a3914 ghostty in readme 2025-08-02 16:45:10 -04:00
bbedward 599118c63e General code cleanups 2025-08-02 16:18:12 -04:00
bbedward 2cf7497324 fix process monitor CPU calculations 2025-08-02 14:55:33 -04:00
bbedward 21c40b58bc widgets: add spacer, divider, tweak interface 2025-08-02 13:10:39 -04:00
bbedward 2e85494236 always center topbar widgets 2025-08-02 12:19:33 -04:00
bbedward 723a9b1824 Merge branch 'master' of github.com:bbedward/dank-material-dark-shell 2025-08-02 12:13:31 -04:00
bbedward be301f829f make cava a singleton 2025-08-02 12:13:05 -04:00
purian23 8fb8281988 Logic updates to re-ordering topbar widgets 2025-08-01 23:14:19 -04:00
purian23 12c62720b3 Show default font to reflect users choice 2025-08-01 23:02:13 -04:00
bbedward 2f97c39647 lookup notif name by desktop entry if not present 2025-08-01 19:51:03 -04:00
bbedward 379f4f9933 add settings IPC 2025-08-01 13:57:49 -04:00
purian23 c794944f01 fix: Restore SystemTray 2025-08-01 13:28:04 -04:00
purian23 6293b0e502 Merge remote-tracking branch 'origin/master' 2025-08-01 12:32:05 -04:00
purian23 f66b5181ce feat: Implement drag & drop topbar widget sections in settings 2025-08-01 12:29:21 -04:00
bbedward 18ee37e18e theme revisions 2025-08-01 09:29:31 -04:00
bbedward be6ef5fc77 fix qt auto-theme 2025-08-01 08:06:21 -04:00
bbedward ce9f4efb5d doc tweaks 2025-07-31 22:52:46 -04:00
bbedward 9f9352b84a colloid note 2025-07-31 22:29:17 -04:00
bbedward b1b2177f51 theme: update import in gtk.css 2025-07-31 22:26:12 -04:00
purian23 b26d83d301 Fedora compatibility & Icon theme updates 2025-07-30 19:10:46 -04:00
purian23 7b14094155 Hide system tray widget when empty 2025-07-30 18:25:04 -04:00
purian23 d8ab38a0af Restore Wifi Services UI 2025-07-29 22:20:06 -04:00
purian23 35c6efdcc5 Optimize services to reduce load 2025-07-29 21:56:41 -04:00
purian23 df147ff9b9 Update Readme w/Fedora guides & improvements 2025-07-29 11:36:36 -04:00
purian23 d656502342 Update auto theme logic 2025-07-28 21:35:52 -04:00
purian23 102b248cad feat: Integrate full shell dynamic theming system 2025-07-28 18:29:16 -04:00
bbedward 99e890cc17 ipc for light/dark mode 2025-07-28 15:31:27 -04:00
bbedward 199c1c3043 Dont double append icons 2025-07-28 14:17:08 -04:00
bbedward bde33b334a bad import removal 2025-07-28 13:31:47 -04:00
bbedward ad6ab67906 remove some outdated stuff 2025-07-28 13:30:13 -04:00
bbedward fd6cfcc2e6 Remove outdated check 2025-07-28 13:27:27 -04:00
bbedward b1db088828 update workspace indicators and qmlformat 2025-07-28 13:16:27 -04:00
bbedward 4581544585 weather: general improvements to setting 2025-07-28 12:57:59 -04:00
bbedward 3ca7e3a888 Remove PlainText from StyledText 2025-07-28 11:51:29 -04:00
bbedward 2ff9c54abb elide system text 2025-07-28 11:50:04 -04:00
bbedward e17bd982fe make icon theme searchable 2025-07-28 11:47:55 -04:00
bbedward d4e5e60e7b Adjust fonts 2025-07-28 11:41:48 -04:00
bbedward 73e1f0b6c0 font tweaks 2025-07-28 11:38:49 -04:00
bbedward 03b8370c14 font re-work, change default to inter variable + fira code 2025-07-28 11:24:24 -04:00
bbedward f16e7a506e more mono fonts 2025-07-27 23:37:29 -04:00
bbedward d9e6302ef8 Merge branch 'master' of github.com:bbedward/dank-material-dark-shell 2025-07-27 23:26:31 -04:00
bbedward b119fd511e Implement font family changes 2025-07-27 23:25:12 -04:00
purian23 2e84e5f74b Update notification dismiss definitions 2025-07-27 23:05:38 -04:00
bbedward 420891e047 Fix icon theme change 2025-07-27 22:52:16 -04:00
bbedward 77051d0d62 de-dupe scrollview 2025-07-27 20:56:09 -04:00
bbedward fae7f36a24 remove unused functions/dead code 2025-07-27 20:23:11 -04:00
bbedward da70434eba notifications: fix stuck externally dismissed notifs 2025-07-27 18:37:56 -04:00
bbedward 84036c57b6 fix lock screen multi-monitor input 2025-07-27 18:26:33 -04:00
purian23 0290446c0c Bluetooth UI tweak 2025-07-27 14:29:44 -04:00
purian23 384460712c Feat: add clipboard history image preview 2025-07-27 14:21:45 -04:00
purian23 2c8854f886 Add DnD tooltip 2025-07-27 00:40:02 -04:00
purian23 cb676d02b8 Update logic to conditionally hide focusedApp & systemTray if no data is available 2025-07-27 00:12:54 -04:00
purian23 43f7ae89cc Feature: Add topbar widget transparency appearance settings 2025-07-27 00:02:47 -04:00
purian23 ec69c64b1a Update notification content spacing and group color. 2025-07-26 22:29:13 -04:00
purian23 a681593eda Address active warnings 2025-07-26 22:13:09 -04:00
bbedward 0affd73f32 remove debug timer 2025-07-26 17:03:29 -04:00
bbedward e160a41347 fix app searching sorting with usage rankings 2025-07-26 16:50:57 -04:00
bbedward d2d8c4d74d cleanup markdown better, don't process code block content 2025-07-26 16:37:45 -04:00
bbedward 1472f00f4b use variants for multi display volume popup/toast/notification 2025-07-26 16:24:35 -04:00
bbedward 01a94a17de notifications: try to prevent zombies better, markdown, re-org 2025-07-26 16:08:58 -04:00
bbedward 484a947127 notification: general improvements to popups 2025-07-26 13:25:39 -04:00
bbedward ec1bf7c110 notifications: improve general layout and visual b ugs 2025-07-26 11:41:25 -04:00
bbedward 7b8efd9114 revise notification position and animations 2025-07-26 10:08:56 -04:00
purian23 308d84bfa1 fix: notification warnings & updates 2025-07-26 01:27:40 -04:00
bbedward 39e3107db6 notifications: reverse order top to bottom, fix warnings 2025-07-26 00:27:59 -04:00
bbedward 3b5ddab1ce notifications: improve resource usage 2025-07-25 23:06:07 -04:00
bbedward ed7370361e notifications: initial refactory of popups 2025-07-25 22:39:01 -04:00
bbedward d28df49f18 standardize some opacity colors 2025-07-25 18:19:22 -04:00
bbedward 1157b0336f revise listview/gridview scrolling 2025-07-25 17:46:03 -04:00
bbedward f26c01916b execDetached for process menu operations 2025-07-25 17:21:51 -04:00
bbedward e14afcefd4 notifications: improve notification center 2025-07-25 16:52:58 -04:00
bbedward 98ddc2805b fix readme 2025-07-25 11:45:35 -04:00
bbedward 3825486829 fix reactivity of app launcher 2025-07-25 11:28:11 -04:00
bbedward 786b8f3fa1 audio: create and connect volume popup to IPCs 2025-07-25 11:24:27 -04:00
purian23 1cf252599b Update to use existing DankSlider codebase 2025-07-24 23:13:23 -04:00
purian23 8e7bcabcf7 Add percentage to the volume control slider 2025-07-24 22:46:11 -04:00
bbedward 1197b9385c styledrect in widgets 2025-07-24 21:07:46 -04:00
bbedward 14a9828296 notifications: modularize 2025-07-24 20:58:57 -04:00
bbedward 0068774a8b subtle redesigns 2025-07-24 20:32:28 -04:00
bbedward aabb0ffcc5 filebrowser: bug fixes 2025-07-24 19:55:41 -04:00
bbedward fc52aa2c7a uncomment toast, and format 2025-07-24 19:46:05 -04:00
bbedward 762785b27a calendar: fix events not loading for right month 2025-07-24 19:19:26 -04:00
bbedward 4086e914a7 stylistic updates 2025-07-24 18:44:19 -04:00
bbedward 366930fc03 launcher: sort by usage frequency 2025-07-24 18:36:23 -04:00
bbedward 99f17de065 Merge branch 'master' of github.com:bbedward/dank-material-dark-shell 2025-07-24 16:29:44 -04:00
bbedward 0208f1e840 dankslider: tooltip with value 2025-07-24 16:29:31 -04:00
purian23 0db026f775 Recalculate compact modes 2025-07-24 16:05:30 -04:00
bbedward 0e968d910d settings: redesign 2025-07-24 16:02:53 -04:00
bbedward c6df2bc11d smarter distance calculation 2025-07-24 15:09:28 -04:00
bbedward 8bcac36be3 lockscreen: implement 2025-07-24 14:56:58 -04:00
purian23 71be8bdeec Update focused app & media control screen response 2025-07-24 14:01:22 -04:00
bbedward e3e3788a37 meta: many resource usage improvements and consolidations 2025-07-24 12:44:11 -04:00
bbedward ee2cbd708d meta: integrate wallpaper, FileBrowser, StateLayer
- A lot of this is implements patterns implemented by soramannew's
  caelestia-shell
2025-07-23 23:20:11 -04:00
purian23 a0735db7a4 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-07-23 18:48:39 -04:00
purian23 8c11e2f0cf Half Dank Status Achieved 2025-07-23 18:47:35 -04:00
bbedward 71f8b8ce9a fix context menus and general stuff 2025-07-23 18:30:43 -04:00
purian23 bb2a410039 Fix 2nd tier expansion 2025-07-23 17:40:00 -04:00
bbedward 4f63d5899b refactor: perf improvement stopping singletons with ref
Also switch to scale+opacity anims with custom curve
2025-07-23 16:54:19 -04:00
bbedward 2c57487046 appdrawer: remove background effect 2025-07-23 15:37:53 -04:00
bbedward 94b10159a9 refactor: app drawer de-dupe 2025-07-23 15:36:29 -04:00
bbedward c01da89311 mecha refactoring 2025-07-23 15:15:51 -04:00
bbedward 19adcf3578 refactor: mega refactoring of a bunch of things 2025-07-23 11:56:18 -04:00
bbedward 14eef59c9f refactors: more DankModals, remove GlobalDropdown and InputDialog 2025-07-23 11:11:56 -04:00
bbedward 56a857939b Merge branch 'master' of github.com:bbedward/dank-material-dark-shell 2025-07-23 10:04:21 -04:00
bbedward 70f8a127c6 processlist: breakout and de-dupe code, change styles
Also reverts bluetooth pairing modals since quickshell did not approve
of the PR
2025-07-23 10:03:29 -04:00
purian23 5b40396a8a Level up notifications 2025-07-23 00:04:16 -04:00
bbedward e94d2af9ae fix auto focusing text fields 2025-07-22 23:51:18 -04:00
bbedward 9c8c25711c sizing changes 2025-07-22 23:35:48 -04:00
bbedward 54a7281fa3 fix sizing of dialogs, re-add ipcs 2025-07-22 23:00:09 -04:00
bbedward 7232d66b5d layer effect instead 2025-07-22 21:20:55 -04:00
bbedward 2f50821db4 Focus text inputs by default 2025-07-22 21:16:00 -04:00
bbedward f1833a81a0 Add DankModal and SystemLogo widgets 2025-07-22 21:12:19 -04:00
bbedward 5cf04aa941 settings: complete re-organize and breakout 2025-07-22 19:51:16 -04:00
bbedward 3157622c8b bluetooth: pairing dialog
- Allows connecting pin/passkey/confirmation dialogs
- Fixes inability to connect to many devices
- Dependent on un-merged quickshell PR: https://github.com/quickshell-mirror/quickshell/pull/138
2025-07-22 19:20:10 -04:00
bbedward 02bd9bbc72 networking: improve wifi experience and bugs 2025-07-22 11:50:07 -04:00
bbedward a4da6921bd settings: restore location search for weather 2025-07-21 22:35:16 -04:00
bbedward 9730e4edae refactor: common DankTextField widget 2025-07-21 22:10:46 -04:00
bbedward 303dc70c28 clipboard: fix focus warning on exit 2025-07-21 21:37:19 -04:00
bbedward de8235d94f refactor: create DankActionButton and implement 2025-07-21 21:06:03 -04:00
bbedward dbf2b58b7f fix focus window lookups 2025-07-21 20:56:33 -04:00
bbedward 42183b308e Fix focused window 2025-07-21 20:52:58 -04:00
bbedward 4238bb4638 refactor: replace SettingsToggle with DankToggle 2025-07-21 20:46:57 -04:00
bbedward 1854395a0c Re-add padding as option 2025-07-21 20:43:23 -04:00
bbedward c47ecf8fb0 focusedwindow: fix various issues with workspaces 2025-07-21 17:36:15 -04:00
bbedward 135cf8f027 workspaces: remove arbitrary padding and minimum width 2025-07-21 17:25:00 -04:00
bbedward 6c9e0fb74c network: wifi indicator fix 2025-07-21 16:12:33 -04:00
bbedward 67f1c64dd6 network: cleanup state handling 2025-07-21 16:08:50 -04:00
bbedward 85cf5060e2 no workspace index by default 2025-07-21 15:53:34 -04:00
bbedward 81c232eb20 Improve NetworkTab view when wifi and ethernet interfaces present 2025-07-21 15:52:10 -04:00
bbedward 56662608d1 Remove pointless connections 2025-07-21 14:02:18 -04:00
purian23 5189a34d40 Add 3 workspace indicators by default 2025-07-21 01:20:10 -04:00
purian23 92908b6e0e DND Hotfix 2025-07-21 00:50:33 -04:00
purian23 6f54e592ba Implement user icon theme selection in settings 2025-07-21 00:34:45 -04:00
purian23 79a600cf0d Enhanced Notification System & Settings 2025-07-20 19:07:48 -04:00
purian23 30983b50d5 Remove redundant network header verbiage 2025-07-19 22:49:11 -04:00
purian23 2454d707d9 Fix workspace swticher index input 2025-07-19 22:41:48 -04:00
Purian23 a28764e8f9 Merge pull request #5 from Amadoabad/indexedWorkspaces
Added number indexing option for the workspace switcher
2025-07-19 22:34:43 -04:00
purian23 0d153c407d Combine network tab interface 2025-07-19 20:52:01 -04:00
amadoabad fa3d02552a Added number indexing option for the workspace switcher 2025-07-20 00:03:39 +03:00
bbedward 4202b8323b Remove dead code 2025-07-19 11:45:08 -04:00
bbedward 063e9bb40f bunch of cleanups 2025-07-19 10:56:32 -04:00
purian23 5626335602 Iterative notification updates 2025-07-19 03:02:27 -04:00
purian23 033d8b034b Implement 2nd tier group expansion on notifications 2025-07-18 23:21:10 -04:00
purian23 1355a77fd0 Implement icon for niri screenshots
- Fixed debug icon warning logic
- Add linux os environment script
2025-07-18 21:13:50 -04:00
bbedward f34ecf786a Migrate DankIcon stragglers 2025-07-18 20:02:57 -04:00
bbedward 1d9477bea3 Fix launcher fallback 2025-07-18 19:50:27 -04:00
bbedward b3aa4719d9 Only material icons rounded 2025-07-18 18:58:01 -04:00
bbedward 8d87fca419 bluetooth: trust device before connecting
Fixes devices not staying in history
2025-07-18 18:32:29 -04:00
bbedward 3c7106735c missing dank icons 2025-07-18 12:38:50 -04:00
bbedward d8938e8d15 initial structure refactor 2025-07-18 12:37:37 -04:00
bbedward 3a3f18c298 meta cleanup and refactors 2025-07-18 11:40:17 -04:00
bbedward 06607fa25e Merge branch 'master' of github.com:bbedward/dank-material-dark-shell 2025-07-17 23:20:48 -04:00
bbedward 42927eeb97 audio: un-dumbify audio service 2025-07-17 23:20:36 -04:00
purian23 353fa1220e Notifiy fix 2025-07-17 23:09:46 -04:00
bbedward 930572e1cb bluetooth: don't disable discovery before connect 2025-07-17 23:00:21 -04:00
bbedward 1e6c801507 Fallback icon font 2025-07-17 22:55:14 -04:00
bbedward fbae03de14 Update readme and make nerd fonts optional 2025-07-17 22:15:15 -04:00
bbedward 66671f757b bluetooth: cleanup service 2025-07-17 20:35:40 -04:00
bbedward a66d6c2b55 bluetooth: swap to signalStrength 2025-07-17 20:11:33 -04:00
bbedward 70b9a3cff4 qmlformat-all script 2025-07-17 18:15:35 -04:00
bbedward 26ad3810b4 qmlformat file 2025-07-17 18:00:31 -04:00
bbedward 023b6bc0d1 Initial qmlformat 2025-07-17 17:58:56 -04:00
bbedward cbb42df3a9 bluetooth: cleanup service usage, add rssi 2025-07-17 16:56:28 -04:00
bbedward 60395b5646 Merge branch 'master' of github.com:bbedward/dank-material-dark-shell 2025-07-17 15:22:47 -04:00
bbedward 7a40156893 meta: large-scale refactor progress 2025-07-17 15:21:37 -04:00
purian23 8a57c6a6b1 Notifications updates 2025-07-17 14:51:54 -04:00
bbedward 77cc9c288b refactor: start trimming AI bullcrap patterns 2025-07-17 13:25:35 -04:00
bbedward cc9b575a1d upower: more sensible usage of upower 2025-07-17 10:57:42 -04:00
bbedward 944e0fe129 audio: some cleanups to audio service 2025-07-17 09:08:59 -04:00
bbedward 6484ee0806 audioservice: fix shell crash on resume from sleep 2025-07-16 21:49:41 -04:00
bbedward b3db4a3015 processlist: fix context menu 2025-07-16 21:09:08 -04:00
bbedward 2f7e00a5a5 Missing mousearea 2025-07-16 18:52:20 -04:00
bbedward 457bbcd131 migrate to qs imports and .qmlls.ini pattern 2025-07-16 18:47:51 -04:00
bbedward a49911863d network: tweak preference settings for clarity 2025-07-16 17:20:30 -04:00
bbedward ed17b43d45 network: general improvements, itnegrate preference of wifi/ethernet 2025-07-16 16:35:32 -04:00
bbedward 2a84a6423b power: use native upower APIs 2025-07-16 12:35:46 -04:00
purian23 21bc4ece52 Notifications base overhaul 2025-07-16 01:31:30 -04:00
bbedward 8905b10773 weather: remove IP-based location
Probably better for privacy concerns just to use New York, NY as a
hardcoded default
2025-07-15 22:33:58 -04:00
bbedward e5ec8ad454 Merge branch 'master' of github.com:bbedward/dank-material-dark-shell 2025-07-15 22:25:04 -04:00
bbedward f4a33fe97b weather: fix location override 2025-07-15 22:24:50 -04:00
BB d75f782115 Merge pull request #1 from Amadoabad/patch-1
fixing typo in launch command
2025-07-15 21:40:18 -04:00
bbedward 064a914fec Merge branch 'master' of github.com:bbedward/dank-material-dark-shell 2025-07-15 21:31:48 -04:00
bbedward 994be601ae MultiEffect for rounded icons 2025-07-15 21:31:41 -04:00
purian23 03485a5084 Implemented Mouse wheel volume control on hover over the audio icon 2025-07-15 21:19:18 -04:00
purian23 b2b664f581 Update brightness controls for portable PC users 2025-07-15 20:28:03 -04:00
purian23 eb5950f8c8 Fixed battery status on laptops. Updated power profiles. 2025-07-15 18:42:56 -04:00
bbedward 651cb87596 Merge branch 'master' of github.com:bbedward/dank-material-dark-shell 2025-07-15 17:13:42 -04:00
bbedward ac2c327bbb weather: initial support for overriding location 2025-07-15 17:13:24 -04:00
purian23 195d5ec123 Merge branch 'master' of https://github.com/bbedward/dank-material-dark-shell 2025-07-15 16:41:47 -04:00
purian23 4ea04f57b4 Migrate notification system to native Quickshell NotificationServer API
- Replace custom NotificationGroupingService with native NotificationService
- Implement proper image/icon priority system (notification image → app icon → fallback)
- Add NotificationItem with image layering and elegant emoji fallbacks
- Create native popup and history components with smooth animations
- Fix Discord/Vesktop avatar display issues
- Clean up legacy notification components and demos
- Improve Material Design 3 theming consistency
2025-07-15 16:41:34 -04:00
bbedward 13ef607758 display: use ddcutil for brightness changing 2025-07-15 16:22:06 -04:00
bbedward 3df92697e0 processmonitor: add system info tab 2025-07-15 16:08:57 -04:00
bbedward 90999e8ac1 battery: use native UPower service 2025-07-15 15:57:12 -04:00
bbedward b10488c505 cava: pass config via stdin 2025-07-15 15:46:09 -04:00
bbedward 6010b964d8 Slider improvements 2025-07-15 15:37:37 -04:00
bbedward 9035cb750b Use integrated pipewire from quickshell 2025-07-15 15:26:01 -04:00
bbedward b7da76147f bluetooth: switch to native quickshell bluetooth service
removes dependency on bluetoothctl commands
2025-07-15 12:43:22 -04:00
bbedward 1396b0b582 Add Performance tab under Process monitor 2025-07-15 11:12:59 -04:00
Ahmed Abdo c37e3a4ea2 fixing typo in launch command 2025-07-15 16:21:19 +03:00
bbedward 0349b2c361 Fix cropped profile image 2025-07-14 21:56:22 -04:00
bbedward f388840443 Fix cursor style on inputs 2025-07-14 21:53:19 -04:00
purian23 e0bf5df74b Merge branch 'master' of https://github.com/bbedward/dank-material-dark-shell 2025-07-14 20:32:51 -04:00
purian23 56b801c294 Redesign of the notification center 2025-07-14 20:32:06 -04:00
bbedward 9f3358b253 Fix some log warnings 2025-07-14 16:11:40 -04:00
bbedward 6f1e23437c Fix the image input 2025-07-14 15:34:18 -04:00
bbedward ac18849052 Initial process list IPC popout 2025-07-14 15:28:40 -04:00
bbedward c9a256bb79 Remove dynamic borders 2025-07-14 15:06:56 -04:00
bbedward 3b16ae73c7 Popup transparency option 2025-07-14 15:00:58 -04:00
bbedward 0a4453ab3b Fix light mode toggle for matugen themes 2025-07-14 14:43:12 -04:00
bbedward 31f1360d1b ipc wallpaper handler, fix high CPU usage 2025-07-14 14:36:08 -04:00
bbedward 58d711b3e9 Fix event animation 2025-07-14 13:54:06 -04:00
bbedward fff7dace3b Animation improvements, fix SysTray 2025-07-14 13:50:00 -04:00
bbedward d5d45b11c3 Improve system monitor display 2025-07-14 13:17:06 -04:00
bbedward 4486e79afe Enhancements to launcher and other warning fixes
- Persist grid/list setting
- Alignment improvement
2025-07-14 12:46:36 -04:00
bbedward 603ac51df0 Better app launchers using rescan, and fuzzysort. GPL license.
FuzzySort was ripped from caelestia, which is the reason for GPL change.
2025-07-14 11:25:18 -04:00
purian23 5c5653a41a Notification updates 2025-07-14 10:51:17 -04:00
purian23 cf5c26522b Initial implementation of grouped notifications 2025-07-14 10:50:48 -04:00
purian23 68bcc2b049 Dep updates 2025-07-13 19:03:55 -04:00
bbedward 6145ec2a80 Column alignment fixes 2025-07-13 15:40:34 -04:00
bbedward a1b6c9e791 Add system resource dropdown 2025-07-13 15:36:41 -04:00
bbedward a9b5e0b09d Better audio device names 2025-07-13 14:39:25 -04:00
bbedward d12d82c43e Clip text 2025-07-13 12:17:11 -04:00
bbedward 19be4c6bbb Configurable widget displays 2025-07-13 11:48:25 -04:00
bbedward 755f13e06b Focused window service 2025-07-13 11:37:33 -04:00
bbedward 7981bbdbc2 Initial README 2025-07-13 11:22:54 -04:00
bbedward c87ec06ed7 Nicer notification click handlign 2025-07-13 11:12:08 -04:00
purian23 75b9c4e224 Updated image & theme logic and added proper close buttons to the notifications 2025-07-13 00:58:52 -04:00
purian23 aeef300ef0 Force circular image cropping on avatars 2025-07-13 00:01:25 -04:00
bbedward 44f6f3ca1a Code cleanups and some keyboard navigation on AppLauncher 2025-07-12 22:47:39 -04:00
bbedward f843d4dc7b Profile image support 2025-07-12 22:33:12 -04:00
bbedward e6821a73a5 Make recent apps a category 2025-07-12 22:00:40 -04:00
bbedward 2e94cf26a7 Conflict fixes 2025-07-12 21:50:42 -04:00
bbedward c2a12bb950 Merge branch 'master' of github.com:bbedward/dank-material-dark-shell 2025-07-12 21:47:10 -04:00
bbedward ed9af263fd Recently used apps 2025-07-12 21:47:02 -04:00
purian23 fff6e95186 Merge remote changes and resolve conflicts 2025-07-12 21:45:51 -04:00
purian23 979bb46a8b Remove duplicate transparency & theme options from the control center display 2025-07-12 21:44:28 -04:00
purian23 58ac3e0942 Remove duplicate transparency & theme options from the control center display 2025-07-12 21:43:09 -04:00
bbedward a6bc8d65e2 General fixes to spacing and control center 2025-07-12 21:31:53 -04:00
purian23 7cbc29423f Update media loading transitions 2025-07-12 19:30:01 -04:00
purian23 8cbbbb8b50 Add media controls on the top bar media widget 2025-07-12 19:23:12 -04:00
purian23 29272f47b0 Update Central Command Center bar widgets padding 2025-07-12 19:03:22 -04:00
purian23 95676fa562 Fix settings crash 2025-07-12 15:44:53 -04:00
bbedward 5a83ba4782 iRemove policy 2025-07-12 15:05:49 -04:00
bbedward 589f3f1907 Control center improvements 2025-07-12 15:05:18 -04:00
bbedward db05496dca Settings globally, refactor some menus 2025-07-12 14:33:50 -04:00
bbedward f61ee93484 Some more consistent spacing 2025-07-12 13:35:01 -04:00
bbedward 8a2b81aafb Implement calendar events with khal 2025-07-12 13:26:09 -04:00
bbedward 095606f6e9 Improve performance of slider 2025-07-12 12:06:33 -04:00
bbedward ba5217f2f1 Redesign center command center 2025-07-12 11:49:41 -04:00
bbedward 2a29f2a0f2 Simplify preferences 2025-07-12 10:58:06 -04:00
bbedward af0522ada5 Many bug fixes for media center, spotlight 2025-07-12 10:47:01 -04:00
purian23 2375b6e7bd Implement transparency background slider 2025-07-12 04:07:25 -04:00
purian23 959c8544bd Implement light & dark mode theme options 2025-07-12 03:53:53 -04:00
purian23 df3a56f9f4 UX update to widget padding heights, updated workspaces 2025-07-12 03:29:03 -04:00
purian23 e76b0ee443 Implement grid view on spotlight launcher. Updated navigation response on both lauchers 2025-07-12 02:46:28 -04:00
bbedward 307cc8700d Refresh app list 2025-07-11 22:49:23 -04:00
bbedward 4318fca4a2 Merge branch 'master' of github.com:bbedward/dank-material-dark-shell 2025-07-11 22:10:03 -04:00
bbedward abb1e09c7b iReposition audio indicator 2025-07-11 22:09:53 -04:00
purian23 ccc3b1fa35 Addidtional clipboard improvements 2025-07-11 22:09:37 -04:00
bbedward 962c56c0ce Modularlize the TopBar 2025-07-11 22:07:54 -04:00
bbedward ecbc835f10 Remove unused files and code 2025-07-11 21:56:13 -04:00
bbedward 0afe57e527 Workspace click fix 2025-07-11 21:50:41 -04:00
bbedward 483ce1cd93 event based workspace switches 2025-07-11 21:36:42 -04:00
purian23 eb6df3318a Fix clipboard theme and added total count 2025-07-11 20:28:08 -04:00
bbedward 2d2f3040b7 App launcher fixes 2025-07-11 20:08:51 -04:00
bbedward 2fc9fa9f8d Break out the ControlCenter into multiple widgets 2025-07-11 18:19:24 -04:00
bbedward 9e34c6fdfa Spotlight initial version 2025-07-11 18:10:27 -04:00
bbedward 75cb1ead2f Auto-detect wallpaper changes, prefs in ~/.config/DankMaterialShell 2025-07-11 16:14:59 -04:00
bbedward 5a9ef71f75 Merge branch 'master' of github.com:bbedward/dank-material-dark-shell 2025-07-11 15:45:25 -04:00
bbedward 688c4b85fb Media fixes 2025-07-11 15:45:18 -04:00
purian23 ea300794b2 Merge remote changes with CPU/RAM monitor widgets 2025-07-11 14:29:42 -04:00
purian23 95dcb25f97 Add CPU & RAM monitor widgets 2025-07-11 14:28:28 -04:00
bbedward d169f5d4a3 iBluetooth repair, input device support, some media player improvements 2025-07-11 13:50:10 -04:00
bbedward b4f73ceb7b Fix audio device change, set-wallpaper, some popup alignment 2025-07-11 12:05:20 -04:00
bbedward 0b4464fe2c Persist theme preference, dynamic auto theme with matugen 2025-07-11 11:41:41 -04:00
bbedward 6e64dfe499 Remove lsb_release dpeendency 2025-07-11 09:49:31 -04:00
bbedward 708fc55dd3 Merge branch 'master' of github.com:bbedward/dank-material-dark-shell 2025-07-11 09:41:52 -04:00
bbedward 4b3a0b15be Update positioning 2025-07-11 09:41:37 -04:00
purian23 f6b25968dc Implement floating panel design 2025-07-11 09:40:37 -04:00
purian23 c9d40bf7fb Implement basic color theme picker in Control Center. Remove color picker icon & App text from launcher button. 2025-07-11 01:13:58 -04:00
purian23 8e4f578b32 App drawer icon updates 2025-07-10 22:32:31 -04:00
purian23 e16dbdb8bf Merge remote changes with battery status & icon size updates 2025-07-10 21:38:35 -04:00
purian23 3a2a31d2ae Fixed battery status and adjusted battery & power icon sizes 2025-07-10 21:21:13 -04:00
bbedward cf1f610eff Scroll bars and fix position 2025-07-10 20:19:10 -04:00
bbedward 052e47614b iSome extra widgets and adjustments 2025-07-10 19:11:35 -04:00
bbedward c4975019e7 Bluetooth improvements, battery widget, notification fixes 2025-07-10 17:44:51 -04:00
bbedward 40b2a3af1e Modularlize the shell 2025-07-10 16:40:04 -04:00
bbedward 7cdeba1625 Fix multi-monitor 2025-07-10 14:14:16 -04:00
bbedward 53687266a1 add shell 2025-07-10 11:28:27 -04:00
BB 2ef160d210 Initial commit 2025-07-10 11:18:24 -04:00
148 changed files with 33125 additions and 32 deletions
+64
View File
@@ -0,0 +1,64 @@
# C++ objects and libs
*.slo
*.lo
*.o
*.a
*.la
*.lai
*.so
*.so.*
*.dll
*.dylib
# Qt-es
object_script.*.Release
object_script.*.Debug
*_plugin_import.cpp
/.qmake.cache
/.qmake.stash
*.pro.user
*.pro.user.*
*.qbs.user
*.qbs.user.*
*.moc
moc_*.cpp
moc_*.h
qrc_*.cpp
ui_*.h
*.qmlc
*.jsc
Makefile*
*build-*
*.qm
*.prl
# Qt unit tests
target_wrapper.*
# QtCreator
*.autosave
# QtCreator Qml
*.qmlproject.user
*.qmlproject.user.*
# QtCreator CMake
CMakeLists.txt.user*
# QtCreator 4.8< compilation database
compile_commands.json
# QtCreator local machine specific files for imported projects
*creator.user*
*_qmlcache.qrc
UNUSED
.qmlls.ini
CLAUDE-activeContext.md
CLAUDE-temp.md
# Auto-generated theme files
*.generated.*
niri-colors.generated.kdl
ghostty-colors.generated.conf
View File
+416
View File
@@ -0,0 +1,416 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## AI Guidance
* Ignore GEMINI.md and GEMINI-*.md files
* After receiving tool results, carefully reflect on their quality and determine optimal next steps before proceeding. Use your thinking to plan and iterate based on this new information, and then take the best next action.
* For maximum efficiency, whenever you need to perform multiple independent operations, invoke all relevant tools simultaneously rather than sequentially.
* Before you finish, please verify your solution
* Do what has been asked; nothing more, nothing less.
* NEVER create files unless they're absolutely necessary for achieving your goal.
* ALWAYS prefer editing an existing file to creating a new one.
* NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.
* When you update or modify core context files, also update markdown documentation and memory bank
* When asked to commit changes, exclude CLAUDE.md and CLAUDE-*.md referenced memory bank system files from any commits.
## Memory Bank System
This project uses a structured memory bank system with specialized context files. Always check these files for relevant information before starting work:
### Core Context Files
* **CLAUDE-activeContext.md** - Current session state, goals, and progress (if exists)
* **CLAUDE-patterns.md** - Established code patterns and conventions (if exists)
* **CLAUDE-decisions.md** - Architecture decisions and rationale (if exists)
* **CLAUDE-troubleshooting.md** - Common issues and proven solutions (if exists)
* **CLAUDE-config-variables.md** - Configuration variables reference (if exists)
* **CLAUDE-temp.md** - Temporary scratch pad (only read when referenced)
**Important:** Always reference the active context file first to understand what's currently being worked on and maintain session continuity.
### Memory Bank System Backups
When asked to backup Memory Bank System files, you will copy the core context files above and @.claude settings directory to directory @/path/to/backup-directory. If files already exist in the backup directory, you will overwrite them.
## Project Overview
## Project Overview
This is a Quickshell-based desktop shell implementation with Material Design 3 dark theme. The shell provides a complete desktop environment experience with panels, widgets, and system integration services.
**Architecture**: Modular design with clean separation between UI components (Modules), system services (Services), and shared utilities (Common).
## Technology Stack
- **QML (Qt Modeling Language)** - Primary language for all UI components
- **Quickshell Framework** - QML-based framework for building desktop shells
- **Qt/QtQuick** - UI rendering and controls
- **Wayland** - Display server protocol
- **Matugen** - Dynamic theming system for wallpaper-based colors and system app theming
## Development Commands
Since this is a Quickshell-based project without traditional build configuration files, development typically involves:
```bash
# Run the shell (requires Quickshell to be installed)
quickshell -p shell.qml
# Or use the shorthand
qs -p .
# Code formatting and linting
qmlformat -i **/*.qml # Format all QML files in place
qmllint **/*.qml # Lint all QML files for syntax errors
```
## Architecture Overview
### Modular Structure
The shell follows a clean modular architecture reduced from 4,830 lines to ~250 lines in shell.qml:
```
shell.qml # Main entry point (minimal orchestration)
├── Common/ # Shared resources
│ ├── Theme.qml # Material Design 3 theme singleton
│ └── Utilities.js # Shared utility functions
├── Services/ # System integration singletons
│ ├── AudioService.qml
│ ├── NetworkService.qml
│ ├── BrightnessService.qml
│ └── [9 total services]
├── Modules/ # UI components
│ ├── TopBar.qml
│ ├── AppLauncher.qml
│ ├── ControlCenter.qml
│ └── [18 total modules]
└── Widgets/ # Reusable UI controls
├── DankIcon.qml
├── DankSlider.qml
├── DankToggle.qml
├── DankTabBar.qml
├── DankGridView.qml
├── DankListView.qml
└── [6 total widgets]
```
### Component Organization
1. **Shell Entry Point** (`shell.qml`)
- Minimal orchestration layer (~250 lines)
- Imports and instantiates components
- Handles global state and property bindings
- Multi-monitor support using Quickshell's `Variants`
2. **Common/** - Shared resources
- `Theme.qml` - Material Design 3 theme singleton with consistent colors, spacing, fonts
- `Utilities.js` - Shared functions for workspace parsing, notifications, menu handling
3. **Services/** - System integration singletons
- **Pattern**: All services use `Singleton` type with `id: root`
- **Independence**: No cross-service dependencies
- **Examples**: AudioService, NetworkService, BrightnessService, WeatherService
- Services handle system commands, state management, and hardware integration
4. **Modules/** - UI components
- **Full-screen components**: AppLauncher, ClipboardHistoryModal, ControlCenter
- **Panel components**: TopBar, SystemTray, NotificationPopup
- **Layout components**: WorkspaceSwitcher
5. **Widgets/** - Reusable UI controls
- **DankIcon**: Centralized icon component with Material Design font integration
- **DankSlider**: Enhanced slider with animations and smart detection
- **DankToggle**: Consistent toggle switch component
- **DankTabBar**: Unified tab bar implementation
- **DankGridView**: Reusable grid view with adaptive columns
- **DankListView**: Reusable list view with configurable styling
### Key Architectural Patterns
1. **Singleton Services Pattern**:
```qml
import QtQuick
import Quickshell
import Quickshell.Io
pragma Singleton
pragma ComponentBehavior: Bound
Singleton {
id: root
property type value: defaultValue
function performAction() { /* implementation */ }
}
```
2. **Smart Feature Detection**: Services detect system capabilities:
```qml
property bool featureAvailable: false
// Auto-hide UI elements when features unavailable
visible: ServiceName.featureAvailable
```
3. **Property Bindings**: Reactive UI updates through property binding
4. **Material Design Theming**: Consistent use of Theme singleton throughout
### Important Components
- **ControlCenter**: System controls (WiFi, Bluetooth, brightness, volume, night mode)
- **AppLauncher**: Full-featured app grid/list with 93+ applications, search, categories
- **ClipboardHistoryModal**: Complete clipboard management with cliphist integration
- **TopBar**: Per-monitor panels with workspace switching, clock, system tray
- **System App Theming**: Automatic GTK and Qt application theming using matugen templates
#### Key Widgets
- **DankIcon**: Centralized icon component with automatic Material Design font detection
- **DankSlider**: Enhanced slider with animations and smart detection
- **DankToggle**: Consistent toggle switch component
- **DankTabBar**: Unified tab bar implementation
- **DankGridView**: Reusable grid view with adaptive columns
- **DankListView**: Reusable list view with configurable styling
## Code Conventions
### QML Style Guidelines
1. **Structure and Formatting**:
- Use 4-space indentation
- `id` should be the first property
- Properties before signal handlers before child components
- Prefer property bindings over imperative code
- **IMPORTANT**: Be very conservative with comments - add comments only when absolutely necessary for understanding complex logic
2. **Naming Conventions**:
- **Services**: Use `Singleton` type with `id: root`
- **Components**: Use descriptive names (e.g., `DankSlider`, `TopBar`)
- **Properties**: camelCase for properties, PascalCase for types
3. **Null-Safe Operations**:
- **Do NOT use** `?.` operator (not supported by qmlformat)
- **Use** `object && object.property` instead of `object?.property`
- **Example**: `activePlayer && activePlayer.trackTitle` instead of `activePlayer?.trackTitle`
4. **Component Structure**:
```qml
// For regular components
Item {
id: root
property type name: value
signal customSignal(type param)
onSignal: { /* handler */ }
Component { /* children */ }
}
// For services (singletons)
Singleton {
id: root
property bool featureAvailable: false
property type currentValue: defaultValue
function performAction(param) { /* implementation */ }
}
```
### Import Guidelines
1. **Standard Import Order**:
```qml
import QtQuick
import QtQuick.Controls // If needed
import Quickshell
import Quickshell.Widgets
import Quickshell.Io // For Process, FileView
import qs.Common // For Theme, utilities
import qs.Services // For service access
import qs.Widgets // For reusable widgets (DankIcon, etc.)
```
2. **Service Dependencies**:
- Services should NOT import other services
- Modules and Widgets can import and use services via property bindings
- Use `Theme.propertyName` for consistent styling
- Use `DankIcon { name: "icon_name" }` for all icons instead of manual Text components
### Component Development Patterns
1. **Smart Feature Detection**:
```qml
// In services - detect capabilities
property bool brightnessAvailable: false
// In modules - adapt UI accordingly
DankSlider {
visible: BrightnessService.brightnessAvailable
enabled: BrightnessService.brightnessAvailable
value: BrightnessService.brightnessLevel
}
```
2. **Reusable Components**:
- Create reusable widgets for common patterns (like DankSlider)
- Use configurable properties for different use cases
- Include proper signal handling with unique names (avoid `valueChanged`)
3. **Service Integration**:
- Services expose properties and functions
- Modules and Widgets bind to service properties for reactive updates
- Use service functions for actions: `ServiceName.performAction(value)`
- **CRITICAL**: DO NOT create wrapper functions for everything - bind directly to underlying APIs when possible
- Example: Use `BluetoothService.adapter.discovering = true` instead of `BluetoothService.startScan()`
- Example: Use `device.connect()` directly instead of `BluetoothService.connect(device.address)`
### Error Handling and Debugging
1. **Console Logging**:
```qml
// Use appropriate log levels
console.log("Info message") // General info
console.warn("Warning message") // Warnings
console.error("Error message") // Errors
// Include context in service operations
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Service failed:", serviceName, "exit code:", exitCode)
}
}
```
2. **Graceful Degradation**:
- Always check feature availability before showing UI
- Provide fallbacks for missing system tools
- Use `visible` and `enabled` properties appropriately
## Multi-Monitor Support
The shell uses Quickshell's `Variants` pattern for multi-monitor support:
- Each connected monitor gets its own top bar instance
- Workspace switchers are per-display and Niri-aware
- Monitors are automatically detected by screen name (DP-1, DP-2, etc.)
- Workspaces are dynamically synchronized with Niri's per-output workspaces
## Common Development Tasks
### Testing and Validation
When modifying the shell:
1. **Test changes**: `qs -p .` (automatic reload on file changes)
2. **Code quality**: Run `qmlformat -i **/*.qml` and `qmllint **/*.qml` to ensure proper formatting and syntax
3. **Performance**: Ensure animations remain smooth (60 FPS target)
4. **Theming**: Use `Theme.propertyName` for Material Design 3 consistency
5. **Wayland compatibility**: Test on Wayland session
6. **Multi-monitor**: Verify behavior with multiple displays
7. **Feature detection**: Test on systems with/without required tools
### Adding New Modules
1. **Create component**:
```bash
# Create new module file
touch Modules/NewModule.qml
```
2. **Follow module patterns**:
- Use `Theme.propertyName` for styling
- Import `qs.Common` and `qs.Services` as needed
- Import `qs.Widgets` for reusable components
- Bind to service properties for reactive updates
- Consider per-screen vs global behavior
- Use `DankIcon` for icons instead of manual Text components
3. **Integration in shell.qml**:
```qml
NewModule {
id: newModule
// Configure properties
}
```
### Adding New Widgets
1. **Create component**:
```bash
# Create new widget file
touch Widgets/NewWidget.qml
```
2. **Follow widget patterns**:
- Use `Theme.propertyName` for styling
- Import `qs.Common` for theming
- Focus on reusability and composition
- Keep widgets simple and focused
- Use `DankIcon` for icons instead of manual Text components
### Adding New Services
1. **Create service**:
```qml
// Services/NewService.qml
import QtQuick
import Quickshell
import Quickshell.Io
pragma Singleton
pragma ComponentBehavior: Bound
Singleton {
id: root
property bool featureAvailable: false
property type currentValue: defaultValue
function performAction(param) {
// Implementation
}
}
```
2. **Use in modules**:
```qml
// In module files
property alias serviceValue: NewService.currentValue
SomeControl {
visible: NewService.featureAvailable
enabled: NewService.featureAvailable
onTriggered: NewService.performAction(value)
}
```
### Debugging Common Issues
1. **Import errors**: Check import paths
2. **Singleton conflicts**: Ensure services use `Singleton` type with `id: root`
3. **Property binding issues**: Use property aliases for reactive updates
4. **Process failures**: Check system tool availability and command syntax
5. **Theme inconsistencies**: Always use `Theme.propertyName` instead of hardcoded values
### Best Practices Summary
- **Modularity**: Keep components focused and independent
- **Reusability**: Create reusable components for common patterns using Widgets/
- **Responsiveness**: Use property bindings for reactive UI
- **Robustness**: Implement feature detection and graceful degradation
- **Consistency**: Follow Material Design 3 principles via Theme singleton
- **Performance**: Minimize expensive operations and use appropriate data structures
- **Icon Management**: Use `DankIcon` for all icons instead of manual Text components
- **Widget System**: Leverage existing widgets (DankSlider, DankToggle, etc.) for consistency
- **NO WRAPPER HELL**: Avoid creating unnecessary wrapper functions - bind directly to underlying APIs for better reactivity and performance
### Common Widget Patterns
1. **Icons**: Always use `DankIcon { name: "icon_name" }` instead of `Text { font.family: Theme.iconFont }`
2. **Sliders**: Use `DankSlider` for consistent styling and behavior
3. **Toggles**: Use `DankToggle` for switches and checkboxes
4. **Tab Bars**: Use `DankTabBar` for tabbed interfaces
5. **Lists**: Use `DankListView` for scrollable lists
6. **Grids**: Use `DankGridView` for grid layouts
+28
View File
@@ -0,0 +1,28 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
Singleton {
id: root
readonly property int durShort: 200
readonly property int durMed: 450
readonly property int durLong: 600
readonly property int slidePx: 80
readonly property var emphasized: [
0.05, 0.00, 0.133333, 0.06, 0.166667, 0.40,
0.208333, 0.82, 0.25, 1.00, 1.00, 1.00
]
readonly property var emphasizedDecel: [ 0.05, 0.70, 0.10, 1.00, 1.00, 1.00 ]
readonly property var emphasizedAccel: [ 0.30, 0.00, 0.80, 0.15, 1.00, 1.00 ]
readonly property var standard: [ 0.20, 0.00, 0.00, 1.00, 1.00, 1.00 ]
readonly property var standardDecel: [ 0.00, 0.00, 0.00, 1.00, 1.00, 1.00 ]
readonly property var standardAccel: [ 0.30, 0.00, 1.00, 1.00, 1.00, 1.00 ]
}
+65
View File
@@ -0,0 +1,65 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
Singleton {
id: root
readonly property Rounding rounding: Rounding {}
readonly property Spacing spacing: Spacing {}
readonly property FontSize fontSize: FontSize {}
readonly property Anim anim: Anim {}
component Rounding: QtObject {
readonly property int small: 8
readonly property int normal: 12
readonly property int large: 16
readonly property int extraLarge: 24
readonly property int full: 1000
}
component Spacing: QtObject {
readonly property int small: 4
readonly property int normal: 8
readonly property int large: 12
readonly property int extraLarge: 16
readonly property int huge: 24
}
component FontSize: QtObject {
readonly property int small: 12
readonly property int normal: 14
readonly property int large: 16
readonly property int extraLarge: 20
readonly property int huge: 24
}
component AnimCurves: QtObject {
readonly property list<real> standard: [0.2, 0, 0, 1, 1, 1]
readonly property list<real> standardAccel: [0.3, 0, 1, 1, 1, 1]
readonly property list<real> standardDecel: [0, 0, 0, 1, 1, 1]
readonly property list<real> emphasized: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1]
readonly property list<real> emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1]
readonly property list<real> emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1]
readonly property list<real> expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1]
readonly property list<real> expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1]
readonly property list<real> expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1]
}
component AnimDurations: QtObject {
readonly property int quick: 150
readonly property int normal: 300
readonly property int slow: 500
readonly property int extraSlow: 1000
readonly property int expressiveFastSpatial: 350
readonly property int expressiveDefaultSpatial: 500
readonly property int expressiveEffects: 200
}
component Anim: QtObject {
readonly property AnimCurves curves: AnimCurves {}
readonly property AnimDurations durations: AnimDurations {}
}
}
+41
View File
@@ -0,0 +1,41 @@
import Quickshell
import qs.Common
pragma Singleton
Singleton {
id: root
// Clear all image cache
function clearImageCache() {
Quickshell.execDetached(["rm", "-rf", Paths.stringify(Paths.imagecache)]);
Paths.mkdir(Paths.imagecache);
}
// Clear cache older than specified minutes
function clearOldCache(ageInMinutes) {
Quickshell.execDetached(["find", Paths.stringify(Paths.imagecache), "-name", "*.png", "-mmin", `+${ageInMinutes}`, "-delete"]);
}
// Clear cache for specific size
function clearCacheForSize(size) {
Quickshell.execDetached(["find", Paths.stringify(Paths.imagecache), "-name", `*@${size}x${size}.png`, "-delete"]);
}
// Get cache size in MB
function getCacheSize(callback) {
var process = Qt.createQmlObject(`
import Quickshell.Io
Process {
command: ["du", "-sm", "${Paths.stringify(Paths.imagecache)}"]
running: true
stdout: StdioCollector {
onStreamFinished: {
var sizeMB = parseInt(text.split("\\t")[0]) || 0
callback(sizeMB)
}
}
}
`, root);
}
}
+367
View File
@@ -0,0 +1,367 @@
pragma Singleton
pragma ComponentBehavior: Bound
import Qt.labs.platform
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Services
import qs.Common
Singleton {
id: root
readonly property string _homeUrl: StandardPaths.writableLocation(StandardPaths.HomeLocation)
readonly property string homeDir: _homeUrl.startsWith("file://") ? _homeUrl.substring(7) : _homeUrl
readonly property string _configUrl: StandardPaths.writableLocation(StandardPaths.ConfigLocation)
readonly property string configDir: _configUrl.startsWith("file://") ? _configUrl.substring(7) : _configUrl
readonly property string shellDir: Qt.resolvedUrl(".").toString().replace("file://", "").replace("/Common/", "")
readonly property string wallpaperPath: Prefs.wallpaperPath
property bool matugenAvailable: false
property bool gtkThemingEnabled: false
property bool qtThemingEnabled: false
property bool systemThemeGenerationInProgress: false
property string matugenJson: ""
property var matugenColors: ({
})
property bool extractionRequested: false
property int colorUpdateTrigger: 0
property string lastWallpaperTimestamp: ""
property color primary: getMatugenColor("primary", "#42a5f5")
property color secondary: getMatugenColor("secondary", "#8ab4f8")
property color tertiary: getMatugenColor("tertiary", "#bb86fc")
property color tertiaryContainer: getMatugenColor("tertiary_container", "#3700b3")
property color error: getMatugenColor("error", "#cf6679")
property color inversePrimary: getMatugenColor("inverse_primary", "#6200ea")
property color bg: getMatugenColor("background", "#1a1c1e")
property color surface: getMatugenColor("surface", "#1a1c1e")
property color surfaceContainer: getMatugenColor("surface_container", "#1e2023")
property color surfaceContainerHigh: getMatugenColor("surface_container_high", "#292b2f")
property color surfaceVariant: getMatugenColor("surface_variant", "#44464f")
property color surfaceText: getMatugenColor("on_background", "#e3e8ef")
property color primaryText: getMatugenColor("on_primary", "#ffffff")
property color surfaceVariantText: getMatugenColor("on_surface_variant", "#c4c7c5")
property color primaryContainer: getMatugenColor("primary_container", "#1976d2")
property color surfaceTint: getMatugenColor("surface_tint", "#8ab4f8")
property color outline: getMatugenColor("outline", "#8e918f")
property color accentHi: primary
property color accentLo: secondary
signal colorsUpdated()
function onLightModeChanged() {
if (matugenColors && Object.keys(matugenColors).length > 0) {
colorUpdateTrigger++;
colorsUpdated();
if (typeof Theme !== "undefined" && Theme.isDynamicTheme) {
generateSystemThemes();
}
}
}
function extractColors() {
extractionRequested = true;
if (matugenAvailable)
fileChecker.running = true;
else
matugenCheck.running = true;
}
function getMatugenColor(path, fallback) {
colorUpdateTrigger;
const colorMode = (typeof Theme !== "undefined" && Theme.isLightMode) ? "light" : "dark";
let cur = matugenColors && matugenColors.colors && matugenColors.colors[colorMode];
for (const part of path.split(".")) {
if (!cur || typeof cur !== "object" || !(part in cur))
return fallback;
cur = cur[part];
}
return cur || fallback;
}
function isColorDark(c) {
return (0.299 * c.r + 0.587 * c.g + 0.114 * c.b) < 0.5;
}
Component.onCompleted: {
matugenCheck.running = true;
checkGtkThemingAvailability();
checkQtThemingAvailability();
if (typeof Theme !== "undefined")
Theme.isLightModeChanged.connect(root.onLightModeChanged);
}
Process {
id: matugenCheck
command: ["which", "matugen"]
onExited: (code) => {
matugenAvailable = (code === 0);
if (!matugenAvailable) {
ToastService.wallpaperErrorStatus = "matugen_missing";
ToastService.showWarning("matugen not found - dynamic theming disabled");
return ;
}
if (extractionRequested) {
fileChecker.running = true;
}
}
}
Process {
id: fileChecker
command: ["test", "-r", wallpaperPath]
onExited: (code) => {
if (code === 0) {
matugenProcess.running = true;
} else {
ToastService.wallpaperErrorStatus = "error";
ToastService.showError("Wallpaper processing failed");
}
}
}
Process {
id: matugenProcess
command: ["matugen", "-v", "image", wallpaperPath, "--json", "hex"]
stdout: StdioCollector {
id: matugenCollector
onStreamFinished: {
const out = matugenCollector.text;
if (!out.length) {
ToastService.wallpaperErrorStatus = "error";
ToastService.showError("Wallpaper Processing Failed");
return ;
}
try {
root.matugenJson = out;
root.matugenColors = JSON.parse(out);
root.colorsUpdated();
generateAppConfigs();
ToastService.clearWallpaperError();
} catch (e) {
ToastService.wallpaperErrorStatus = "error";
ToastService.showError("Wallpaper Processing Failed");
}
}
}
stderr: StdioCollector {
id: matugenErr
}
}
function generateAppConfigs() {
if (!matugenColors || !matugenColors.colors) {
return;
}
generateNiriConfig();
generateGhosttyConfig();
if (gtkThemingEnabled && typeof Prefs !== "undefined" && Prefs.gtkThemingEnabled) {
generateGtkThemes();
}
if (qtThemingEnabled && typeof Prefs !== "undefined" && Prefs.qtThemingEnabled) {
generateQtThemes();
}
}
function generateNiriConfig() {
var dark = matugenColors.colors.dark;
if (!dark) return;
var bg = dark.background || "#1a1c1e";
var primary = dark.primary || "#42a5f5";
var secondary = dark.secondary || "#8ab4f8";
var inverse = dark.inverse_primary || "#6200ea";
var content = `layout {
border {
active-color "${primary}"
inactive-color "${secondary}"
}
focus-ring {
active-color "${inverse}"
}
background-color "${bg}"
}`;
Quickshell.execDetached(["bash", "-c", `echo '${content}' > niri-colors.generated.kdl`]);
}
function generateGhosttyConfig() {
var dark = matugenColors.colors.dark;
var light = matugenColors.colors.light;
if (!dark || !light) return;
var bg = dark.background || "#1a1c1e";
var fg = dark.on_background || "#e3e8ef";
var primary = dark.primary || "#42a5f5";
var secondary = dark.secondary || "#8ab4f8";
var tertiary = dark.tertiary || "#bb86fc";
var tertiary_ctr = dark.tertiary_container || "#3700b3";
var error = dark.error || "#cf6679";
var inverse = dark.inverse_primary || "#6200ea";
var bg_b = light.background || "#fef7ff";
var fg_b = light.on_background || "#1d1b20";
var primary_b = light.primary || "#1976d2";
var secondary_b = light.secondary || "#1565c0";
var tertiary_b = light.tertiary || "#7b1fa2";
var tertiary_ctr_b = light.tertiary_container || "#e1bee7";
var error_b = light.error || "#b00020";
var inverse_b = light.inverse_primary || "#bb86fc";
var content = `background = ${bg}
foreground = ${fg}
cursor-color = ${inverse}
selection-background = ${secondary}
selection-foreground = #ffffff
palette = 0=${bg}
palette = 1=${error}
palette = 2=${tertiary}
palette = 3=${secondary}
palette = 4=${primary}
palette = 5=${tertiary_ctr}
palette = 6=${inverse}
palette = 7=${fg}
palette = 8=${bg_b}
palette = 9=${error_b}
palette = 10=${tertiary_b}
palette = 11=${secondary_b}
palette = 12=${primary_b}
palette = 13=${tertiary_ctr_b}
palette = 14=${inverse_b}
palette = 15=${fg_b}`;
var ghosttyConfigDir = configDir + "/ghostty";
var ghosttyConfigPath = ghosttyConfigDir + "/config-dankcolors";
Quickshell.execDetached(["bash", "-c", `mkdir -p '${ghosttyConfigDir}' && echo '${content}' > '${ghosttyConfigPath}'`]);
}
function checkGtkThemingAvailability() {
gtkAvailabilityChecker.running = true;
}
function checkQtThemingAvailability() {
qtAvailabilityChecker.running = true;
}
function generateSystemThemes() {
if (systemThemeGenerationInProgress) {
return;
}
if (!matugenAvailable) {
return;
}
if (!wallpaperPath || wallpaperPath === "") {
return;
}
const isLight = (typeof Theme !== "undefined" && Theme.isLightMode) ? "true" : "false";
const iconTheme = (typeof Prefs !== "undefined" && Prefs.iconTheme) ? Prefs.iconTheme : "System Default";
const gtkTheming = (typeof Prefs !== "undefined" && Prefs.gtkThemingEnabled) ? "true" : "false";
const qtTheming = (typeof Prefs !== "undefined" && Prefs.qtThemingEnabled) ? "true" : "false";
systemThemeGenerationInProgress = true;
systemThemeGenerator.command = [shellDir + "/generate-themes.sh", wallpaperPath, shellDir, configDir, "generate", isLight, iconTheme, gtkTheming, qtTheming];
systemThemeGenerator.running = true;
}
function generateGtkThemes() {
generateSystemThemes();
}
function generateQtThemes() {
generateSystemThemes();
}
function restoreSystemThemes() {
const shellDir = root.shellDir;
if (!shellDir) {
return;
}
const isLight = (typeof Theme !== "undefined" && Theme.isLightMode) ? "true" : "false";
const iconTheme = (typeof Prefs !== "undefined" && Prefs.iconTheme) ? Prefs.iconTheme : "System Default";
const gtkTheming = (typeof Prefs !== "undefined" && Prefs.gtkThemingEnabled) ? "true" : "false";
const qtTheming = (typeof Prefs !== "undefined" && Prefs.qtThemingEnabled) ? "true" : "false";
systemThemeRestoreProcess.command = [shellDir + "/generate-themes.sh", "", shellDir, configDir, "restore", isLight, iconTheme, gtkTheming, qtTheming];
systemThemeRestoreProcess.running = true;
}
Process {
id: gtkAvailabilityChecker
command: ["bash", "-c", "command -v gsettings >/dev/null && [ -d " + configDir + "/gtk-3.0 -o -d " + configDir + "/gtk-4.0 ]"]
running: false
onExited: (exitCode) => {
gtkThemingEnabled = (exitCode === 0);
}
}
Process {
id: qtAvailabilityChecker
command: ["bash", "-c", "command -v qt5ct >/dev/null || command -v qt6ct >/dev/null"]
running: false
onExited: (exitCode) => {
qtThemingEnabled = (exitCode === 0);
}
}
Process {
id: systemThemeGenerator
running: false
stdout: StdioCollector {
id: systemThemeStdout
}
stderr: StdioCollector {
id: systemThemeStderr
}
onExited: (exitCode) => {
systemThemeGenerationInProgress = false;
if (exitCode !== 0) {
ToastService.showError("Failed to generate system themes: " + systemThemeStderr.text);
}
}
}
Process {
id: systemThemeRestoreProcess
running: false
stdout: StdioCollector {
id: restoreThemeStdout
}
stderr: StdioCollector {
id: restoreThemeStderr
}
onExited: (exitCode) => {
if (exitCode === 0) {
ToastService.showInfo("System themes restored to default");
} else {
ToastService.showWarning("Failed to restore system themes: " + restoreThemeStderr.text);
}
}
}
}
+42
View File
@@ -0,0 +1,42 @@
pragma Singleton
import Quickshell
import QtCore
Singleton {
id: root
readonly property url home: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
readonly property url pictures: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0]
readonly property url data: `${StandardPaths.standardLocations(StandardPaths.GenericDataLocation)[0]}/DankMaterialShell`
readonly property url state: `${StandardPaths.standardLocations(StandardPaths.GenericStateLocation)[0]}/DankMaterialShell`
readonly property url cache: `${StandardPaths.standardLocations(StandardPaths.GenericCacheLocation)[0]}/DankMaterialShell`
readonly property url config: `${StandardPaths.standardLocations(StandardPaths.GenericConfigLocation)[0]}/DankMaterialShell`
readonly property url imagecache: `${cache}/imagecache`
function stringify(path: url): string {
return path.toString().replace(/%20/g, " ");
}
function expandTilde(path: string): string {
return strip(path.replace("~", stringify(root.home)));
}
function shortenHome(path: string): string {
return path.replace(strip(root.home), "~");
}
function strip(path: url): string {
return stringify(path).replace("file://", "");
}
function mkdir(path: url): void {
Quickshell.execDetached(["mkdir", "-p", strip(path)]);
}
function copy(from: url, to: url): void {
Quickshell.execDetached(["cp", strip(from), strip(to)]);
}
}
+970
View File
@@ -0,0 +1,970 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtCore
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Services
Singleton {
id: root
property int themeIndex: 0
property bool themeIsDynamic: false
property bool isLightMode: false
property real topBarTransparency: 0.75
property real topBarWidgetTransparency: 0.85
property real popupTransparency: 0.92
property real dockTransparency: 1.0
property var appUsageRanking: {}
property bool use24HourClock: true
property bool useFahrenheit: false
property bool nightModeEnabled: false
property string profileImage: ""
property string weatherLocation: "New York, NY"
property string weatherCoordinates: "40.7128,-74.0060"
property bool showLauncherButton: true
property bool showWorkspaceSwitcher: true
property bool showFocusedWindow: true
property bool showWeather: true
property bool showMusic: true
property bool showClipboard: true
property bool showSystemResources: true
property bool showSystemTray: true
property bool showClock: true
property bool showNotificationButton: true
property bool showBattery: true
property bool showControlCenterButton: true
property bool showWorkspaceIndex: false
property bool showWorkspacePadding: false
property var topBarLeftWidgets: ["launcherButton", "workspaceSwitcher", "focusedWindow"]
property var topBarCenterWidgets: ["clock", "music", "weather"]
property var topBarRightWidgets: ["systemTray", "clipboard", "systemResources", "notificationButton", "battery", "controlCenterButton"]
property alias topBarLeftWidgetsModel: leftWidgetsModel
property alias topBarCenterWidgetsModel: centerWidgetsModel
property alias topBarRightWidgetsModel: rightWidgetsModel
signal forceTopBarLayoutRefresh()
ListModel {
id: leftWidgetsModel
}
ListModel {
id: centerWidgetsModel
}
ListModel {
id: rightWidgetsModel
}
property string appLauncherViewMode: "list"
property string spotlightModalViewMode: "list"
property string networkPreference: "auto"
property string iconTheme: "System Default"
property var availableIconThemes: ["System Default"]
property string systemDefaultIconTheme: ""
property bool qt5ctAvailable: false
property bool qt6ctAvailable: false
property bool useOSLogo: false
property string osLogoColorOverride: ""
property real osLogoBrightness: 0.5
property real osLogoContrast: 1.0
property string wallpaperPath: ""
property bool wallpaperDynamicTheming: true
property string wallpaperLastPath: ""
property string profileLastPath: ""
property bool doNotDisturb: false
property string fontFamily: "Inter Variable"
property string monoFontFamily: "Fira Code"
property int fontWeight: Font.Normal
property bool gtkThemingEnabled: false
property bool qtThemingEnabled: false
property bool showDock: false
property var pinnedApps: []
property bool dockAutoHide: false
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: _configUrl.startsWith("file://") ? _configUrl.substring(7) : _configUrl
Timer {
id: fontCheckTimer
interval: 3000
repeat: false
onTriggered: {
var availableFonts = Qt.fontFamilies()
var missingFonts = []
if (fontFamily === defaultFontFamily && !availableFonts.includes(defaultFontFamily)) {
missingFonts.push(defaultFontFamily)
}
if (monoFontFamily === defaultMonoFontFamily && !availableFonts.includes(defaultMonoFontFamily)) {
missingFonts.push(defaultMonoFontFamily)
}
if (missingFonts.length > 0) {
var message = "Missing fonts: " + missingFonts.join(", ") + ". Using system defaults."
ToastService.showWarning(message)
}
}
}
Component.onCompleted: {
loadSettings();
fontCheckTimer.start();
initializeListModels();
}
function initializeListModels() {
updateListModel(leftWidgetsModel, topBarLeftWidgets);
updateListModel(centerWidgetsModel, topBarCenterWidgets);
updateListModel(rightWidgetsModel, topBarRightWidgets);
}
function loadSettings() {
parseSettings(settingsFile.text());
}
function parseSettings(content) {
try {
if (content && content.trim()) {
var settings = JSON.parse(content);
themeIndex = settings.themeIndex !== undefined ? settings.themeIndex : 0;
themeIsDynamic = settings.themeIsDynamic !== undefined ? settings.themeIsDynamic : false;
isLightMode = settings.isLightMode !== undefined ? settings.isLightMode : false;
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;
dockTransparency = settings.dockTransparency !== undefined ? (settings.dockTransparency > 1 ? settings.dockTransparency / 100 : settings.dockTransparency) : 1.0;
appUsageRanking = settings.appUsageRanking || {};
use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true;
useFahrenheit = settings.useFahrenheit !== undefined ? settings.useFahrenheit : false;
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false;
profileImage = settings.profileImage !== undefined ? settings.profileImage : "";
weatherLocation = settings.weatherLocation !== undefined ? settings.weatherLocation : "New York, NY";
weatherCoordinates = settings.weatherCoordinates !== undefined ? settings.weatherCoordinates : "40.7128,-74.0060";
showLauncherButton = settings.showLauncherButton !== undefined ? settings.showLauncherButton : true;
showWorkspaceSwitcher = settings.showWorkspaceSwitcher !== undefined ? settings.showWorkspaceSwitcher : true;
showFocusedWindow = settings.showFocusedWindow !== undefined ? settings.showFocusedWindow : true;
showWeather = settings.showWeather !== undefined ? settings.showWeather : true;
showMusic = settings.showMusic !== undefined ? settings.showMusic : true;
showClipboard = settings.showClipboard !== undefined ? settings.showClipboard : true;
showSystemResources = settings.showSystemResources !== undefined ? settings.showSystemResources : true;
showSystemTray = settings.showSystemTray !== undefined ? settings.showSystemTray : true;
showClock = settings.showClock !== undefined ? settings.showClock : true;
showNotificationButton = settings.showNotificationButton !== undefined ? settings.showNotificationButton : true;
showBattery = settings.showBattery !== undefined ? settings.showBattery : true;
showControlCenterButton = settings.showControlCenterButton !== undefined ? settings.showControlCenterButton : true;
showWorkspaceIndex = settings.showWorkspaceIndex !== undefined ? settings.showWorkspaceIndex : false;
showWorkspacePadding = settings.showWorkspacePadding !== undefined ? settings.showWorkspacePadding : false;
if (settings.topBarWidgetOrder) {
topBarLeftWidgets = settings.topBarWidgetOrder.filter(w => ["launcherButton", "workspaceSwitcher", "focusedWindow"].includes(w));
topBarCenterWidgets = settings.topBarWidgetOrder.filter(w => ["clock", "music", "weather"].includes(w));
topBarRightWidgets = settings.topBarWidgetOrder.filter(w => ["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 : ["clock", "music", "weather"];
var rightWidgets = settings.topBarRightWidgets !== undefined ? settings.topBarRightWidgets : ["systemTray", "clipboard", "systemResources", "notificationButton", "battery", "controlCenterButton"];
topBarLeftWidgets = leftWidgets;
topBarCenterWidgets = centerWidgets;
topBarRightWidgets = rightWidgets;
updateListModel(leftWidgetsModel, leftWidgets);
updateListModel(centerWidgetsModel, centerWidgets);
updateListModel(rightWidgetsModel, rightWidgets);
}
appLauncherViewMode = settings.appLauncherViewMode !== undefined ? settings.appLauncherViewMode : "list";
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.0;
wallpaperPath = settings.wallpaperPath !== undefined ? settings.wallpaperPath : "";
wallpaperDynamicTheming = settings.wallpaperDynamicTheming !== undefined ? settings.wallpaperDynamicTheming : true;
wallpaperLastPath = settings.wallpaperLastPath !== undefined ? settings.wallpaperLastPath : "";
profileLastPath = settings.profileLastPath !== undefined ? settings.profileLastPath : "";
doNotDisturb = settings.doNotDisturb !== undefined ? settings.doNotDisturb : false;
fontFamily = settings.fontFamily !== undefined ? settings.fontFamily : defaultFontFamily;
monoFontFamily = settings.monoFontFamily !== undefined ? settings.monoFontFamily : defaultMonoFontFamily;
fontWeight = settings.fontWeight !== undefined ? settings.fontWeight : Font.Normal;
gtkThemingEnabled = settings.gtkThemingEnabled !== undefined ? settings.gtkThemingEnabled : false;
qtThemingEnabled = settings.qtThemingEnabled !== undefined ? settings.qtThemingEnabled : false;
showDock = settings.showDock !== undefined ? settings.showDock : false;
pinnedApps = settings.pinnedApps !== undefined ? settings.pinnedApps : [];
dockAutoHide = settings.dockAutoHide !== undefined ? settings.dockAutoHide : false;
applyStoredTheme();
detectAvailableIconThemes();
detectQtTools();
updateGtkIconTheme(iconTheme);
applyStoredIconTheme();
} else {
applyStoredTheme();
}
} catch (e) {
applyStoredTheme();
}
}
function saveSettings() {
settingsFile.setText(JSON.stringify({
"themeIndex": themeIndex,
"themeIsDynamic": themeIsDynamic,
"isLightMode": isLightMode,
"topBarTransparency": topBarTransparency,
"topBarWidgetTransparency": topBarWidgetTransparency,
"popupTransparency": popupTransparency,
"dockTransparency": dockTransparency,
"appUsageRanking": appUsageRanking,
"use24HourClock": use24HourClock,
"useFahrenheit": useFahrenheit,
"nightModeEnabled": nightModeEnabled,
"profileImage": profileImage,
"weatherLocation": weatherLocation,
"weatherCoordinates": weatherCoordinates,
"showLauncherButton": showLauncherButton,
"showWorkspaceSwitcher": showWorkspaceSwitcher,
"showFocusedWindow": showFocusedWindow,
"showWeather": showWeather,
"showMusic": showMusic,
"showClipboard": showClipboard,
"showSystemResources": showSystemResources,
"showSystemTray": showSystemTray,
"showClock": showClock,
"showNotificationButton": showNotificationButton,
"showBattery": showBattery,
"showControlCenterButton": showControlCenterButton,
"showWorkspaceIndex": showWorkspaceIndex,
"showWorkspacePadding": showWorkspacePadding,
"topBarLeftWidgets": topBarLeftWidgets,
"topBarCenterWidgets": topBarCenterWidgets,
"topBarRightWidgets": topBarRightWidgets,
"appLauncherViewMode": appLauncherViewMode,
"spotlightModalViewMode": spotlightModalViewMode,
"networkPreference": networkPreference,
"iconTheme": iconTheme,
"useOSLogo": useOSLogo,
"osLogoColorOverride": osLogoColorOverride,
"osLogoBrightness": osLogoBrightness,
"osLogoContrast": osLogoContrast,
"wallpaperPath": wallpaperPath,
"wallpaperDynamicTheming": wallpaperDynamicTheming,
"wallpaperLastPath": wallpaperLastPath,
"profileLastPath": profileLastPath,
"doNotDisturb": doNotDisturb,
"fontFamily": fontFamily,
"monoFontFamily": monoFontFamily,
"fontWeight": fontWeight,
"gtkThemingEnabled": gtkThemingEnabled,
"qtThemingEnabled": qtThemingEnabled,
"showDock": showDock,
"pinnedApps": pinnedApps,
"dockAutoHide": dockAutoHide
}, null, 2));
}
function setShowWorkspaceIndex(enabled) {
showWorkspaceIndex = enabled;
saveSettings();
}
function setShowWorkspacePadding(enabled) {
showWorkspacePadding = enabled;
saveSettings();
}
function applyStoredTheme() {
if (typeof Theme !== "undefined") {
Theme.isLightMode = isLightMode;
Theme.switchTheme(themeIndex, themeIsDynamic, false);
} else {
Qt.callLater(() => {
if (typeof Theme !== "undefined") {
Theme.isLightMode = isLightMode;
Theme.switchTheme(themeIndex, themeIsDynamic, false);
}
});
}
}
function setTheme(index, isDynamic) {
themeIndex = index;
themeIsDynamic = isDynamic;
saveSettings();
}
function setLightMode(lightMode) {
isLightMode = lightMode;
if (typeof Theme !== "undefined") {
Theme.isLightMode = lightMode;
}
saveSettings();
}
function setTopBarTransparency(transparency) {
topBarTransparency = transparency;
saveSettings();
}
function setTopBarWidgetTransparency(transparency) {
topBarWidgetTransparency = transparency;
saveSettings();
}
function setPopupTransparency(transparency) {
popupTransparency = transparency;
saveSettings();
}
function setDockTransparency(transparency) {
dockTransparency = transparency;
saveSettings();
}
function addAppUsage(app) {
if (!app)
return;
var appId = app.id || (app.execString || app.exec || "");
if (!appId)
return;
var currentRanking = Object.assign({}, appUsageRanking);
if (currentRanking[appId]) {
currentRanking[appId].usageCount = (currentRanking[appId].usageCount || 1) + 1;
currentRanking[appId].lastUsed = Date.now();
currentRanking[appId].icon = app.icon || currentRanking[appId].icon || "application-x-executable";
currentRanking[appId].name = app.name || currentRanking[appId].name || "";
} else {
currentRanking[appId] = {
"name": app.name || "",
"exec": app.execString || app.exec || "",
"icon": app.icon || "application-x-executable",
"comment": app.comment || "",
"usageCount": 1,
"lastUsed": Date.now()
};
}
appUsageRanking = currentRanking;
saveSettings();
}
function getAppUsageRanking() {
return appUsageRanking;
}
function getRankedApps() {
var apps = [];
for (var appId in appUsageRanking) {
var appData = appUsageRanking[appId];
apps.push({
id: appId,
name: appData.name,
exec: appData.exec,
icon: appData.icon,
comment: appData.comment,
usageCount: appData.usageCount,
lastUsed: appData.lastUsed
});
}
return apps.sort(function(a, b) {
if (a.usageCount !== b.usageCount)
return b.usageCount - a.usageCount;
return a.name.localeCompare(b.name);
});
}
function cleanupAppUsageRanking(availableAppIds) {
var currentRanking = Object.assign({}, appUsageRanking);
var hasChanges = false;
for (var appId in currentRanking) {
if (availableAppIds.indexOf(appId) === -1) {
delete currentRanking[appId];
hasChanges = true;
}
}
if (hasChanges) {
appUsageRanking = currentRanking;
saveSettings();
}
}
// New preference setters
function setClockFormat(use24Hour) {
use24HourClock = use24Hour;
saveSettings();
}
function setTemperatureUnit(fahrenheit) {
useFahrenheit = fahrenheit;
saveSettings();
}
function setNightModeEnabled(enabled) {
nightModeEnabled = enabled;
saveSettings();
}
function setProfileImage(imageUrl) {
profileImage = imageUrl;
saveSettings();
}
// Widget visibility setters
function setShowLauncherButton(enabled) {
showLauncherButton = enabled;
saveSettings();
}
function setShowWorkspaceSwitcher(enabled) {
showWorkspaceSwitcher = enabled;
saveSettings();
}
function setShowFocusedWindow(enabled) {
showFocusedWindow = enabled;
saveSettings();
}
function setShowWeather(enabled) {
showWeather = enabled;
saveSettings();
}
function setShowMusic(enabled) {
showMusic = enabled;
saveSettings();
}
function setShowClipboard(enabled) {
showClipboard = enabled;
saveSettings();
}
function setShowSystemResources(enabled) {
showSystemResources = enabled;
saveSettings();
}
function setShowSystemTray(enabled) {
showSystemTray = enabled;
saveSettings();
}
function setShowClock(enabled) {
showClock = enabled;
saveSettings();
}
function setShowNotificationButton(enabled) {
showNotificationButton = enabled;
saveSettings();
}
function setShowBattery(enabled) {
showBattery = enabled;
saveSettings();
}
function setShowControlCenterButton(enabled) {
showControlCenterButton = enabled;
saveSettings();
}
function setTopBarWidgetOrder(order) {
topBarWidgetOrder = order;
saveSettings();
}
function setTopBarLeftWidgets(order) {
topBarLeftWidgets = order;
updateListModel(leftWidgetsModel, order);
saveSettings();
}
function setTopBarCenterWidgets(order) {
topBarCenterWidgets = order;
updateListModel(centerWidgetsModel, order);
saveSettings();
}
function setTopBarRightWidgets(order) {
topBarRightWidgets = order;
updateListModel(rightWidgetsModel, order);
saveSettings();
}
function updateListModel(listModel, order) {
listModel.clear();
for (var i = 0; i < order.length; i++) {
var widgetId = typeof order[i] === "string" ? order[i] : order[i].id;
var enabled = typeof order[i] === "string" ? true : order[i].enabled;
var size = typeof order[i] === "string" ? undefined : order[i].size;
var item = {"widgetId": widgetId, "enabled": enabled};
if (size !== undefined) {
item.size = size;
}
listModel.append(item);
}
}
function resetTopBarWidgetsToDefault() {
var defaultLeft = ["launcherButton", "workspaceSwitcher", "focusedWindow"];
var defaultCenter = ["clock", "music", "weather"];
var defaultRight = ["systemTray", "clipboard", "systemResources", "notificationButton", "battery", "controlCenterButton"];
topBarLeftWidgets = defaultLeft;
topBarCenterWidgets = defaultCenter;
topBarRightWidgets = defaultRight;
updateListModel(leftWidgetsModel, defaultLeft);
updateListModel(centerWidgetsModel, defaultCenter);
updateListModel(rightWidgetsModel, defaultRight);
showLauncherButton = true;
showWorkspaceSwitcher = true;
showFocusedWindow = true;
showWeather = true;
showMusic = true;
showClipboard = true;
showSystemResources = true;
showSystemTray = true;
showClock = true;
showNotificationButton = true;
showBattery = true;
showControlCenterButton = true;
saveSettings();
}
// View mode setters
function setAppLauncherViewMode(mode) {
appLauncherViewMode = mode;
saveSettings();
}
function setSpotlightModalViewMode(mode) {
spotlightModalViewMode = mode;
saveSettings();
}
// Weather location setter
function setWeatherLocation(displayName, coordinates) {
weatherLocation = displayName;
weatherCoordinates = coordinates;
saveSettings();
}
// Network preference setter
function setNetworkPreference(preference) {
networkPreference = preference;
saveSettings();
}
function detectAvailableIconThemes() {
// First detect system default, then available themes
systemDefaultDetectionProcess.running = true;
}
function detectQtTools() {
qtToolsDetectionProcess.running = true;
}
function setIconTheme(themeName) {
iconTheme = themeName;
updateGtkIconTheme(themeName);
updateQtIconTheme(themeName);
saveSettings();
if (typeof Theme !== "undefined" && Theme.isDynamicTheme && typeof Colors !== "undefined") {
Colors.generateSystemThemes();
}
}
function updateGtkIconTheme(themeName) {
var gtkThemeName = (themeName === "System Default") ? systemDefaultIconTheme : themeName;
if (gtkThemeName !== "System Default" && gtkThemeName !== "") {
var script =
"if command -v gsettings >/dev/null 2>&1 && gsettings list-schemas | grep -q org.gnome.desktop.interface; then\n" +
" gsettings set org.gnome.desktop.interface icon-theme '" + gtkThemeName + "'\n" +
" echo 'Updated via gsettings'\n" +
"elif command -v dconf >/dev/null 2>&1; then\n" +
" dconf write /org/gnome/desktop/interface/icon-theme \\\"" + gtkThemeName + "\\\"\n" +
" echo 'Updated via dconf'\n" +
"fi\n" +
"\n" +
"# Ensure config directories exist\n" +
"mkdir -p " + _configDir + "/gtk-3.0 " + _configDir + "/gtk-4.0\n" +
"\n" +
"# Update settings.ini files (keep existing gtk-theme-name)\n" +
"for config_dir in " + _configDir + "/gtk-3.0 " + _configDir + "/gtk-4.0; do\n" +
" settings_file=\"$config_dir/settings.ini\"\n" +
" if [ -f \"$settings_file\" ]; then\n" +
" # Update existing icon-theme-name line or add it\n" +
" if grep -q '^gtk-icon-theme-name=' \"$settings_file\"; then\n" +
" sed -i 's/^gtk-icon-theme-name=.*/gtk-icon-theme-name=" + gtkThemeName + "/' \"$settings_file\"\n" +
" else\n" +
" # Add icon theme setting to [Settings] section or create it\n" +
" if grep -q '\\[Settings\\]' \"$settings_file\"; then\n" +
" sed -i '/\\[Settings\\]/a gtk-icon-theme-name=" + gtkThemeName + "' \"$settings_file\"\n" +
" else\n" +
" echo -e '\\n[Settings]\\ngtk-icon-theme-name=" + gtkThemeName + "' >> \"$settings_file\"\n" +
" fi\n" +
" fi\n" +
" else\n" +
" # Create new settings.ini file\n" +
" echo -e '[Settings]\\ngtk-icon-theme-name=" + gtkThemeName + "' > \"$settings_file\"\n" +
" fi\n" +
" echo \"Updated $settings_file\"\n" +
"done\n" +
"\n" +
"# Clear icon cache and force refresh\n" +
"rm -rf ~/.cache/icon-cache ~/.cache/thumbnails 2>/dev/null || true\n" +
"# Send SIGHUP to running GTK applications to reload themes (Fedora-specific)\n" +
"pkill -HUP -f 'gtk' 2>/dev/null || true\n";
Quickshell.execDetached(["sh", "-lc", script]);
}
}
function updateQtIconTheme(themeName) {
var qtThemeName = (themeName === "System Default") ? "" : themeName;
var home = _shq(root._homeUrl.replace("file://", ""));
if (!qtThemeName) {
var revertScript =
"remove_icon_theme() {\n" +
" local config_file=\"$1\"\n" +
" if [ -f \"$config_file\" ]; then\n" +
" awk '\n" +
" BEGIN { in_appearance = 0 }\n" +
" /^\\[Appearance\\]/ { in_appearance = 1; print; next }\n" +
" /^\\[/ && in_appearance { in_appearance = 0 }\n" +
" in_appearance && /^icon_theme=/ { next }\n" +
" { print }\n" +
" ' \"$config_file\" > \"$config_file.tmp\" && mv \"$config_file.tmp\" \"$config_file\"\n" +
" fi\n" +
"}\n" +
"remove_icon_theme " + _configDir + "/qt5ct/qt5ct.conf\n" +
"remove_icon_theme " + _configDir + "/qt6ct/qt6ct.conf\n" +
"rm -f " + _configDir + "/environment.d/95-qtct.conf 2>/dev/null || true\n" +
"systemctl --user import-environment --unset=QT_QPA_PLATFORMTHEME --unset=QT_QPA_PLATFORMTHEME_QT6 2>/dev/null || true\n" +
"dbus-update-activation-environment --systemd QT_QPA_PLATFORMTHEME= QT_QPA_PLATFORMTHEME_QT6= 2>/dev/null || true\n" +
"rm -rf " + home + "/.cache/icon-cache " + home + "/.cache/thumbnails 2>/dev/null || true\n";
Quickshell.execDetached(["sh", "-lc", revertScript]);
return;
}
var script =
"mkdir -p " + _configDir + "/qt5ct " + _configDir + "/qt6ct " + _configDir + "/environment.d 2>/dev/null || true\n" +
"update_qt_config() {\n" +
" local config_file=\"$1\"\n" +
" local theme_name=\"$2\"\n" +
" if [ -f \"$config_file\" ]; then\n" +
" if grep -q '^\\[Appearance\\]' \"$config_file\"; then\n" +
" awk -v theme=\"$theme_name\" '\n" +
" BEGIN { in_appearance = 0; icon_theme_added = 0 }\n" +
" /^\\[Appearance\\]/ { in_appearance = 1; print; next }\n" +
" /^\\[/ && !/^\\[Appearance\\]/ { \n" +
" if (in_appearance && !icon_theme_added) { \n" +
" print \"icon_theme=\" theme; icon_theme_added = 1 \n" +
" } \n" +
" in_appearance = 0; print; next \n" +
" }\n" +
" in_appearance && /^icon_theme=/ { \n" +
" if (!icon_theme_added) { \n" +
" print \"icon_theme=\" theme; icon_theme_added = 1 \n" +
" } \n" +
" next \n" +
" }\n" +
" { print }\n" +
" END { if (in_appearance && !icon_theme_added) print \"icon_theme=\" theme }\n" +
" ' \"$config_file\" > \"$config_file.tmp\" && mv \"$config_file.tmp\" \"$config_file\"\n" +
" else\n" +
" printf '\\n[Appearance]\\nicon_theme=%s\\n' \"$theme_name\" >> \"$config_file\"\n" +
" fi\n" +
" else\n" +
" printf '[Appearance]\\nicon_theme=%s\\n' \"$theme_name\" > \"$config_file\"\n" +
" fi\n" +
"}\n" +
"update_qt_config " + _configDir + "/qt5ct/qt5ct.conf " + _shq(qtThemeName) + "\n" +
"update_qt_config " + _configDir + "/qt6ct/qt6ct.conf " + _shq(qtThemeName) + "\n" +
"if command -v qt5ct >/dev/null 2>&1; then\n" +
" printf 'QT_QPA_PLATFORMTHEME=qt5ct\\n' > " + _configDir + "/environment.d/95-qtct.conf\n" +
" if command -v qt6ct >/dev/null 2>&1; then\n" +
" printf 'QT_QPA_PLATFORMTHEME_QT6=qt6ct\\n' >> " + _configDir + "/environment.d/95-qtct.conf\n" +
" fi\n" +
"else\n" +
" rm -f " + _configDir + "/environment.d/95-qtct.conf 2>/dev/null || true\n" +
"fi\n" +
"systemctl --user import-environment QT_QPA_PLATFORMTHEME QT_QPA_PLATFORMTHEME_QT6 2>/dev/null || true\n" +
"dbus-update-activation-environment --systemd QT_QPA_PLATFORMTHEME QT_QPA_PLATFORMTHEME_QT6 2>/dev/null || true\n" +
"rm -rf " + home + "/.cache/icon-cache " + home + "/.cache/thumbnails 2>/dev/null || true\n";
Quickshell.execDetached(["sh", "-lc", script]);
}
function applyStoredIconTheme() {
updateGtkIconTheme(iconTheme);
updateQtIconTheme(iconTheme);
}
function setUseOSLogo(enabled) {
useOSLogo = enabled;
saveSettings();
}
function setOSLogoColorOverride(color) {
osLogoColorOverride = color;
saveSettings();
}
function setOSLogoBrightness(brightness) {
osLogoBrightness = brightness;
saveSettings();
}
function setOSLogoContrast(contrast) {
osLogoContrast = contrast;
saveSettings();
}
function setWallpaperPath(path) {
wallpaperPath = path;
saveSettings();
if (wallpaperDynamicTheming && path && typeof Theme !== "undefined") {
Theme.switchTheme(themeIndex, true, true);
}
}
function setWallpaperDynamicTheming(enabled) {
wallpaperDynamicTheming = enabled;
saveSettings();
if (enabled && wallpaperPath && typeof Theme !== "undefined") {
Theme.switchTheme(themeIndex, true, true);
}
}
function setWallpaper(imagePath) {
wallpaperPath = imagePath;
saveSettings();
if (wallpaperDynamicTheming && typeof Colors !== "undefined") {
Colors.extractColors();
}
}
function setDoNotDisturb(enabled) {
doNotDisturb = enabled;
saveSettings();
}
function setFontFamily(family) {
fontFamily = family;
saveSettings();
}
function setFontWeight(weight) {
fontWeight = weight;
saveSettings();
}
function setMonoFontFamily(family) {
monoFontFamily = family;
saveSettings();
}
function setGtkThemingEnabled(enabled) {
gtkThemingEnabled = enabled;
saveSettings();
}
function setQtThemingEnabled(enabled) {
qtThemingEnabled = enabled;
saveSettings();
}
function setShowDock(enabled) {
showDock = enabled;
saveSettings();
}
function setPinnedApps(apps) {
pinnedApps = apps;
pinnedAppsChanged(); // Explicitly emit the signal
saveSettings();
}
function addPinnedApp(appId) {
if (!appId) return;
var currentPinned = [...pinnedApps];
if (currentPinned.indexOf(appId) === -1) {
currentPinned.push(appId);
setPinnedApps(currentPinned);
}
}
function removePinnedApp(appId) {
if (!appId) return;
var currentPinned = pinnedApps.filter(id => id !== appId);
setPinnedApps(currentPinned);
}
function isPinnedApp(appId) {
return appId && pinnedApps.indexOf(appId) !== -1;
}
function setDockAutoHide(enabled) {
dockAutoHide = enabled;
saveSettings();
}
function _shq(s) {
return "'" + String(s).replace(/'/g, "'\\''") + "'";
}
FileView {
id: settingsFile
path: StandardPaths.writableLocation(StandardPaths.ConfigLocation) + "/DankMaterialShell/settings.json"
blockLoading: true
blockWrites: true
watchChanges: true
onLoaded: {
parseSettings(settingsFile.text());
}
onLoadFailed: (error) => {
applyStoredTheme();
}
}
Process {
id: systemDefaultDetectionProcess
command: ["sh", "-c", "gsettings get org.gnome.desktop.interface icon-theme 2>/dev/null | sed \"s/'//g\" || echo ''"]
running: false
onExited: (exitCode) => {
if (exitCode === 0 && stdout && stdout.length > 0) {
systemDefaultIconTheme = stdout.trim();
} else {
systemDefaultIconTheme = "";
}
iconThemeDetectionProcess.running = true;
}
}
Process {
id: iconThemeDetectionProcess
command: ["sh", "-c", "find /usr/share/icons ~/.local/share/icons ~/.icons -maxdepth 1 -type d 2>/dev/null | sed 's|.*/||' | grep -v '^icons$' | sort -u"]
running: false
stdout: StdioCollector {
onStreamFinished: {
var detectedThemes = ["System Default"];
if (text && text.trim()) {
var themes = text.trim().split('\n');
for (var i = 0; i < themes.length; i++) {
var theme = themes[i].trim();
if (theme && theme !== "" && theme !== "default" && theme !== "hicolor" && theme !== "locolor") {
detectedThemes.push(theme);
}
}
}
availableIconThemes = detectedThemes;
}
}
}
Process {
id: qtToolsDetectionProcess
command: ["sh", "-c", "echo -n 'qt5ct:'; command -v qt5ct >/dev/null && echo 'true' || echo 'false'; echo -n 'qt6ct:'; command -v qt6ct >/dev/null && echo 'true' || echo 'false'"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text && text.trim()) {
var lines = text.trim().split('\n');
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
if (line.startsWith('qt5ct:')) {
qt5ctAvailable = line.split(':')[1] === 'true';
} else if (line.startsWith('qt6ct:')) {
qt6ctAvailable = line.split(':')[1] === 'true';
}
}
}
}
}
}
IpcHandler {
target: "wallpaper"
function get(): string {
return root.wallpaperPath || ""
}
function set(path: string): string {
if (!path) {
return "ERROR: No path provided"
}
var absolutePath = path.startsWith("/") ? path : StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/" + path
try {
root.setWallpaper(absolutePath)
return "SUCCESS: Wallpaper set to " + absolutePath
} catch (e) {
return "ERROR: Failed to set wallpaper: " + e.toString()
}
}
function clear(): string {
root.setWallpaper("")
return "SUCCESS: Wallpaper cleared"
}
}
IpcHandler {
target: "theme"
function toggle(): string {
root.setLightMode(!root.isLightMode)
return root.isLightMode ? "light" : "dark"
}
function light(): string {
root.setLightMode(true)
return "light"
}
function dark(): string {
root.setLightMode(false)
return "dark"
}
function getMode(): string {
return root.isLightMode ? "light" : "dark"
}
}
}
+9
View File
@@ -0,0 +1,9 @@
import QtQuick
import Quickshell
QtObject {
required property Singleton service
Component.onCompleted: service.refCount++
Component.onDestruction: service.refCount--
}
+617
View File
@@ -0,0 +1,617 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Services.UPower
Singleton {
id: root
property var themes: [{
"name": "Blue",
"primary": "#42a5f5",
"primaryText": "#ffffff",
"primaryContainer": "#1976d2",
"secondary": "#8ab4f8",
"surface": "#1a1c1e",
"surfaceText": "#e3e8ef",
"surfaceVariant": "#44464f",
"surfaceVariantText": "#c4c7c5",
"surfaceTint": "#8ab4f8",
"background": "#1a1c1e",
"backgroundText": "#e3e8ef",
"outline": "#8e918f",
"surfaceContainer": "#1e2023",
"surfaceContainerHigh": "#292b2f"
}, {
"name": "Deep Blue",
"primary": "#0061a4",
"primaryText": "#ffffff",
"primaryContainer": "#004881",
"secondary": "#42a5f5",
"surface": "#1a1c1e",
"surfaceText": "#e3e8ef",
"surfaceVariant": "#44464f",
"surfaceVariantText": "#c4c7c5",
"surfaceTint": "#8ab4f8",
"background": "#1a1c1e",
"backgroundText": "#e3e8ef",
"outline": "#8e918f",
"surfaceContainer": "#1e2023",
"surfaceContainerHigh": "#292b2f"
}, {
"name": "Purple",
"primary": "#D0BCFF",
"primaryText": "#381E72",
"primaryContainer": "#4F378B",
"secondary": "#CCC2DC",
"surface": "#10121E",
"surfaceText": "#E6E0E9",
"surfaceVariant": "#49454F",
"surfaceVariantText": "#CAC4D0",
"surfaceTint": "#D0BCFF",
"background": "#10121E",
"backgroundText": "#E6E0E9",
"outline": "#938F99",
"surfaceContainer": "#1D1B20",
"surfaceContainerHigh": "#2B2930"
}, {
"name": "Green",
"primary": "#4caf50",
"primaryText": "#ffffff",
"primaryContainer": "#388e3c",
"secondary": "#81c995",
"surface": "#0f1411",
"surfaceText": "#e1f5e3",
"surfaceVariant": "#404943",
"surfaceVariantText": "#c1cbc4",
"surfaceTint": "#81c995",
"background": "#0f1411",
"backgroundText": "#e1f5e3",
"outline": "#8b938c",
"surfaceContainer": "#1a1f1b",
"surfaceContainerHigh": "#252a26"
}, {
"name": "Orange",
"primary": "#ff6d00",
"primaryText": "#ffffff",
"primaryContainer": "#e65100",
"secondary": "#ffb74d",
"surface": "#1c1410",
"surfaceText": "#f5f1ea",
"surfaceVariant": "#4a453a",
"surfaceVariantText": "#cbc5b8",
"surfaceTint": "#ffb74d",
"background": "#1c1410",
"backgroundText": "#f5f1ea",
"outline": "#958f84",
"surfaceContainer": "#211e17",
"surfaceContainerHigh": "#2c291f"
}, {
"name": "Red",
"primary": "#f44336",
"primaryText": "#ffffff",
"primaryContainer": "#d32f2f",
"secondary": "#f28b82",
"surface": "#1c1011",
"surfaceText": "#f5e8ea",
"surfaceVariant": "#4a3f41",
"surfaceVariantText": "#cbc2c4",
"surfaceTint": "#f28b82",
"background": "#1c1011",
"backgroundText": "#f5e8ea",
"outline": "#958b8d",
"surfaceContainer": "#211b1c",
"surfaceContainerHigh": "#2c2426"
}, {
"name": "Cyan",
"primary": "#00bcd4",
"primaryText": "#ffffff",
"primaryContainer": "#0097a7",
"secondary": "#4dd0e1",
"surface": "#0f1617",
"surfaceText": "#e8f4f5",
"surfaceVariant": "#3f474a",
"surfaceVariantText": "#c2c9cb",
"surfaceTint": "#4dd0e1",
"background": "#0f1617",
"backgroundText": "#e8f4f5",
"outline": "#8c9194",
"surfaceContainer": "#1a1f20",
"surfaceContainerHigh": "#252b2c"
}, {
"name": "Pink",
"primary": "#e91e63",
"primaryText": "#ffffff",
"primaryContainer": "#c2185b",
"secondary": "#f8bbd9",
"surface": "#1a1014",
"surfaceText": "#f3e8ee",
"surfaceVariant": "#483f45",
"surfaceVariantText": "#c9c2c7",
"surfaceTint": "#f8bbd9",
"background": "#1a1014",
"backgroundText": "#f3e8ee",
"outline": "#938a90",
"surfaceContainer": "#1f1b1e",
"surfaceContainerHigh": "#2a2428"
}, {
"name": "Amber",
"primary": "#ffc107",
"primaryText": "#000000",
"primaryContainer": "#ff8f00",
"secondary": "#ffd54f",
"surface": "#1a1710",
"surfaceText": "#f3f0e8",
"surfaceVariant": "#49453a",
"surfaceVariantText": "#cac5b8",
"surfaceTint": "#ffd54f",
"background": "#1a1710",
"backgroundText": "#f3f0e8",
"outline": "#949084",
"surfaceContainer": "#1f1e17",
"surfaceContainerHigh": "#2a281f"
}, {
"name": "Coral",
"primary": "#ffb4ab",
"primaryText": "#5f1412",
"primaryContainer": "#8c1d18",
"secondary": "#f9dedc",
"surface": "#1a1110",
"surfaceText": "#f1e8e7",
"surfaceVariant": "#4a4142",
"surfaceVariantText": "#cdc2c1",
"surfaceTint": "#ffb4ab",
"background": "#1a1110",
"backgroundText": "#f1e8e7",
"outline": "#968b8a",
"surfaceContainer": "#201a19",
"surfaceContainerHigh": "#2b2221"
}]
property var lightThemes: [{
"name": "Blue Light",
"primary": "#1976d2",
"primaryText": "#ffffff",
"primaryContainer": "#e3f2fd",
"secondary": "#42a5f5",
"surface": "#fefefe",
"surfaceText": "#1a1c1e",
"surfaceVariant": "#e7e0ec",
"surfaceVariantText": "#49454f",
"surfaceTint": "#1976d2",
"background": "#fefefe",
"backgroundText": "#1a1c1e",
"outline": "#79747e",
"surfaceContainer": "#f3f3f3",
"surfaceContainerHigh": "#ececec"
}, {
"name": "Deep Blue Light",
"primary": "#0061a4",
"primaryText": "#ffffff",
"primaryContainer": "#cfe5ff",
"secondary": "#1976d2",
"surface": "#fefefe",
"surfaceText": "#1a1c1e",
"surfaceVariant": "#e7e0ec",
"surfaceVariantText": "#49454f",
"surfaceTint": "#0061a4",
"background": "#fefefe",
"backgroundText": "#1a1c1e",
"outline": "#79747e",
"surfaceContainer": "#f3f3f3",
"surfaceContainerHigh": "#ececec"
}, {
"name": "Purple Light",
"primary": "#6750A4",
"primaryText": "#ffffff",
"primaryContainer": "#EADDFF",
"secondary": "#625B71",
"surface": "#FFFBFE",
"surfaceText": "#1C1B1F",
"surfaceVariant": "#E7E0EC",
"surfaceVariantText": "#49454F",
"surfaceTint": "#6750A4",
"background": "#FFFBFE",
"backgroundText": "#1C1B1F",
"outline": "#79747E",
"surfaceContainer": "#F3EDF7",
"surfaceContainerHigh": "#ECE6F0"
}, {
"name": "Green Light",
"primary": "#2e7d32",
"primaryText": "#ffffff",
"primaryContainer": "#e8f5e8",
"secondary": "#4caf50",
"surface": "#fefefe",
"surfaceText": "#1a1c1e",
"surfaceVariant": "#e7e0ec",
"surfaceVariantText": "#49454f",
"surfaceTint": "#2e7d32",
"background": "#fefefe",
"backgroundText": "#1a1c1e",
"outline": "#79747e",
"surfaceContainer": "#f3f3f3",
"surfaceContainerHigh": "#ececec"
}, {
"name": "Orange Light",
"primary": "#e65100",
"primaryText": "#ffffff",
"primaryContainer": "#ffecb3",
"secondary": "#ff9800",
"surface": "#fefefe",
"surfaceText": "#1a1c1e",
"surfaceVariant": "#e7e0ec",
"surfaceVariantText": "#49454f",
"surfaceTint": "#e65100",
"background": "#fefefe",
"backgroundText": "#1a1c1e",
"outline": "#79747e",
"surfaceContainer": "#f3f3f3",
"surfaceContainerHigh": "#ececec"
}, {
"name": "Red Light",
"primary": "#d32f2f",
"primaryText": "#ffffff",
"primaryContainer": "#ffebee",
"secondary": "#f44336",
"surface": "#fefefe",
"surfaceText": "#1a1c1e",
"surfaceVariant": "#e7e0ec",
"surfaceVariantText": "#49454f",
"surfaceTint": "#d32f2f",
"background": "#fefefe",
"backgroundText": "#1a1c1e",
"outline": "#79747e",
"surfaceContainer": "#f3f3f3",
"surfaceContainerHigh": "#ececec"
}, {
"name": "Cyan Light",
"primary": "#0097a7",
"primaryText": "#ffffff",
"primaryContainer": "#e0f2f1",
"secondary": "#00bcd4",
"surface": "#fefefe",
"surfaceText": "#1a1c1e",
"surfaceVariant": "#e7e0ec",
"surfaceVariantText": "#49454f",
"surfaceTint": "#0097a7",
"background": "#fefefe",
"backgroundText": "#1a1c1e",
"outline": "#79747e",
"surfaceContainer": "#f3f3f3",
"surfaceContainerHigh": "#ececec"
}, {
"name": "Pink Light",
"primary": "#c2185b",
"primaryText": "#ffffff",
"primaryContainer": "#fce4ec",
"secondary": "#e91e63",
"surface": "#fefefe",
"surfaceText": "#1a1c1e",
"surfaceVariant": "#e7e0ec",
"surfaceVariantText": "#49454f",
"surfaceTint": "#c2185b",
"background": "#fefefe",
"backgroundText": "#1a1c1e",
"outline": "#79747e",
"surfaceContainer": "#f3f3f3",
"surfaceContainerHigh": "#ececec"
}, {
"name": "Amber Light",
"primary": "#ff8f00",
"primaryText": "#000000",
"primaryContainer": "#fff8e1",
"secondary": "#ffc107",
"surface": "#fefefe",
"surfaceText": "#1a1c1e",
"surfaceVariant": "#e7e0ec",
"surfaceVariantText": "#49454f",
"surfaceTint": "#ff8f00",
"background": "#fefefe",
"backgroundText": "#1a1c1e",
"outline": "#79747e",
"surfaceContainer": "#f3f3f3",
"surfaceContainerHigh": "#ececec"
}, {
"name": "Coral Light",
"primary": "#8c1d18",
"primaryText": "#ffffff",
"primaryContainer": "#ffdad6",
"secondary": "#ff5449",
"surface": "#fefefe",
"surfaceText": "#1a1c1e",
"surfaceVariant": "#e7e0ec",
"surfaceVariantText": "#49454f",
"surfaceTint": "#8c1d18",
"background": "#fefefe",
"backgroundText": "#1a1c1e",
"outline": "#79747e",
"surfaceContainer": "#f3f3f3",
"surfaceContainerHigh": "#ececec"
}]
property int currentThemeIndex: 0
property bool isDynamicTheme: false
property bool isLightMode: false
property color primary: isDynamicTheme ? Colors.accentHi : getCurrentTheme().primary
property color primaryText: isDynamicTheme ? Colors.primaryText : getCurrentTheme().primaryText
property color primaryContainer: isDynamicTheme ? Colors.primaryContainer : getCurrentTheme().primaryContainer
property color secondary: isDynamicTheme ? Colors.accentLo : getCurrentTheme().secondary
property color surface: isDynamicTheme ? Colors.surface : getCurrentTheme().surface
property color surfaceText: isDynamicTheme ? Colors.surfaceText : getCurrentTheme().surfaceText
property color surfaceVariant: isDynamicTheme ? Colors.surfaceVariant : getCurrentTheme().surfaceVariant
property color surfaceVariantText: isDynamicTheme ? Colors.surfaceVariantText : getCurrentTheme().surfaceVariantText
property color surfaceTint: isDynamicTheme ? Colors.surfaceTint : getCurrentTheme().surfaceTint
property color background: isDynamicTheme ? Colors.bg : getCurrentTheme().background
property color backgroundText: isDynamicTheme ? Colors.surfaceText : getCurrentTheme().backgroundText
property color outline: isDynamicTheme ? Colors.outline : getCurrentTheme().outline
property color surfaceContainer: isDynamicTheme ? Colors.surfaceContainer : getCurrentTheme().surfaceContainer
property color surfaceContainerHigh: isDynamicTheme ? Colors.surfaceContainerHigh : getCurrentTheme().surfaceContainerHigh
property color archBlue: "#1793D1"
property color success: "#4CAF50"
property color warning: "#FF9800"
property color info: "#2196F3"
property color error: "#F2B8B5"
property color primaryHover: Qt.rgba(primary.r, primary.g, primary.b, 0.12)
property color primaryHoverLight: Qt.rgba(primary.r, primary.g, primary.b, 0.08)
property color primaryPressed: Qt.rgba(primary.r, primary.g, primary.b, 0.16)
property color primarySelected: Qt.rgba(primary.r, primary.g, primary.b, 0.3)
property color primaryBackground: Qt.rgba(primary.r, primary.g, primary.b, 0.04)
property color secondaryHover: Qt.rgba(secondary.r, secondary.g, secondary.b, 0.08)
property color surfaceHover: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.08)
property color surfacePressed: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.12)
property color surfaceSelected: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.15)
property color surfaceLight: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.1)
property color surfaceVariantAlpha: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.2)
property color surfaceTextHover: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.08)
property color surfaceTextPressed: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.12)
property color surfaceTextAlpha: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.3)
property color surfaceTextLight: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.06)
property color surfaceTextMedium: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.7)
property color outlineLight: Qt.rgba(outline.r, outline.g, outline.b, 0.05)
property color outlineMedium: Qt.rgba(outline.r, outline.g, outline.b, 0.08)
property color outlineStrong: Qt.rgba(outline.r, outline.g, outline.b, 0.12)
property color outlineSelected: Qt.rgba(outline.r, outline.g, outline.b, 0.2)
property color outlineHeavy: Qt.rgba(outline.r, outline.g, outline.b, 0.3)
property color outlineButton: Qt.rgba(outline.r, outline.g, outline.b, 0.5)
property color errorHover: Qt.rgba(error.r, error.g, error.b, 0.12)
property color errorPressed: Qt.rgba(error.r, error.g, error.b, 0.9)
property color warningHover: Qt.rgba(warning.r, warning.g, warning.b, 0.12)
property color shadowLight: Qt.rgba(0, 0, 0, 0.05)
property color shadowMedium: Qt.rgba(0, 0, 0, 0.08)
property color shadowDark: Qt.rgba(0, 0, 0, 0.1)
property color shadowStrong: Qt.rgba(0, 0, 0, 0.3)
property int shortDuration: 150
property int mediumDuration: 300
property int longDuration: 500
property int extraLongDuration: 1000
property int standardEasing: Easing.OutCubic
property int emphasizedEasing: Easing.OutQuart
property real cornerRadius: 12
property real cornerRadiusSmall: 8
property real cornerRadiusLarge: 16
property real cornerRadiusXLarge: 24
property real spacingXS: 4
property real spacingS: 8
property real spacingM: 12
property real spacingL: 16
property real spacingXL: 24
property real fontSizeSmall: 12
property real fontSizeMedium: 14
property real fontSizeLarge: 16
property real fontSizeXLarge: 20
property real barHeight: 48
property real iconSize: 24
property real iconSizeSmall: 16
property real iconSizeLarge: 32
property real opacityDisabled: 0.38
property real opacityMedium: 0.6
property real opacityHigh: 0.87
property real opacityFull: 1
property real panelTransparency: 0.85
property real widgetTransparency: 0.85
property real popupTransparency: 0.92
function onColorsUpdated() {
if (isDynamicTheme) {
currentThemeIndex = 10;
isDynamicTheme = true;
if (typeof Prefs !== "undefined")
Prefs.setTheme(currentThemeIndex, isDynamicTheme);
}
}
function switchTheme(themeIndex, isDynamic = false, savePrefs = true) {
if (isDynamic && themeIndex === 10) {
isDynamicTheme = true;
if (typeof Colors !== "undefined") {
Colors.extractColors();
}
} else if (themeIndex >= 0 && themeIndex < themes.length) {
if (isDynamicTheme && typeof Colors !== "undefined") {
Colors.restoreSystemThemes();
}
currentThemeIndex = themeIndex;
isDynamicTheme = false;
}
if (savePrefs && typeof Prefs !== "undefined")
Prefs.setTheme(currentThemeIndex, isDynamicTheme);
}
function toggleLightMode(savePrefs = true) {
isLightMode = !isLightMode;
if (savePrefs && typeof Prefs !== "undefined")
Prefs.setLightMode(isLightMode);
}
function getCurrentThemeArray() {
return isLightMode ? lightThemes : themes;
}
function getCurrentTheme() {
var themeArray = getCurrentThemeArray();
return currentThemeIndex < themeArray.length ? themeArray[currentThemeIndex] : themeArray[0];
}
function getPopupBackgroundAlpha() {
return popupTransparency;
}
function getContentBackgroundAlpha() {
return popupTransparency;
}
function popupBackground() {
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, popupTransparency);
}
function contentBackground() {
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, popupTransparency);
}
function panelBackground() {
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, panelTransparency);
}
function widgetBackground() {
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, widgetTransparency);
}
function getBatteryIcon(level, isCharging, batteryAvailable) {
if (!batteryAvailable)
return _getBatteryPowerProfileIcon();
if (isCharging) {
if (level >= 90)
return "battery_charging_full";
if (level >= 80)
return "battery_charging_90";
if (level >= 60)
return "battery_charging_80";
if (level >= 50)
return "battery_charging_60";
if (level >= 30)
return "battery_charging_50";
if (level >= 20)
return "battery_charging_30";
return "battery_charging_20";
} else {
if (level >= 95)
return "battery_full";
if (level >= 85)
return "battery_6_bar";
if (level >= 70)
return "battery_5_bar";
if (level >= 55)
return "battery_4_bar";
if (level >= 40)
return "battery_3_bar";
if (level >= 25)
return "battery_2_bar";
if (level >= 10)
return "battery_1_bar";
return "battery_alert";
}
}
function _getBatteryPowerProfileIcon() {
if (typeof PowerProfiles === "undefined")
return "balance";
switch (PowerProfiles.profile) {
case PowerProfile.PowerSaver:
return "energy_savings_leaf";
case PowerProfile.Performance:
return "rocket_launch";
default:
return "balance";
}
}
function getPowerProfileIcon(profile) {
switch (profile) {
case PowerProfile.PowerSaver:
return "battery_saver";
case PowerProfile.Balanced:
return "battery_std";
case PowerProfile.Performance:
return "flash_on";
default:
return "settings";
}
}
function getPowerProfileLabel(profile) {
switch (profile) {
case PowerProfile.PowerSaver:
return "Power Saver";
case PowerProfile.Balanced:
return "Balanced";
case PowerProfile.Performance:
return "Performance";
default:
return profile.charAt(0).toUpperCase() + profile.slice(1);
}
}
function getPowerProfileDescription(profile) {
switch (profile) {
case PowerProfile.PowerSaver:
return "Extend battery life";
case PowerProfile.Balanced:
return "Balance power and performance";
case PowerProfile.Performance:
return "Prioritize performance";
default:
return "Custom power profile";
}
}
Component.onCompleted: {
if (typeof Colors !== "undefined")
Colors.colorsUpdated.connect(root.onColorsUpdated);
if (typeof Prefs !== "undefined") {
if (Prefs.popupTransparency !== undefined)
root.popupTransparency = Prefs.popupTransparency;
if (Prefs.topBarWidgetTransparency !== undefined)
root.widgetTransparency = Prefs.topBarWidgetTransparency;
if (Prefs.popupTransparencyChanged)
Prefs.popupTransparencyChanged.connect(function() {
if (typeof Prefs !== "undefined" && Prefs.popupTransparency !== undefined)
root.popupTransparency = Prefs.popupTransparency;
});
if (Prefs.topBarWidgetTransparencyChanged)
Prefs.topBarWidgetTransparencyChanged.connect(function() {
if (typeof Prefs !== "undefined" && Prefs.topBarWidgetTransparency !== undefined)
root.widgetTransparency = Prefs.topBarWidgetTransparency;
});
}
}
}
+678
View File
@@ -0,0 +1,678 @@
.pragma library
var single = (search, target) => {
if(!search || !target) return NULL
var preparedSearch = getPreparedSearch(search)
if(!isPrepared(target)) target = getPrepared(target)
var searchBitflags = preparedSearch.bitflags
if((searchBitflags & target._bitflags) !== searchBitflags) return NULL
return algorithm(preparedSearch, target)
}
var go = (search, targets, options) => {
if(!search) return options?.all ? all(targets, options) : noResults
var preparedSearch = getPreparedSearch(search)
var searchBitflags = preparedSearch.bitflags
var containsSpace = preparedSearch.containsSpace
var threshold = denormalizeScore( options?.threshold || 0 )
var limit = options?.limit || INFINITY
var resultsLen = 0; var limitedCount = 0
var targetsLen = targets.length
function push_result(result) {
if(resultsLen < limit) { q.add(result); ++resultsLen }
else {
++limitedCount
if(result._score > q.peek()._score) q.replaceTop(result)
}
}
// This code is copy/pasted 3 times for performance reasons [options.key, options.keys, no keys]
// options.key
if(options?.key) {
var key = options.key
for(var i = 0; i < targetsLen; ++i) { var obj = targets[i]
var target = getValue(obj, key)
if(!target) continue
if(!isPrepared(target)) target = getPrepared(target)
if((searchBitflags & target._bitflags) !== searchBitflags) continue
var result = algorithm(preparedSearch, target)
if(result === NULL) continue
if(result._score < threshold) continue
result.obj = obj
push_result(result)
}
// options.keys
} else if(options?.keys) {
var keys = options.keys
var keysLen = keys.length
outer: for(var i = 0; i < targetsLen; ++i) { var obj = targets[i]
{ // early out based on bitflags
var keysBitflags = 0
for (var keyI = 0; keyI < keysLen; ++keyI) {
var key = keys[keyI]
var target = getValue(obj, key)
if(!target) { tmpTargets[keyI] = noTarget; continue }
if(!isPrepared(target)) target = getPrepared(target)
tmpTargets[keyI] = target
keysBitflags |= target._bitflags
}
if((searchBitflags & keysBitflags) !== searchBitflags) continue
}
if(containsSpace) for(let i=0; i<preparedSearch.spaceSearches.length; i++) keysSpacesBestScores[i] = NEGATIVE_INFINITY
for (var keyI = 0; keyI < keysLen; ++keyI) {
target = tmpTargets[keyI]
if(target === noTarget) { tmpResults[keyI] = noTarget; continue }
tmpResults[keyI] = algorithm(preparedSearch, target, /*allowSpaces=*/false, /*allowPartialMatch=*/containsSpace)
if(tmpResults[keyI] === NULL) { tmpResults[keyI] = noTarget; continue }
// todo: this seems weird and wrong. like what if our first match wasn't good. this should just replace it instead of averaging with it
// if our second match isn't good we ignore it instead of averaging with it
if(containsSpace) for(let i=0; i<preparedSearch.spaceSearches.length; i++) {
if(allowPartialMatchScores[i] > -1000) {
if(keysSpacesBestScores[i] > NEGATIVE_INFINITY) {
var tmp = (keysSpacesBestScores[i] + allowPartialMatchScores[i]) / 4/*bonus score for having multiple matches*/
if(tmp > keysSpacesBestScores[i]) keysSpacesBestScores[i] = tmp
}
}
if(allowPartialMatchScores[i] > keysSpacesBestScores[i]) keysSpacesBestScores[i] = allowPartialMatchScores[i]
}
}
if(containsSpace) {
for(let i=0; i<preparedSearch.spaceSearches.length; i++) { if(keysSpacesBestScores[i] === NEGATIVE_INFINITY) continue outer }
} else {
var hasAtLeast1Match = false
for(let i=0; i < keysLen; i++) { if(tmpResults[i]._score !== NEGATIVE_INFINITY) { hasAtLeast1Match = true; break } }
if(!hasAtLeast1Match) continue
}
var objResults = new KeysResult(keysLen)
for(let i=0; i < keysLen; i++) { objResults[i] = tmpResults[i] }
if(containsSpace) {
var score = 0
for(let i=0; i<preparedSearch.spaceSearches.length; i++) score += keysSpacesBestScores[i]
} else {
// todo could rewrite this scoring to be more similar to when there's spaces
// if we match multiple keys give us bonus points
var score = NEGATIVE_INFINITY
for(let i=0; i<keysLen; i++) {
var result = objResults[i]
if(result._score > -1000) {
if(score > NEGATIVE_INFINITY) {
var tmp = (score + result._score) / 4/*bonus score for having multiple matches*/
if(tmp > score) score = tmp
}
}
if(result._score > score) score = result._score
}
}
objResults.obj = obj
objResults._score = score
if(options?.scoreFn) {
score = options.scoreFn(objResults)
if(!score) continue
score = denormalizeScore(score)
objResults._score = score
}
if(score < threshold) continue
push_result(objResults)
}
// no keys
} else {
for(var i = 0; i < targetsLen; ++i) { var target = targets[i]
if(!target) continue
if(!isPrepared(target)) target = getPrepared(target)
if((searchBitflags & target._bitflags) !== searchBitflags) continue
var result = algorithm(preparedSearch, target)
if(result === NULL) continue
if(result._score < threshold) continue
push_result(result)
}
}
if(resultsLen === 0) return noResults
var results = new Array(resultsLen)
for(var i = resultsLen - 1; i >= 0; --i) results[i] = q.poll()
results.total = resultsLen + limitedCount
return results
}
// this is written as 1 function instead of 2 for minification. perf seems fine ...
// except when minified. the perf is very slow
var highlight = (result, open='<b>', close='</b>') => {
var callback = typeof open === 'function' ? open : undefined
var target = result.target
var targetLen = target.length
var indexes = result.indexes
var highlighted = ''
var matchI = 0
var indexesI = 0
var opened = false
var parts = []
for(var i = 0; i < targetLen; ++i) { var char = target[i]
if(indexes[indexesI] === i) {
++indexesI
if(!opened) { opened = true
if(callback) {
parts.push(highlighted); highlighted = ''
} else {
highlighted += open
}
}
if(indexesI === indexes.length) {
if(callback) {
highlighted += char
parts.push(callback(highlighted, matchI++)); highlighted = ''
parts.push(target.substr(i+1))
} else {
highlighted += char + close + target.substr(i+1)
}
break
}
} else {
if(opened) { opened = false
if(callback) {
parts.push(callback(highlighted, matchI++)); highlighted = ''
} else {
highlighted += close
}
}
}
highlighted += char
}
return callback ? parts : highlighted
}
var prepare = (target) => {
if(typeof target === 'number') target = ''+target
else if(typeof target !== 'string') target = ''
var info = prepareLowerInfo(target)
return new_result(target, {_targetLower:info._lower, _targetLowerCodes:info.lowerCodes, _bitflags:info.bitflags})
}
var cleanup = () => { preparedCache.clear(); preparedSearchCache.clear() }
// Below this point is only internal code
// Below this point is only internal code
// Below this point is only internal code
// Below this point is only internal code
class Result {
get ['indexes']() { return this._indexes.slice(0, this._indexes.len).sort((a,b)=>a-b) }
set ['indexes'](indexes) { return this._indexes = indexes }
['highlight'](open, close) { return highlight(this, open, close) }
get ['score']() { return normalizeScore(this._score) }
set ['score'](score) { this._score = denormalizeScore(score) }
}
class KeysResult extends Array {
get ['score']() { return normalizeScore(this._score) }
set ['score'](score) { this._score = denormalizeScore(score) }
}
var new_result = (target, options) => {
const result = new Result()
result['target'] = target
result['obj'] = options.obj ?? NULL
result._score = options._score ?? NEGATIVE_INFINITY
result._indexes = options._indexes ?? []
result._targetLower = options._targetLower ?? ''
result._targetLowerCodes = options._targetLowerCodes ?? NULL
result._nextBeginningIndexes = options._nextBeginningIndexes ?? NULL
result._bitflags = options._bitflags ?? 0
return result
}
var normalizeScore = score => {
if(score === NEGATIVE_INFINITY) return 0
if(score > 1) return score
return Math.E ** ( ((-score + 1)**.04307 - 1) * -2)
}
var denormalizeScore = normalizedScore => {
if(normalizedScore === 0) return NEGATIVE_INFINITY
if(normalizedScore > 1) return normalizedScore
return 1 - Math.pow((Math.log(normalizedScore) / -2 + 1), 1 / 0.04307)
}
var prepareSearch = (search) => {
if(typeof search === 'number') search = ''+search
else if(typeof search !== 'string') search = ''
search = search.trim()
var info = prepareLowerInfo(search)
var spaceSearches = []
if(info.containsSpace) {
var searches = search.split(/\s+/)
searches = [...new Set(searches)] // distinct
for(var i=0; i<searches.length; i++) {
if(searches[i] === '') continue
var _info = prepareLowerInfo(searches[i])
spaceSearches.push({lowerCodes:_info.lowerCodes, _lower:searches[i].toLowerCase(), containsSpace:false})
}
}
return {lowerCodes: info.lowerCodes, _lower: info._lower, containsSpace: info.containsSpace, bitflags: info.bitflags, spaceSearches: spaceSearches}
}
var getPrepared = (target) => {
if(target.length > 999) return prepare(target) // don't cache huge targets
var targetPrepared = preparedCache.get(target)
if(targetPrepared !== undefined) return targetPrepared
targetPrepared = prepare(target)
preparedCache.set(target, targetPrepared)
return targetPrepared
}
var getPreparedSearch = (search) => {
if(search.length > 999) return prepareSearch(search) // don't cache huge searches
var searchPrepared = preparedSearchCache.get(search)
if(searchPrepared !== undefined) return searchPrepared
searchPrepared = prepareSearch(search)
preparedSearchCache.set(search, searchPrepared)
return searchPrepared
}
var all = (targets, options) => {
var results = []; results.total = targets.length // this total can be wrong if some targets are skipped
var limit = options?.limit || INFINITY
if(options?.key) {
for(var i=0;i<targets.length;i++) { var obj = targets[i]
var target = getValue(obj, options.key)
if(target == NULL) continue
if(!isPrepared(target)) target = getPrepared(target)
var result = new_result(target.target, {_score: target._score, obj: obj})
results.push(result); if(results.length >= limit) return results
}
} else if(options?.keys) {
for(var i=0;i<targets.length;i++) { var obj = targets[i]
var objResults = new KeysResult(options.keys.length)
for (var keyI = options.keys.length - 1; keyI >= 0; --keyI) {
var target = getValue(obj, options.keys[keyI])
if(!target) { objResults[keyI] = noTarget; continue }
if(!isPrepared(target)) target = getPrepared(target)
target._score = NEGATIVE_INFINITY
target._indexes.len = 0
objResults[keyI] = target
}
objResults.obj = obj
objResults._score = NEGATIVE_INFINITY
results.push(objResults); if(results.length >= limit) return results
}
} else {
for(var i=0;i<targets.length;i++) { var target = targets[i]
if(target == NULL) continue
if(!isPrepared(target)) target = getPrepared(target)
target._score = NEGATIVE_INFINITY
target._indexes.len = 0
results.push(target); if(results.length >= limit) return results
}
}
return results
}
var algorithm = (preparedSearch, prepared, allowSpaces=false, allowPartialMatch=false) => {
if(allowSpaces===false && preparedSearch.containsSpace) return algorithmSpaces(preparedSearch, prepared, allowPartialMatch)
var searchLower = preparedSearch._lower
var searchLowerCodes = preparedSearch.lowerCodes
var searchLowerCode = searchLowerCodes[0]
var targetLowerCodes = prepared._targetLowerCodes
var searchLen = searchLowerCodes.length
var targetLen = targetLowerCodes.length
var searchI = 0 // where we at
var targetI = 0 // where you at
var matchesSimpleLen = 0
// very basic fuzzy match; to remove non-matching targets ASAP!
// walk through target. find sequential matches.
// if all chars aren't found then exit
for(;;) {
var isMatch = searchLowerCode === targetLowerCodes[targetI]
if(isMatch) {
matchesSimple[matchesSimpleLen++] = targetI
++searchI; if(searchI === searchLen) break
searchLowerCode = searchLowerCodes[searchI]
}
++targetI; if(targetI >= targetLen) return NULL // Failed to find searchI
}
var searchI = 0
var successStrict = false
var matchesStrictLen = 0
var nextBeginningIndexes = prepared._nextBeginningIndexes
if(nextBeginningIndexes === NULL) nextBeginningIndexes = prepared._nextBeginningIndexes = prepareNextBeginningIndexes(prepared.target)
targetI = matchesSimple[0]===0 ? 0 : nextBeginningIndexes[matchesSimple[0]-1]
// Our target string successfully matched all characters in sequence!
// Let's try a more advanced and strict test to improve the score
// only count it as a match if it's consecutive or a beginning character!
var backtrackCount = 0
if(targetI !== targetLen) for(;;) {
if(targetI >= targetLen) {
// We failed to find a good spot for this search char, go back to the previous search char and force it forward
if(searchI <= 0) break // We failed to push chars forward for a better match
++backtrackCount; if(backtrackCount > 200) break // exponential backtracking is taking too long, just give up and return a bad match
--searchI
var lastMatch = matchesStrict[--matchesStrictLen]
targetI = nextBeginningIndexes[lastMatch]
} else {
var isMatch = searchLowerCodes[searchI] === targetLowerCodes[targetI]
if(isMatch) {
matchesStrict[matchesStrictLen++] = targetI
++searchI; if(searchI === searchLen) { successStrict = true; break }
++targetI
} else {
targetI = nextBeginningIndexes[targetI]
}
}
}
// check if it's a substring match
var substringIndex = searchLen <= 1 ? -1 : prepared._targetLower.indexOf(searchLower, matchesSimple[0]) // perf: this is slow
var isSubstring = !!~substringIndex
var isSubstringBeginning = !isSubstring ? false : substringIndex===0 || prepared._nextBeginningIndexes[substringIndex-1] === substringIndex
// if it's a substring match but not at a beginning index, let's try to find a substring starting at a beginning index for a better score
if(isSubstring && !isSubstringBeginning) {
for(var i=0; i<nextBeginningIndexes.length; i=nextBeginningIndexes[i]) {
if(i <= substringIndex) continue
for(var s=0; s<searchLen; s++) if(searchLowerCodes[s] !== prepared._targetLowerCodes[i+s]) break
if(s === searchLen) { substringIndex = i; isSubstringBeginning = true; break }
}
}
// tally up the score & keep track of matches for highlighting later
// if it's a simple match, we'll switch to a substring match if a substring exists
// if it's a strict match, we'll switch to a substring match only if that's a better score
var calculateScore = matches => {
var score = 0
var extraMatchGroupCount = 0
for(var i = 1; i < searchLen; ++i) {
if(matches[i] - matches[i-1] !== 1) {score -= matches[i]; ++extraMatchGroupCount}
}
var unmatchedDistance = matches[searchLen-1] - matches[0] - (searchLen-1)
score -= (12+unmatchedDistance) * extraMatchGroupCount // penality for more groups
if(matches[0] !== 0) score -= matches[0]*matches[0]*.2 // penality for not starting near the beginning
if(!successStrict) {
score *= 1000
} else {
// successStrict on a target with too many beginning indexes loses points for being a bad target
var uniqueBeginningIndexes = 1
for(var i = nextBeginningIndexes[0]; i < targetLen; i=nextBeginningIndexes[i]) ++uniqueBeginningIndexes
if(uniqueBeginningIndexes > 24) score *= (uniqueBeginningIndexes-24)*10 // quite arbitrary numbers here ...
}
score -= (targetLen - searchLen)/2 // penality for longer targets
if(isSubstring) score /= 1+searchLen*searchLen*1 // bonus for being a full substring
if(isSubstringBeginning) score /= 1+searchLen*searchLen*1 // bonus for substring starting on a beginningIndex
score -= (targetLen - searchLen)/2 // penality for longer targets
return score
}
if(!successStrict) {
if(isSubstring) for(var i=0; i<searchLen; ++i) matchesSimple[i] = substringIndex+i // at this point it's safe to overwrite matchehsSimple with substr matches
var matchesBest = matchesSimple
var score = calculateScore(matchesBest)
} else {
if(isSubstringBeginning) {
for(var i=0; i<searchLen; ++i) matchesSimple[i] = substringIndex+i // at this point it's safe to overwrite matchehsSimple with substr matches
var matchesBest = matchesSimple
var score = calculateScore(matchesSimple)
} else {
var matchesBest = matchesStrict
var score = calculateScore(matchesStrict)
}
}
prepared._score = score
for(var i = 0; i < searchLen; ++i) prepared._indexes[i] = matchesBest[i]
prepared._indexes.len = searchLen
const result = new Result()
result.target = prepared.target
result._score = prepared._score
result._indexes = prepared._indexes
return result
}
var algorithmSpaces = (preparedSearch, target, allowPartialMatch) => {
var seen_indexes = new Set()
var score = 0
var result = NULL
var first_seen_index_last_search = 0
var searches = preparedSearch.spaceSearches
var searchesLen = searches.length
var changeslen = 0
// Return _nextBeginningIndexes back to its normal state
var resetNextBeginningIndexes = () => {
for(let i=changeslen-1; i>=0; i--) target._nextBeginningIndexes[nextBeginningIndexesChanges[i*2 + 0]] = nextBeginningIndexesChanges[i*2 + 1]
}
var hasAtLeast1Match = false
for(var i=0; i<searchesLen; ++i) {
allowPartialMatchScores[i] = NEGATIVE_INFINITY
var search = searches[i]
result = algorithm(search, target)
if(allowPartialMatch) {
if(result === NULL) continue
hasAtLeast1Match = true
} else {
if(result === NULL) {resetNextBeginningIndexes(); return NULL}
}
// if not the last search, we need to mutate _nextBeginningIndexes for the next search
var isTheLastSearch = i === searchesLen - 1
if(!isTheLastSearch) {
var indexes = result._indexes
var indexesIsConsecutiveSubstring = true
for(let i=0; i<indexes.len-1; i++) {
if(indexes[i+1] - indexes[i] !== 1) {
indexesIsConsecutiveSubstring = false; break;
}
}
if(indexesIsConsecutiveSubstring) {
var newBeginningIndex = indexes[indexes.len-1] + 1
var toReplace = target._nextBeginningIndexes[newBeginningIndex-1]
for(let i=newBeginningIndex-1; i>=0; i--) {
if(toReplace !== target._nextBeginningIndexes[i]) break
target._nextBeginningIndexes[i] = newBeginningIndex
nextBeginningIndexesChanges[changeslen*2 + 0] = i
nextBeginningIndexesChanges[changeslen*2 + 1] = toReplace
changeslen++
}
}
}
score += result._score / searchesLen
allowPartialMatchScores[i] = result._score / searchesLen
// dock points based on order otherwise "c man" returns Manifest.cpp instead of CheatManager.h
if(result._indexes[0] < first_seen_index_last_search) {
score -= (first_seen_index_last_search - result._indexes[0]) * 2
}
first_seen_index_last_search = result._indexes[0]
for(var j=0; j<result._indexes.len; ++j) seen_indexes.add(result._indexes[j])
}
if(allowPartialMatch && !hasAtLeast1Match) return NULL
resetNextBeginningIndexes()
// allows a search with spaces that's an exact substring to score well
var allowSpacesResult = algorithm(preparedSearch, target, /*allowSpaces=*/true)
if(allowSpacesResult !== NULL && allowSpacesResult._score > score) {
if(allowPartialMatch) {
for(var i=0; i<searchesLen; ++i) {
allowPartialMatchScores[i] = allowSpacesResult._score / searchesLen
}
}
return allowSpacesResult
}
if(allowPartialMatch) result = target
result._score = score
var i = 0
for (let index of seen_indexes) result._indexes[i++] = index
result._indexes.len = i
return result
}
// we use this instead of just .normalize('NFD').replace(/[\u0300-\u036f]/g, '') because that screws with japanese characters
var remove_accents = (str) => str.replace(/\p{Script=Latin}+/gu, match => match.normalize('NFD')).replace(/[\u0300-\u036f]/g, '')
var prepareLowerInfo = (str) => {
str = remove_accents(str)
var strLen = str.length
var lower = str.toLowerCase()
var lowerCodes = [] // new Array(strLen) sparse array is too slow
var bitflags = 0
var containsSpace = false // space isn't stored in bitflags because of how searching with a space works
for(var i = 0; i < strLen; ++i) {
var lowerCode = lowerCodes[i] = lower.charCodeAt(i)
if(lowerCode === 32) {
containsSpace = true
continue // it's important that we don't set any bitflags for space
}
var bit = lowerCode>=97&&lowerCode<=122 ? lowerCode-97 // alphabet
: lowerCode>=48&&lowerCode<=57 ? 26 // numbers
// 3 bits available
: lowerCode<=127 ? 30 // other ascii
: 31 // other utf8
bitflags |= 1<<bit
}
return {lowerCodes:lowerCodes, bitflags:bitflags, containsSpace:containsSpace, _lower:lower}
}
var prepareBeginningIndexes = (target) => {
var targetLen = target.length
var beginningIndexes = []; var beginningIndexesLen = 0
var wasUpper = false
var wasAlphanum = false
for(var i = 0; i < targetLen; ++i) {
var targetCode = target.charCodeAt(i)
var isUpper = targetCode>=65&&targetCode<=90
var isAlphanum = isUpper || targetCode>=97&&targetCode<=122 || targetCode>=48&&targetCode<=57
var isBeginning = isUpper && !wasUpper || !wasAlphanum || !isAlphanum
wasUpper = isUpper
wasAlphanum = isAlphanum
if(isBeginning) beginningIndexes[beginningIndexesLen++] = i
}
return beginningIndexes
}
var prepareNextBeginningIndexes = (target) => {
target = remove_accents(target)
var targetLen = target.length
var beginningIndexes = prepareBeginningIndexes(target)
var nextBeginningIndexes = [] // new Array(targetLen) sparse array is too slow
var lastIsBeginning = beginningIndexes[0]
var lastIsBeginningI = 0
for(var i = 0; i < targetLen; ++i) {
if(lastIsBeginning > i) {
nextBeginningIndexes[i] = lastIsBeginning
} else {
lastIsBeginning = beginningIndexes[++lastIsBeginningI]
nextBeginningIndexes[i] = lastIsBeginning===undefined ? targetLen : lastIsBeginning
}
}
return nextBeginningIndexes
}
var preparedCache = new Map()
var preparedSearchCache = new Map()
// the theory behind these being globals is to reduce garbage collection by not making new arrays
var matchesSimple = []; var matchesStrict = []
var nextBeginningIndexesChanges = [] // allows straw berry to match strawberry well, by modifying the end of a substring to be considered a beginning index for the rest of the search
var keysSpacesBestScores = []; var allowPartialMatchScores = []
var tmpTargets = []; var tmpResults = []
// prop = 'key' 2.5ms optimized for this case, seems to be about as fast as direct obj[prop]
// prop = 'key1.key2' 10ms
// prop = ['key1', 'key2'] 27ms
// prop = obj => obj.tags.join() ??ms
var getValue = (obj, prop) => {
var tmp = obj[prop]; if(tmp !== undefined) return tmp
if(typeof prop === 'function') return prop(obj) // this should run first. but that makes string props slower
var segs = prop
if(!Array.isArray(prop)) segs = prop.split('.')
var len = segs.length
var i = -1
while (obj && (++i < len)) obj = obj[segs[i]]
return obj
}
var isPrepared = (x) => { return typeof x === 'object' && typeof x._bitflags === 'number' }
var INFINITY = Infinity; var NEGATIVE_INFINITY = -INFINITY
var noResults = []; noResults.total = 0
var NULL = null
var noTarget = prepare('')
// Hacked version of https://github.com/lemire/FastPriorityQueue.js
var fastpriorityqueue=r=>{var e=[],o=0,a={},v=r=>{for(var a=0,v=e[a],c=1;c<o;){var s=c+1;a=c,s<o&&e[s]._score<e[c]._score&&(a=s),e[a-1>>1]=e[a],c=1+(a<<1)}for(var f=a-1>>1;a>0&&v._score<e[f]._score;f=(a=f)-1>>1)e[a]=e[f];e[a]=v};return a.add=(r=>{var a=o;e[o++]=r;for(var v=a-1>>1;a>0&&r._score<e[v]._score;v=(a=v)-1>>1)e[a]=e[v];e[a]=r}),a.poll=(r=>{if(0!==o){var a=e[0];return e[0]=e[--o],v(),a}}),a.peek=(r=>{if(0!==o)return e[0]}),a.replaceTop=(r=>{e[0]=r,v()}),a}
var q = fastpriorityqueue() // reuse this
+105
View File
@@ -0,0 +1,105 @@
// This exists only beacause I haven't been able to get linkColor to work with MarkdownText
// May not be necessary if that's possible tbh.
function markdownToHtml(text) {
if (!text) return "";
// Store code blocks and inline code to protect them from further processing
const codeBlocks = [];
const inlineCode = [];
let blockIndex = 0;
let inlineIndex = 0;
// First, extract and replace code blocks with placeholders
let html = text.replace(/```([\s\S]*?)```/g, (match, code) => {
// Trim leading and trailing blank lines only
const trimmedCode = code.replace(/^\n+|\n+$/g, '');
// Escape HTML entities in code
const escapedCode = trimmedCode.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
codeBlocks.push(`<pre><code>${escapedCode}</code></pre>`);
return `\x00CODEBLOCK${blockIndex++}\x00`;
});
// Extract and replace inline code
html = html.replace(/`([^`]+)`/g, (match, code) => {
// Escape HTML entities in code
const escapedCode = code.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
inlineCode.push(`<code>${escapedCode}</code>`);
return `\x00INLINECODE${inlineIndex++}\x00`;
});
// Now process everything else
// Escape HTML entities (but not in code blocks)
html = html.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
// Headers
html = html.replace(/^### (.*?)$/gm, '<h3>$1</h3>');
html = html.replace(/^## (.*?)$/gm, '<h2>$1</h2>');
html = html.replace(/^# (.*?)$/gm, '<h1>$1</h1>');
// Bold and italic (order matters!)
html = html.replace(/\*\*\*(.*?)\*\*\*/g, '<b><i>$1</i></b>');
html = html.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>');
html = html.replace(/\*(.*?)\*/g, '<i>$1</i>');
html = html.replace(/___(.*?)___/g, '<b><i>$1</i></b>');
html = html.replace(/__(.*?)__/g, '<b>$1</b>');
html = html.replace(/_(.*?)_/g, '<i>$1</i>');
// Links
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
// Lists
html = html.replace(/^\* (.*?)$/gm, '<li>$1</li>');
html = html.replace(/^- (.*?)$/gm, '<li>$1</li>');
html = html.replace(/^\d+\. (.*?)$/gm, '<li>$1</li>');
// Wrap consecutive list items in ul/ol tags
html = html.replace(/(<li>[\s\S]*?<\/li>\s*)+/g, function(match) {
return '<ul>' + match + '</ul>';
});
// Detect plain URLs and wrap them in anchor tags (but not inside existing <a> or markdown links)
html = html.replace(/(^|[^"'>])((https?|file):\/\/[^\s<]+)/g, '$1<a href="$2">$2</a>');
// Restore code blocks and inline code BEFORE line break processing
html = html.replace(/\x00CODEBLOCK(\d+)\x00/g, (match, index) => {
return codeBlocks[parseInt(index)];
});
html = html.replace(/\x00INLINECODE(\d+)\x00/g, (match, index) => {
return inlineCode[parseInt(index)];
});
// Line breaks (after code blocks are restored)
html = html.replace(/\n\n/g, '</p><p>');
html = html.replace(/\n/g, '<br/>');
// Wrap in paragraph tags if not already wrapped
if (!html.startsWith('<')) {
html = '<p>' + html + '</p>';
}
// Clean up the final HTML
// Remove <br/> tags immediately before block elements
html = html.replace(/<br\/>\s*<pre>/g, '<pre>');
html = html.replace(/<br\/>\s*<ul>/g, '<ul>');
html = html.replace(/<br\/>\s*<h[1-6]>/g, '<h$1>');
// Remove empty paragraphs
html = html.replace(/<p>\s*<\/p>/g, '');
html = html.replace(/<p>\s*<br\/>\s*<\/p>/g, '');
// Remove excessive line breaks
html = html.replace(/(<br\/>){3,}/g, '<br/><br/>'); // Max 2 consecutive line breaks
html = html.replace(/(<\/p>)\s*(<p>)/g, '$1$2'); // Remove whitespace between paragraphs
// Remove leading/trailing whitespace
html = html.trim();
return html;
}
+674
View File
@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
+669
View File
@@ -0,0 +1,669 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Services
import qs.Widgets
DankModal {
id: clipboardHistoryModal
property bool isVisible: false
property int totalCount: 0
property var activeTheme: Theme
property bool showClearConfirmation: false
property var clipboardEntries: []
property string searchText: ""
function updateFilteredModel() {
filteredClipboardModel.clear();
for (let i = 0; i < clipboardModel.count; i++) {
const entry = clipboardModel.get(i).entry;
if (searchText.trim().length === 0) {
filteredClipboardModel.append({
"entry": entry
});
} else {
const content = getEntryPreview(entry).toLowerCase();
if (content.includes(searchText.toLowerCase()))
filteredClipboardModel.append({
"entry": entry
});
}
}
clipboardHistoryModal.totalCount = filteredClipboardModel.count;
}
function toggle() {
if (isVisible)
hide();
else
show();
}
function show() {
clipboardHistoryModal.isVisible = true;
initializeThumbnailSystem();
refreshClipboard();
}
function hide() {
clipboardHistoryModal.isVisible = false;
clipboardHistoryModal.searchText = "";
cleanupTempFiles();
}
function initializeThumbnailSystem() {
// No initialization needed - using direct image display
}
function cleanupTempFiles() {
Quickshell.execDetached(["sh", "-c", "rm -f /tmp/clipboard_*.png"]);
}
function generateThumbnails() {
// No thumbnail generation needed - using direct image display
}
function refreshClipboard() {
clipboardProcess.running = true;
}
function copyEntry(entry) {
const entryId = entry.split('\t')[0];
Quickshell.execDetached(["sh", "-c", `cliphist decode ${entryId} | wl-copy`]);
ToastService.showInfo("Copied to clipboard");
clipboardHistoryModal.hide();
}
function deleteEntry(entry) {
deleteProcess.command = ["sh", "-c", `echo '${entry.replace(/'/g, "'\\''")}' | cliphist delete`];
deleteProcess.running = true;
}
function clearAll() {
clearProcess.running = true;
}
function getEntryPreview(entry) {
let content = entry.replace(/^\s*\d+\s+/, "");
if (content.includes("image/") || content.includes("binary data") || /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(content)) {
const dimensionMatch = content.match(/(\d+)x(\d+)/);
if (dimensionMatch)
return `Image ${dimensionMatch[1]}×${dimensionMatch[2]}`;
const typeMatch = content.match(/\b(png|jpg|jpeg|gif|bmp|webp)\b/i);
if (typeMatch)
return `Image (${typeMatch[1].toUpperCase()})`;
return "Image";
}
if (content.length > 100)
return content.substring(0, 100) + "...";
return content;
}
function getEntryType(entry) {
if (entry.includes("image/") || entry.includes("binary data") || /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(entry) || /\b(png|jpg|jpeg|gif|bmp|webp)\b/i.test(entry))
return "image";
if (entry.length > 200)
return "long_text";
return "text";
}
visible: isVisible
width: 650
height: 550
keyboardFocus: "ondemand"
backgroundColor: Theme.popupBackground()
cornerRadius: Theme.cornerRadiusLarge
borderColor: Theme.outlineMedium
borderWidth: 1
enableShadow: true
onBackgroundClicked: {
hide();
}
DankModal {
id: clearConfirmDialog
visible: showClearConfirmation
width: 350
height: 150
keyboardFocus: "ondemand"
onBackgroundClicked: {
showClearConfirmation = false;
}
content: Component {
Item {
anchors.fill: parent
Column {
anchors.centerIn: parent
width: parent.width - Theme.spacingM * 2
spacing: Theme.spacingM
StyledText {
text: "Clear All History?"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: "This will permanently delete all clipboard history."
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
wrapMode: Text.WordWrap
width: parent.width
horizontalAlignment: Text.AlignHCenter
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
Rectangle {
width: 100
height: 40
radius: Theme.cornerRadius
color: cancelClearButton.containsMouse ? Theme.surfaceTextPressed : Theme.surfaceVariantAlpha
StyledText {
text: "Cancel"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: cancelClearButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: showClearConfirmation = false
}
}
Rectangle {
width: 100
height: 40
radius: Theme.cornerRadius
color: confirmClearButton.containsMouse ? Theme.errorPressed : Theme.error
StyledText {
text: "Clear All"
font.pixelSize: Theme.fontSizeMedium
color: Theme.primaryText
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: confirmClearButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
clearAll();
showClearConfirmation = false;
hide();
}
}
}
}
}
}
}
}
ListModel {
id: clipboardModel
}
ListModel {
id: filteredClipboardModel
}
Process {
id: clipboardProcess
command: ["cliphist", "list"]
running: false
stdout: StdioCollector {
onStreamFinished: {
clipboardModel.clear();
const lines = text.trim().split('\n');
for (const line of lines) {
if (line.trim().length > 0)
clipboardModel.append({
"entry": line
});
}
updateFilteredModel();
}
}
}
Process {
id: deleteProcess
running: false
onExited: (exitCode) => {
if (exitCode === 0)
refreshClipboard();
else
console.warn("Failed to delete clipboard entry");
}
}
Process {
id: clearProcess
command: ["cliphist", "wipe"]
running: false
onExited: (exitCode) => {
if (exitCode === 0) {
clipboardModel.clear();
filteredClipboardModel.clear();
totalCount = 0;
} else {
}
}
}
IpcHandler {
function open() {
clipboardHistoryModal.show();
return "CLIPBOARD_OPEN_SUCCESS";
}
function close() {
clipboardHistoryModal.hide();
return "CLIPBOARD_CLOSE_SUCCESS";
}
function toggle() {
clipboardHistoryModal.toggle();
return "CLIPBOARD_TOGGLE_SUCCESS";
}
target: "clipboard"
}
content: Component {
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
Item {
width: parent.width
height: 40
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "content_paste"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: `Clipboard History (${totalCount})`
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankActionButton {
iconName: "delete_sweep"
iconSize: Theme.iconSize
iconColor: Theme.error
hoverColor: Theme.errorHover
onClicked: {
showClearConfirmation = true;
}
}
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
hoverColor: Theme.errorHover
onClicked: hide()
}
}
}
DankTextField {
id: searchField
width: parent.width
placeholderText: "Search clipboard history..."
leftIconName: "search"
showClearButton: true
onTextChanged: {
clipboardHistoryModal.searchText = text;
updateFilteredModel();
}
Connections {
function onOpened() {
searchField.forceActiveFocus();
}
function onDialogClosed() {
searchField.clearFocus();
}
target: clipboardHistoryModal
}
}
Rectangle {
width: parent.width
height: parent.height - 110
radius: Theme.cornerRadiusLarge
color: Theme.surfaceLight
border.color: Theme.outlineLight
border.width: 1
clip: true
ListView {
id: clipboardListView
property real wheelMultiplier: 1.8
property int wheelBaseStep: 160
anchors.fill: parent
anchors.margins: Theme.spacingS
clip: true
model: filteredClipboardModel
spacing: Theme.spacingXS
WheelHandler {
target: null
onWheel: (ev) => {
let dy = ev.pixelDelta.y !== 0 ? ev.pixelDelta.y : (ev.angleDelta.y / 120) * clipboardListView.wheelBaseStep;
if (ev.inverted)
dy = -dy;
const maxY = Math.max(0, clipboardListView.contentHeight - clipboardListView.height);
clipboardListView.contentY = Math.max(0, Math.min(maxY, clipboardListView.contentY - dy * clipboardListView.wheelMultiplier));
ev.accepted = true;
}
}
StyledText {
text: "No clipboard entries found"
anchors.centerIn: parent
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
visible: filteredClipboardModel.count === 0
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
ScrollBar.horizontal: ScrollBar {
policy: ScrollBar.AlwaysOff
}
delegate: Rectangle {
property string entryType: getEntryType(model.entry)
property string entryPreview: getEntryPreview(model.entry)
property int entryIndex: index + 1
property string entryData: model.entry
property alias thumbnailImageSource: thumbnailImageSource
width: clipboardListView.width
height: Math.max(entryType === "image" ? 72 : 60, contentText.contentHeight + Theme.spacingL)
radius: Theme.cornerRadius
color: mouseArea.containsMouse ? Theme.primaryHover : Theme.primaryBackground
border.color: Theme.outlineStrong
border.width: 1
Row {
anchors.fill: parent
anchors.margins: Theme.spacingM
anchors.rightMargin: Theme.spacingS // Reduced right margin
spacing: Theme.spacingL
Rectangle {
width: 24
height: 24
radius: 12
color: Theme.primarySelected
anchors.verticalCenter: parent.verticalCenter
StyledText {
anchors.centerIn: parent
text: entryIndex.toString()
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.primary
}
}
Row {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 68 // Account for index (24) + spacing (16) + delete button (32) - small margin
spacing: Theme.spacingM
Item {
width: entryType === "image" ? 48 : Theme.iconSize
height: entryType === "image" ? 48 : Theme.iconSize
anchors.verticalCenter: parent.verticalCenter
CachingImage {
id: thumbnailImageSource
anchors.fill: parent
property string entryId: model.entry.split('\t')[0]
source: entryType === "image" && imageLoader.imageData ? `data:image/png;base64,${imageLoader.imageData}` : ""
fillMode: Image.PreserveAspectCrop
smooth: true
cache: true
visible: false // Hide the original image
asynchronous: true
Process {
id: imageLoader
property string imageData: ""
command: ["sh", "-c", `cliphist decode ${thumbnailImageSource.entryId} | base64 -w 0`]
running: entryType === "image"
stdout: StdioCollector {
onStreamFinished: {
imageLoader.imageData = text.trim();
}
}
}
}
MultiEffect {
anchors.fill: parent
anchors.margins: 2
source: thumbnailImageSource
maskEnabled: true
maskSource: clipboardCircularMask
visible: entryType === "image" && thumbnailImageSource.status === Image.Ready
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
Item {
id: clipboardCircularMask
width: 48 - 4
height: 48 - 4
layer.enabled: true
layer.smooth: true
visible: false
Rectangle {
anchors.fill: parent
radius: width / 2
color: "black"
antialiasing: true
}
}
DankIcon {
visible: !(entryType === "image" && thumbnailImageSource.status === Image.Ready)
name: {
if (entryType === "image")
return "image";
if (entryType === "long_text")
return "subject";
return "content_copy";
}
size: Theme.iconSize
color: Theme.primary
anchors.centerIn: parent
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - (entryType === "image" ? 48 : Theme.iconSize) - Theme.spacingM
spacing: Theme.spacingXS
StyledText {
text: {
switch (entryType) {
case "image":
return "Image • " + entryPreview;
case "long_text":
return "Long Text";
default:
return "Text";
}
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
font.weight: Font.Medium
width: parent.width
elide: Text.ElideRight
}
StyledText {
id: contentText
text: entryPreview
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
width: parent.width
wrapMode: Text.WordWrap
maximumLineCount: entryType === "long_text" ? 3 : 1
elide: Text.ElideRight
}
}
}
}
DankActionButton {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
iconName: "close"
iconSize: Theme.iconSize - 6
iconColor: Theme.error
hoverColor: Theme.errorHover
onClicked: {
deleteEntry(model.entry);
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
anchors.rightMargin: 40 // Enough space to avoid delete button (32 + 8 margin)
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: copyEntry(model.entry)
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
}
}
}
}
}
+199
View File
@@ -0,0 +1,199 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Wayland
import qs.Common
PanelWindow {
id: root
property alias content: contentLoader.sourceComponent
property real width: 400
property real height: 300
readonly property real screenWidth: screen ? screen.width : 1920
readonly property real screenHeight: screen ? screen.height : 1080
property bool showBackground: true
property real backgroundOpacity: 0.5
property string positioning: "center"
property point customPosition: Qt.point(0, 0)
property string keyboardFocus: "ondemand"
property bool closeOnEscapeKey: true
property bool closeOnBackgroundClick: true
property string animationType: "scale"
property int animationDuration: Theme.mediumDuration
property var animationEasing: Theme.emphasizedEasing
property color backgroundColor: Theme.surfaceContainer
property color borderColor: Theme.outlineMedium
property real borderWidth: 1
property real cornerRadius: Theme.cornerRadiusLarge
property bool enableShadow: false
signal opened()
signal dialogClosed()
signal backgroundClicked()
function open() {
visible = true;
}
function close() {
visible = false;
}
function toggle() {
visible = !visible;
}
color: "transparent"
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: {
switch (root.keyboardFocus) {
case "exclusive":
return WlrKeyboardFocus.Exclusive;
case "none":
return WlrKeyboardFocus.None;
default:
return WlrKeyboardFocus.OnDemand;
}
}
onVisibleChanged: {
if (root.visible) {
opened();
} else {
if (Qt.inputMethod) {
Qt.inputMethod.hide();
Qt.inputMethod.reset();
}
dialogClosed();
}
}
anchors {
top: true
left: true
right: true
bottom: true
}
Rectangle {
id: background
anchors.fill: parent
color: "black"
opacity: root.showBackground ? (root.visible ? root.backgroundOpacity : 0) : 0
visible: root.showBackground
MouseArea {
anchors.fill: parent
enabled: root.closeOnBackgroundClick
onClicked: (mouse) => {
var localPos = mapToItem(contentContainer, mouse.x, mouse.y);
if (localPos.x < 0 || localPos.x > contentContainer.width || localPos.y < 0 || localPos.y > contentContainer.height)
root.backgroundClicked();
}
}
Behavior on opacity {
NumberAnimation {
duration: root.animationDuration
easing.type: root.animationEasing
}
}
}
Rectangle {
id: contentContainer
width: root.width
height: root.height
anchors.centerIn: positioning === "center" ? parent : undefined
x: {
if (positioning === "top-right")
return Math.max(Theme.spacingL, root.screenWidth - width - Theme.spacingL);
else if (positioning === "custom")
return root.customPosition.x;
return 0; // Will be overridden by anchors.centerIn when positioning === "center"
}
y: {
if (positioning === "top-right")
return Theme.barHeight + Theme.spacingXS;
else if (positioning === "custom")
return root.customPosition.y;
return 0; // Will be overridden by anchors.centerIn when positioning === "center"
}
color: root.backgroundColor
radius: root.cornerRadius
border.color: root.borderColor
border.width: root.borderWidth
layer.enabled: root.enableShadow
opacity: root.visible ? 1 : 0
scale: {
if (root.animationType === "scale")
return root.visible ? 1 : 0.9;
return 1;
}
transform: root.animationType === "slide" ? slideTransform : null
Translate {
id: slideTransform
x: root.visible ? 0 : 15
y: root.visible ? 0 : -30
}
Loader {
id: contentLoader
anchors.fill: parent
active: true
asynchronous: false
}
Behavior on opacity {
NumberAnimation {
duration: root.animationDuration
easing.type: root.animationEasing
}
}
Behavior on scale {
enabled: root.animationType === "scale"
NumberAnimation {
duration: root.animationDuration
easing.type: root.animationEasing
}
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 8
shadowBlur: 1
shadowColor: Theme.shadowStrong
shadowOpacity: 0.3
}
}
FocusScope {
id: focusScope
anchors.fill: parent
visible: root.visible // Only active when the modal is visible
Keys.onEscapePressed: (event) => {
if (root.closeOnEscapeKey) {
root.visible = false;
event.accepted = true;
}
}
}
}
+306
View File
@@ -0,0 +1,306 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtCore
import Qt.labs.folderlistmodel
import Quickshell.Io
import qs.Common
import qs.Widgets
DankModal {
id: fileBrowserModal
signal fileSelected(string path)
property string homeDir: StandardPaths.writableLocation(StandardPaths.HomeLocation)
property string currentPath: ""
property var fileExtensions: ["*.*"]
property string browserTitle: "Select File"
property string browserIcon: "folder_open"
property string browserType: "generic" // "wallpaper" or "profile" for last path memory
FolderListModel {
id: folderModel
showDirsFirst: true
showDotAndDotDot: false
showHidden: false
nameFilters: fileExtensions
showFiles: true
showDirs: true
folder: currentPath ? "file://" + currentPath : "file://" + homeDir
}
function isImageFile(fileName) {
if (!fileName) return false
var ext = fileName.toLowerCase().split('.').pop()
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext)
}
function getLastPath() {
var lastPath = "";
if (browserType === "wallpaper") {
lastPath = Prefs.wallpaperLastPath;
} else if (browserType === "profile") {
lastPath = Prefs.profileLastPath;
}
if (lastPath && lastPath !== "") {
return lastPath;
}
return homeDir;
}
function saveLastPath(path) {
if (browserType === "wallpaper") {
Prefs.wallpaperLastPath = path;
} else if (browserType === "profile") {
Prefs.profileLastPath = path;
}
Prefs.saveSettings();
}
Component.onCompleted: {
currentPath = getLastPath();
}
width: 800
height: 600
keyboardFocus: "ondemand"
enableShadow: true
visible: false
onBackgroundClicked: visible = false
onVisibleChanged: {
if (visible) {
var startPath = getLastPath();
currentPath = startPath;
}
}
onCurrentPathChanged: {
}
function navigateUp() {
var path = currentPath;
if (path === homeDir) {
return;
}
var lastSlash = path.lastIndexOf('/');
if (lastSlash > 0) {
var newPath = path.substring(0, lastSlash);
if (newPath.length < homeDir.length) {
currentPath = homeDir;
saveLastPath(homeDir);
} else {
currentPath = newPath;
saveLastPath(newPath);
}
}
}
function navigateTo(path) {
currentPath = path;
saveLastPath(path); // Save the path when navigating
}
content: Component {
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
Item {
width: parent.width
height: 40
Row {
spacing: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
DankIcon {
name: browserIcon
size: Theme.iconSizeLarge
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: browserTitle
font.pixelSize: Theme.fontSizeXLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
DankActionButton {
circular: false
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
hoverColor: Theme.errorHover
onClicked: fileBrowserModal.visible = false
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
width: parent.width
spacing: Theme.spacingS
StyledRect {
width: 32
height: 32
radius: Theme.cornerRadius
color: mouseArea.containsMouse && currentPath !== homeDir ? Theme.surfaceVariant : "transparent"
opacity: currentPath !== homeDir ? 1.0 : 0.0
DankIcon {
anchors.centerIn: parent
name: "arrow_back"
size: Theme.iconSizeSmall
color: Theme.surfaceText
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: currentPath !== homeDir
cursorShape: currentPath !== homeDir ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: currentPath !== homeDir
onClicked: navigateUp()
}
}
StyledText {
text: fileBrowserModal.currentPath.replace("file://", "")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
width: parent.width - 40 - Theme.spacingS
elide: Text.ElideMiddle
anchors.verticalCenter: parent.verticalCenter
maximumLineCount: 1
wrapMode: Text.NoWrap
}
}
GridView {
id: fileGrid
width: parent.width
height: parent.height - 80
clip: true
cellWidth: 150
cellHeight: 130
cacheBuffer: 260
model: folderModel
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
ScrollBar.horizontal: ScrollBar { policy: ScrollBar.AlwaysOff }
property real wheelMultiplier: 1.8
property int wheelBaseStep: 160
WheelHandler {
target: null
onWheel: (ev) => {
let dy = ev.pixelDelta.y !== 0
? ev.pixelDelta.y
: (ev.angleDelta.y / 120) * fileGrid.wheelBaseStep;
if (ev.inverted) dy = -dy;
const maxY = Math.max(0, fileGrid.contentHeight - fileGrid.height);
fileGrid.contentY = Math.max(0, Math.min(maxY,
fileGrid.contentY - dy * fileGrid.wheelMultiplier));
ev.accepted = true;
}
}
delegate: StyledRect {
id: delegateRoot
required property bool fileIsDir
required property string filePath
required property string fileName
required property url fileURL
width: 140
height: 120
radius: Theme.cornerRadius
color: mouseArea.containsMouse ? Theme.surfaceVariant : "transparent"
border.color: Theme.outline
border.width: mouseArea.containsMouse ? 1 : 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
Item {
width: 80
height: 60
anchors.horizontalCenter: parent.horizontalCenter
CachingImage {
anchors.fill: parent
imagePath: !delegateRoot.fileIsDir ? delegateRoot.filePath : ""
fillMode: Image.PreserveAspectCrop
visible: !delegateRoot.fileIsDir && isImageFile(delegateRoot.fileName)
maxCacheSize: 80
}
DankIcon {
anchors.centerIn: parent
name: "description"
size: Theme.iconSizeLarge
color: Theme.primary
visible: !delegateRoot.fileIsDir && !isImageFile(delegateRoot.fileName)
}
DankIcon {
anchors.centerIn: parent
name: "folder"
size: Theme.iconSizeLarge
color: Theme.primary
visible: delegateRoot.fileIsDir
}
}
StyledText {
text: delegateRoot.fileName || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
width: 120
elide: Text.ElideMiddle
horizontalAlignment: Text.AlignHCenter
anchors.horizontalCenter: parent.horizontalCenter
maximumLineCount: 2
wrapMode: Text.WordWrap
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (delegateRoot.fileIsDir) {
navigateTo(delegateRoot.filePath);
} else {
fileSelected(delegateRoot.filePath);
}
}
}
}
}
}
}
}
+200
View File
@@ -0,0 +1,200 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
DankModal {
id: root
property bool networkInfoModalVisible: false
property string networkSSID: ""
property var networkData: null
property string networkDetails: ""
function showNetworkInfo(ssid, data) {
networkSSID = ssid;
networkData = data;
networkInfoModalVisible = true;
NetworkService.fetchNetworkInfo(ssid);
}
function hideDialog() {
networkInfoModalVisible = false;
networkSSID = "";
networkData = null;
networkDetails = "";
}
visible: networkInfoModalVisible
width: 600
height: 500
enableShadow: true
onBackgroundClicked: {
hideDialog();
}
onVisibleChanged: {
if (!visible) {
networkSSID = "";
networkData = null;
networkDetails = "";
}
}
content: Component {
Item {
anchors.fill: parent
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
Row {
width: parent.width
Column {
width: parent.width - 40
spacing: Theme.spacingXS
StyledText {
text: "Network Information"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: "Details for \"" + networkSSID + "\""
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium
width: parent.width
elide: Text.ElideRight
}
}
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
hoverColor: Theme.errorHover
onClicked: {
root.hideDialog();
}
}
}
Flickable {
property real wheelMultiplier: 1.8
property int wheelBaseStep: 160
width: parent.width
height: parent.height - 140
clip: true
contentWidth: width
contentHeight: detailsRect.height
WheelHandler {
target: null
onWheel: (ev) => {
let dy = ev.pixelDelta.y !== 0 ? ev.pixelDelta.y : (ev.angleDelta.y / 120) * parent.wheelBaseStep;
if (ev.inverted)
dy = -dy;
const maxY = Math.max(0, parent.contentHeight - parent.height);
parent.contentY = Math.max(0, Math.min(maxY, parent.contentY - dy * parent.wheelMultiplier));
ev.accepted = true;
}
}
Rectangle {
id: detailsRect
width: parent.width
height: Math.max(parent.parent.height, detailsText.contentHeight + Theme.spacingM * 2)
radius: Theme.cornerRadius
color: Theme.surfaceHover
border.color: Theme.outlineStrong
border.width: 1
StyledText {
id: detailsText
anchors.fill: parent
anchors.margins: Theme.spacingM
text: NetworkService.networkInfoDetails.replace(/\\n/g, '\n') || "No information available"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
wrapMode: Text.WordWrap
lineHeight: 1.5
}
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
ScrollBar.horizontal: ScrollBar {
policy: ScrollBar.AlwaysOff
}
}
Item {
width: parent.width
height: 40
Rectangle {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
width: Math.max(70, closeText.contentWidth + Theme.spacingM * 2)
height: 36
radius: Theme.cornerRadius
color: closeArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
StyledText {
id: closeText
anchors.centerIn: parent
text: "Close"
font.pixelSize: Theme.fontSizeMedium
color: Theme.background
font.weight: Font.Medium
}
MouseArea {
id: closeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.hideDialog();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
}
}
+168
View File
@@ -0,0 +1,168 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Widgets
DankModal {
id: root
property bool powerConfirmVisible: false
property string powerConfirmAction: ""
property string powerConfirmTitle: ""
property string powerConfirmMessage: ""
function executePowerAction(action) {
let command = [];
switch (action) {
case "logout":
command = ["niri", "msg", "action", "quit", "-s"];
break;
case "suspend":
command = ["systemctl", "suspend"];
break;
case "reboot":
command = ["systemctl", "reboot"];
break;
case "poweroff":
command = ["systemctl", "poweroff"];
break;
}
if (command.length > 0) {
Quickshell.execDetached(command);
}
}
visible: powerConfirmVisible
width: 350
height: 160
keyboardFocus: "ondemand"
enableShadow: false
onBackgroundClicked: {
powerConfirmVisible = false;
}
content: Component {
Item {
anchors.fill: parent
Column {
anchors.centerIn: parent
width: parent.width - Theme.spacingM * 2
spacing: Theme.spacingM
StyledText {
text: powerConfirmTitle
font.pixelSize: Theme.fontSizeLarge
color: {
switch (powerConfirmAction) {
case "poweroff":
return Theme.error;
case "reboot":
return Theme.warning;
default:
return Theme.surfaceText;
}
}
font.weight: Font.Medium
width: parent.width
horizontalAlignment: Text.AlignHCenter
}
StyledText {
text: powerConfirmMessage
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
width: parent.width
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
}
Item {
height: Theme.spacingS
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
Rectangle {
width: 120
height: 40
radius: Theme.cornerRadius
color: cancelButton.containsMouse ? Theme.surfaceTextPressed : Theme.surfaceVariantAlpha
StyledText {
text: "Cancel"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: cancelButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerConfirmVisible = false;
}
}
}
Rectangle {
width: 120
height: 40
radius: Theme.cornerRadius
color: {
let baseColor;
switch (powerConfirmAction) {
case "poweroff":
baseColor = Theme.error;
break;
case "reboot":
baseColor = Theme.warning;
break;
default:
baseColor = Theme.primary;
break;
}
return confirmButton.containsMouse ? Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 0.9) : baseColor;
}
StyledText {
text: "Confirm"
font.pixelSize: Theme.fontSizeMedium
color: Theme.primaryText
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: confirmButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerConfirmVisible = false;
executePowerAction(powerConfirmAction);
}
}
}
}
}
}
}
}
+317
View File
@@ -0,0 +1,317 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Modules.ProcessList
import qs.Services
import qs.Widgets
DankModal {
id: processListModal
property int currentTab: 0
property var tabNames: ["Processes", "Performance", "System"]
function show() {
processListModal.visible = true;
UserInfoService.getUptime();
}
function hide() {
processListModal.visible = false;
if (processContextMenu.visible)
processContextMenu.close();
}
function toggle() {
if (processListModal.visible)
hide();
else
show();
}
width: 900
height: 680
visible: false
keyboardFocus: "exclusive"
backgroundColor: Theme.popupBackground()
cornerRadius: Theme.cornerRadiusXLarge
enableShadow: true
onBackgroundClicked: hide()
Ref {
service: SysMonitorService
}
Component {
id: processesTabComponent
ProcessesTab {
contextMenu: processContextMenu
}
}
Component {
id: performanceTabComponent
PerformanceTab {
}
}
Component {
id: systemTabComponent
SystemTab {
}
}
ProcessContextMenu {
id: processContextMenu
}
content: Component {
Item {
anchors.fill: parent
focus: true
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Escape) {
processListModal.hide();
event.accepted = true;
} else if (event.key === Qt.Key_1) {
currentTab = 0;
event.accepted = true;
} else if (event.key === Qt.Key_2) {
currentTab = 1;
event.accepted = true;
} else if (event.key === Qt.Key_3) {
currentTab = 2;
event.accepted = true;
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
RowLayout {
Layout.fillWidth: true
height: 40
StyledText {
text: "System Monitor"
font.pixelSize: Theme.fontSizeLarge + 4
font.weight: Font.Bold
color: Theme.surfaceText
Layout.alignment: Qt.AlignVCenter
}
Item {
Layout.fillWidth: true
}
DankActionButton {
circular: false
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
hoverColor: Theme.errorHover
onClicked: processListModal.hide()
Layout.alignment: Qt.AlignVCenter
}
}
Rectangle {
Layout.fillWidth: true
height: 52
color: Theme.surfaceSelected
radius: Theme.cornerRadiusLarge
border.color: Theme.outlineLight
border.width: 1
Row {
anchors.fill: parent
anchors.margins: 4
spacing: 2
Repeater {
model: tabNames
Rectangle {
width: (parent.width - (tabNames.length - 1) * 2) / tabNames.length
height: 44
radius: Theme.cornerRadiusLarge
color: currentTab === index ? Theme.primaryPressed : (tabMouseArea.containsMouse ? Theme.primaryHoverLight : "transparent")
border.color: currentTab === index ? Theme.primary : "transparent"
border.width: currentTab === index ? 1 : 0
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: {
switch (index) {
case 0:
return "list_alt";
case 1:
return "analytics";
case 2:
return "settings";
default:
return "tab";
}
}
size: Theme.iconSize - 2
color: currentTab === index ? Theme.primary : Theme.surfaceText
opacity: currentTab === index ? 1 : 0.7
anchors.verticalCenter: parent.verticalCenter
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
StyledText {
text: modelData
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: currentTab === index ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: -1
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
}
MouseArea {
id: tabMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
currentTab = index;
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Theme.cornerRadiusLarge
color: Theme.surfaceLight
border.color: Theme.outlineLight
border.width: 1
Loader {
id: processesTab
anchors.fill: parent
anchors.margins: Theme.spacingS
active: currentTab === 0
visible: currentTab === 0
opacity: currentTab === 0 ? 1 : 0
sourceComponent: processesTabComponent
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
Loader {
id: performanceTab
anchors.fill: parent
anchors.margins: Theme.spacingS
active: currentTab === 1
visible: currentTab === 1
opacity: currentTab === 1 ? 1 : 0
sourceComponent: performanceTabComponent
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
Loader {
id: systemTab
anchors.fill: parent
anchors.margins: Theme.spacingS
active: currentTab === 2
visible: currentTab === 2
opacity: currentTab === 2 ? 1 : 0
sourceComponent: systemTabComponent
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
}
}
}
}
+183
View File
@@ -0,0 +1,183 @@
pragma ComponentBehavior: Bound
import Quickshell.Io
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Modules.Settings
import qs.Widgets
DankModal {
id: settingsModal
property bool settingsVisible: false
signal closingModal()
onVisibleChanged: {
if (!visible)
closingModal();
}
visible: settingsVisible
width: 750
height: 750
keyboardFocus: "ondemand"
onBackgroundClicked: {
settingsVisible = false;
}
FocusScope {
anchors.fill: parent
focus: settingsModal.settingsVisible
Keys.onEscapePressed: settingsModal.settingsVisible = false
}
content: Component {
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "settings"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Settings"
font.pixelSize: Theme.fontSizeXLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - 175 // Spacer to push close button to the right
height: 1
}
DankActionButton {
circular: false
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
hoverColor: Theme.errorHover
onClicked: settingsModal.settingsVisible = false
}
}
Column {
width: parent.width
height: parent.height - 50
spacing: 0
DankTabBar {
id: settingsTabBar
width: parent.width
model: [
{ text: "Personalization", icon: "person" },
{ text: "Time & Weather", icon: "schedule" },
{ text: "Widgets", icon: "widgets" },
{ text: "Launcher", icon: "apps" },
{ text: "Appearance", icon: "palette" }
]
}
Item {
width: parent.width
height: parent.height - settingsTabBar.height
Rectangle {
anchors.fill: parent
anchors.margins: Theme.spacingL
color: "transparent"
Loader {
anchors.fill: parent
active: settingsTabBar.currentIndex === 0
visible: active
asynchronous: false
sourceComponent: Component {
PersonalizationTab {}
}
}
Loader {
anchors.fill: parent
active: settingsTabBar.currentIndex === 1
visible: active
asynchronous: true
sourceComponent: Component {
TimeWeatherTab {}
}
}
Loader {
anchors.fill: parent
active: settingsTabBar.currentIndex === 2
visible: active
asynchronous: true
sourceComponent: Component {
WidgetsTab {}
}
}
Loader {
anchors.fill: parent
active: settingsTabBar.currentIndex === 3
visible: active
asynchronous: true
sourceComponent: Component {
LauncherTab {}
}
}
Loader {
anchors.fill: parent
active: settingsTabBar.currentIndex === 4
visible: active
asynchronous: false
sourceComponent: Component {
AppearanceTab {}
}
}
}
}
}
}
}
IpcHandler {
function open() {
settingsModal.settingsVisible = true;
return "SETTINGS_OPEN_SUCCESS";
}
function close() {
settingsModal.settingsVisible = false;
return "SETTINGS_CLOSE_SUCCESS";
}
function toggle() {
settingsModal.settingsVisible = !settingsModal.settingsVisible;
return "SETTINGS_TOGGLE_SUCCESS";
}
target: "settings"
}
}
+530
View File
@@ -0,0 +1,530 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Modules.AppDrawer
import qs.Services
import qs.Widgets
DankModal {
id: spotlightModal
property bool spotlightOpen: false
function show() {
spotlightOpen = true;
appLauncher.searchQuery = "";
}
function hide() {
spotlightOpen = false;
appLauncher.searchQuery = "";
appLauncher.selectedIndex = 0;
appLauncher.setCategory("All");
}
function toggle() {
if (spotlightOpen)
hide();
else
show();
}
visible: spotlightOpen
width: 550
height: 600
keyboardFocus: "ondemand"
backgroundColor: Theme.popupBackground()
cornerRadius: Theme.cornerRadiusXLarge
borderColor: Theme.outlineMedium
borderWidth: 1
enableShadow: true
onVisibleChanged: {
if (visible && !spotlightOpen)
show();
}
onBackgroundClicked: {
spotlightOpen = false;
}
Component.onCompleted: {
}
AppLauncher {
id: appLauncher
viewMode: Prefs.spotlightModalViewMode
gridColumns: 4
onAppLaunched: hide()
onViewModeSelected: function(mode) {
Prefs.setSpotlightModalViewMode(mode);
}
}
IpcHandler {
function open() {
spotlightModal.show();
return "SPOTLIGHT_OPEN_SUCCESS";
}
function close() {
spotlightModal.hide();
return "SPOTLIGHT_CLOSE_SUCCESS";
}
function toggle() {
spotlightModal.toggle();
return "SPOTLIGHT_TOGGLE_SUCCESS";
}
target: "spotlight"
}
content: Component {
Item {
id: spotlightKeyHandler
anchors.fill: parent
focus: true
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Escape) {
hide();
event.accepted = true;
} else if (event.key === Qt.Key_Down) {
appLauncher.selectNext();
event.accepted = true;
} else if (event.key === Qt.Key_Up) {
appLauncher.selectPrevious();
event.accepted = true;
} else if (event.key === Qt.Key_Right && appLauncher.viewMode === "grid") {
appLauncher.selectNextInRow();
event.accepted = true;
} else if (event.key === Qt.Key_Left && appLauncher.viewMode === "grid") {
appLauncher.selectPreviousInRow();
event.accepted = true;
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
appLauncher.launchSelected();
event.accepted = true;
} else if (!searchField.activeFocus && event.text && event.text.length > 0 && event.text.match(/[a-zA-Z0-9\\s]/)) {
searchField.forceActiveFocus();
searchField.insertText(event.text);
event.accepted = true;
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
Rectangle {
width: parent.width
height: categorySelector.height + Theme.spacingM * 2
radius: Theme.cornerRadiusLarge
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) => {
return appLauncher.setCategory(category);
}
}
}
Row {
width: parent.width
spacing: Theme.spacingM
DankTextField {
id: searchField
width: parent.width - 80 - Theme.spacingM // Leave space for view toggle buttons
height: 56
cornerRadius: Theme.cornerRadiusLarge
backgroundColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.7)
normalBorderColor: Theme.outlineMedium
focusedBorderColor: Theme.primary
leftIconName: "search"
leftIconSize: Theme.iconSize
leftIconColor: Theme.surfaceVariantText
leftIconFocusedColor: Theme.primary
showClearButton: true
textColor: Theme.surfaceText
font.pixelSize: Theme.fontSizeLarge
enabled: spotlightOpen
placeholderText: "Search applications..."
ignoreLeftRightKeys: true
keyForwardTargets: [spotlightKeyHandler]
text: appLauncher.searchQuery
onTextEdited: {
appLauncher.searchQuery = text;
}
Keys.onPressed: (event) => {
if (event.key === Qt.Key_Escape) {
hide();
event.accepted = true;
} else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length > 0) {
if (appLauncher.keyboardNavigationActive && appLauncher.model.count > 0)
appLauncher.launchSelected();
else if (appLauncher.model.count > 0)
appLauncher.launchApp(appLauncher.model.get(0));
event.accepted = true;
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up || event.key === Qt.Key_Left || event.key === Qt.Key_Right || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) {
event.accepted = false;
}
}
Connections {
function onOpened() {
searchField.forceActiveFocus();
}
function onDialogClosed() {
searchField.clearFocus();
}
target: spotlightModal
}
}
Row {
spacing: Theme.spacingXS
visible: appLauncher.model.count > 0
anchors.verticalCenter: parent.verticalCenter
Rectangle {
width: 36
height: 36
radius: Theme.cornerRadiusLarge
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
name: "view_list"
size: 18
color: appLauncher.viewMode === "list" ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: listViewArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
appLauncher.setViewMode("list");
}
}
}
Rectangle {
width: 36
height: 36
radius: Theme.cornerRadiusLarge
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
name: "grid_view"
size: 18
color: appLauncher.viewMode === "grid" ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: gridViewArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
appLauncher.setViewMode("grid");
}
}
}
}
}
Rectangle {
id: resultsContainer
width: parent.width
height: parent.height - y
radius: Theme.cornerRadiusLarge
color: Theme.surfaceLight
border.color: Theme.outlineLight
border.width: 1
DankListView {
id: resultsList
anchors.fill: parent
anchors.margins: Theme.spacingS
visible: appLauncher.viewMode === "list"
model: appLauncher.model
currentIndex: appLauncher.selectedIndex
itemHeight: 60
iconSize: 40
showDescription: true
hoverUpdatesSelection: false
keyboardNavigationActive: appLauncher.keyboardNavigationActive
onItemClicked: function(index, modelData) {
appLauncher.launchApp(modelData);
}
onItemHovered: function(index) {
appLauncher.selectedIndex = index;
}
onItemRightClicked: function(index, modelData, mouseX, mouseY) {
contextMenu.show(mouseX, mouseY, modelData)
}
onKeyboardNavigationReset: {
appLauncher.keyboardNavigationActive = false;
}
}
DankGridView {
id: resultsGrid
anchors.fill: parent
anchors.margins: Theme.spacingS
visible: appLauncher.viewMode === "grid"
model: appLauncher.model
columns: 4
adaptiveColumns: false
minCellWidth: 120
maxCellWidth: 160
iconSizeRatio: 0.55
maxIconSize: 48
currentIndex: appLauncher.selectedIndex
hoverUpdatesSelection: false
keyboardNavigationActive: appLauncher.keyboardNavigationActive
onItemClicked: function(index, modelData) {
appLauncher.launchApp(modelData);
}
onItemHovered: function(index) {
appLauncher.selectedIndex = index;
}
onItemRightClicked: function(index, modelData, mouseX, mouseY) {
contextMenu.show(mouseX, mouseY, modelData)
}
onKeyboardNavigationReset: {
appLauncher.keyboardNavigationActive = false;
}
}
}
}
}
}
Popup {
id: contextMenu
property var currentApp: null
function show(x, y, app) {
currentApp = app
if (!contextMenu.parent && typeof Overlay !== "undefined" && Overlay.overlay)
contextMenu.parent = Overlay.overlay;
const menuWidth = 180;
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2;
const screenWidth = Screen.width;
const screenHeight = Screen.height;
let finalX = x;
let finalY = y;
if (x + menuWidth > screenWidth - 20)
finalX = x - menuWidth;
if (y + menuHeight > screenHeight - 20)
finalY = y - menuHeight;
contextMenu.x = Math.max(20, finalX);
contextMenu.y = Math.max(20, finalY);
open();
}
width: 180
height: menuColumn.implicitHeight + Theme.spacingS * 2
padding: 0
modal: false
closePolicy: Popup.CloseOnEscape
onClosed: {
closePolicy = Popup.CloseOnEscape;
}
onOpened: {
outsideClickTimer.start();
}
Timer {
id: outsideClickTimer
interval: 100
onTriggered: {
contextMenu.closePolicy = Popup.CloseOnEscape | Popup.CloseOnPressOutside;
}
}
background: Rectangle {
color: "transparent"
}
contentItem: Rectangle {
color: Theme.popupBackground()
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
Column {
id: menuColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 1
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadiusSmall
color: pinMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: {
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry) return "push_pin"
var appId = contextMenu.currentApp.desktopEntry.id || contextMenu.currentApp.desktopEntry.execString || ""
return Prefs.isPinnedApp(appId) ? "keep_off" : "push_pin"
}
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: {
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry) return "Pin to Dock"
var appId = contextMenu.currentApp.desktopEntry.id || contextMenu.currentApp.desktopEntry.execString || ""
return Prefs.isPinnedApp(appId) ? "Unpin from Dock" : "Pin to Dock"
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: pinMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry) return
var appId = contextMenu.currentApp.desktopEntry.id || contextMenu.currentApp.desktopEntry.execString || ""
if (Prefs.isPinnedApp(appId)) {
Prefs.removePinnedApp(appId)
} else {
Prefs.addPinnedApp(appId)
}
contextMenu.close()
}
}
}
Rectangle {
width: parent.width - Theme.spacingS * 2
height: 5
anchors.horizontalCenter: parent.horizontalCenter
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
}
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadiusSmall
color: launchMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "launch"
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Launch"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: launchMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (contextMenu.currentApp) {
appLauncher.launchApp(contextMenu.currentApp)
}
contextMenu.close()
}
}
}
}
}
}
}
+270
View File
@@ -0,0 +1,270 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets
DankModal {
id: root
property bool wifiPasswordModalVisible: false
property string wifiPasswordSSID: ""
property string wifiPasswordInput: ""
visible: wifiPasswordModalVisible
width: 420
height: 230
keyboardFocus: "ondemand"
onVisibleChanged: {
if (!visible)
wifiPasswordInput = "";
}
onBackgroundClicked: {
wifiPasswordModalVisible = false;
wifiPasswordInput = "";
}
Connections {
function onPasswordDialogShouldReopenChanged() {
if (NetworkService.passwordDialogShouldReopen && NetworkService.connectingSSID !== "") {
wifiPasswordSSID = NetworkService.connectingSSID;
wifiPasswordInput = "";
wifiPasswordModalVisible = true;
NetworkService.passwordDialogShouldReopen = false;
}
}
target: NetworkService
}
content: Component {
Item {
anchors.fill: parent
Column {
anchors.centerIn: parent
width: parent.width - Theme.spacingM * 2
spacing: Theme.spacingM
Row {
width: parent.width
Column {
width: parent.width - 40
spacing: Theme.spacingXS
StyledText {
text: "Connect to Wi-Fi"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: "Enter password for \"" + wifiPasswordSSID + "\""
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium
width: parent.width
elide: Text.ElideRight
}
}
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
hoverColor: Theme.errorHover
onClicked: {
wifiPasswordModalVisible = false;
wifiPasswordInput = "";
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: Theme.surfaceHover
border.color: passwordInput.activeFocus ? Theme.primary : Theme.outlineStrong
border.width: passwordInput.activeFocus ? 2 : 1
DankTextField {
id: passwordInput
anchors.fill: parent
font.pixelSize: Theme.fontSizeMedium
textColor: Theme.surfaceText
text: wifiPasswordInput
echoMode: showPasswordCheckbox.checked ? TextInput.Normal : TextInput.Password
placeholderText: "Enter password"
backgroundColor: "transparent"
normalBorderColor: "transparent"
focusedBorderColor: "transparent"
onTextEdited: {
wifiPasswordInput = text;
}
onAccepted: {
NetworkService.connectToWifiWithPassword(wifiPasswordSSID, passwordInput.text);
wifiPasswordModalVisible = false;
wifiPasswordInput = "";
passwordInput.text = "";
}
Connections {
function onOpened() {
passwordInput.forceActiveFocus();
}
function onDialogClosed() {
passwordInput.clearFocus();
}
target: root
}
}
}
Row {
spacing: Theme.spacingS
Rectangle {
id: showPasswordCheckbox
property bool checked: false
width: 20
height: 20
radius: 4
color: checked ? Theme.primary : "transparent"
border.color: checked ? Theme.primary : Theme.outlineButton
border.width: 2
DankIcon {
anchors.centerIn: parent
name: "check"
size: 12
color: Theme.background
visible: parent.checked
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
showPasswordCheckbox.checked = !showPasswordCheckbox.checked;
}
}
}
StyledText {
text: "Show password"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Item {
width: parent.width
height: 40
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Rectangle {
width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2)
height: 36
radius: Theme.cornerRadius
color: cancelArea.containsMouse ? Theme.surfaceTextHover : "transparent"
border.color: Theme.surfaceVariantAlpha
border.width: 1
StyledText {
id: cancelText
anchors.centerIn: parent
text: "Cancel"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
MouseArea {
id: cancelArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
wifiPasswordModalVisible = false;
wifiPasswordInput = "";
}
}
}
Rectangle {
width: Math.max(80, connectText.contentWidth + Theme.spacingM * 2)
height: 36
radius: Theme.cornerRadius
color: connectArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
enabled: passwordInput.text.length > 0
opacity: enabled ? 1 : 0.5
StyledText {
id: connectText
anchors.centerIn: parent
text: "Connect"
font.pixelSize: Theme.fontSizeMedium
color: Theme.background
font.weight: Font.Medium
}
MouseArea {
id: connectArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: parent.enabled
onClicked: {
NetworkService.connectToWifiWithPassword(wifiPasswordSSID, passwordInput.text);
wifiPasswordModalVisible = false;
wifiPasswordInput = "";
passwordInput.text = "";
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
}
}
}
+595
View File
@@ -0,0 +1,595 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Modules.AppDrawer
import qs.Services
import qs.Widgets
PanelWindow {
id: appDrawerPopout
property bool isVisible: false
function show() {
appDrawerPopout.isVisible = true;
appLauncher.searchQuery = "";
}
function hide() {
appDrawerPopout.isVisible = false;
}
function toggle() {
if (appDrawerPopout.isVisible)
hide();
else
show();
}
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: isVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
WlrLayershell.namespace: "quickshell-launcher"
visible: isVisible
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
AppLauncher {
id: appLauncher
viewMode: Prefs.appLauncherViewMode
gridColumns: 4
onAppLaunched: appDrawerPopout.hide()
onViewModeSelected: function(mode) {
Prefs.setAppLauncherViewMode(mode);
}
}
MouseArea {
anchors.fill: parent
enabled: appDrawerPopout.isVisible
onClicked: function(mouse) {
var localPos = mapToItem(launcherLoader, mouse.x, mouse.y);
if (localPos.x < 0 || localPos.x > launcherLoader.width || localPos.y < 0 || localPos.y > launcherLoader.height)
appDrawerPopout.hide();
}
}
Loader {
id: launcherLoader
asynchronous: true
active: appDrawerPopout.isVisible
width: 520
height: 600
x: Theme.spacingL
y: Theme.barHeight + Theme.spacingXS
opacity: appDrawerPopout.isVisible ? 1 : 0
scale: appDrawerPopout.isVisible ? 1 : 0.9
Behavior on opacity {
NumberAnimation {
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
Behavior on scale {
NumberAnimation {
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
sourceComponent: Rectangle {
id: launcherPanel
color: Theme.popupBackground()
radius: Theme.cornerRadiusXLarge
antialiasing: true
smooth: true
Rectangle {
anchors.fill: parent
anchors.margins: -3
color: "transparent"
radius: parent.radius + 3
border.color: Qt.rgba(0, 0, 0, 0.05)
border.width: 1
z: -3
}
Rectangle {
anchors.fill: parent
anchors.margins: -2
color: "transparent"
radius: parent.radius + 2
border.color: Qt.rgba(0, 0, 0, 0.08)
border.width: 1
z: -2
}
Rectangle {
anchors.fill: parent
color: "transparent"
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 1
radius: parent.radius
z: -1
}
Item {
id: keyHandler
anchors.fill: parent
focus: true
Component.onCompleted: {
if (appDrawerPopout.isVisible)
forceActiveFocus();
}
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Escape) {
appDrawerPopout.hide();
event.accepted = true;
} else if (event.key === Qt.Key_Down) {
appLauncher.selectNext();
event.accepted = true;
} else if (event.key === Qt.Key_Up) {
appLauncher.selectPrevious();
event.accepted = true;
} else if (event.key === Qt.Key_Right && appLauncher.viewMode === "grid") {
appLauncher.selectNextInRow();
event.accepted = true;
} else if (event.key === Qt.Key_Left && appLauncher.viewMode === "grid") {
appLauncher.selectPreviousInRow();
event.accepted = true;
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
appLauncher.launchSelected();
event.accepted = true;
} else if (!searchField.activeFocus && event.text && event.text.length > 0 && event.text.match(/[a-zA-Z0-9\\s]/)) {
searchField.forceActiveFocus();
searchField.insertText(event.text);
event.accepted = true;
}
}
Column {
width: parent.width - Theme.spacingL * 2
height: parent.height - Theme.spacingL * 2
x: Theme.spacingL
y: Theme.spacingL
spacing: Theme.spacingL
Row {
width: parent.width
height: 40
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: "Applications"
font.pixelSize: Theme.fontSizeLarge + 4
font.weight: Font.Bold
color: Theme.surfaceText
}
Item {
width: parent.width - 200
height: 1
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: appLauncher.model.count + " apps"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
}
}
DankTextField {
id: searchField
width: parent.width
height: 52
cornerRadius: Theme.cornerRadiusLarge
backgroundColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.7)
normalBorderColor: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
focusedBorderColor: Theme.primary
leftIconName: "search"
leftIconSize: Theme.iconSize
leftIconColor: Theme.surfaceVariantText
leftIconFocusedColor: Theme.primary
showClearButton: true
font.pixelSize: Theme.fontSizeLarge
enabled: appDrawerPopout.isVisible
placeholderText: "Search applications..."
ignoreLeftRightKeys: true
keyForwardTargets: [keyHandler]
onTextEdited: {
appLauncher.searchQuery = text;
}
Keys.onPressed: function(event) {
if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length > 0) {
if (appLauncher.keyboardNavigationActive && appLauncher.model.count > 0) {
appLauncher.launchSelected();
} else if (appLauncher.model.count > 0) {
var firstApp = appLauncher.model.get(0);
appLauncher.launchApp(firstApp);
}
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)) {
event.accepted = false;
}
}
Component.onCompleted: {
if (appDrawerPopout.isVisible)
searchField.forceActiveFocus();
}
Connections {
function onIsVisibleChanged() {
if (appDrawerPopout.isVisible)
Qt.callLater(function() {
searchField.forceActiveFocus();
});
else
searchField.clearFocus();
}
target: appDrawerPopout
}
}
Row {
width: parent.width
height: 40
spacing: Theme.spacingM
visible: searchField.text.length === 0
Item {
width: 200
height: 36
DankDropdown {
anchors.fill: parent
text: ""
currentValue: appLauncher.selectedCategory
options: appLauncher.categories
optionIcons: appLauncher.categoryIcons
onValueChanged: function(value) {
appLauncher.setCategory(value);
}
}
}
Item {
width: parent.width - 300
height: 1
}
Row {
spacing: 4
anchors.verticalCenter: parent.verticalCenter
DankActionButton {
buttonSize: 36
circular: false
iconName: "view_list"
iconSize: 20
iconColor: appLauncher.viewMode === "list" ? Theme.primary : Theme.surfaceText
hoverColor: appLauncher.viewMode === "list" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
backgroundColor: appLauncher.viewMode === "list" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
onClicked: {
appLauncher.setViewMode("list");
}
}
DankActionButton {
buttonSize: 36
circular: false
iconName: "grid_view"
iconSize: 20
iconColor: appLauncher.viewMode === "grid" ? Theme.primary : Theme.surfaceText
hoverColor: appLauncher.viewMode === "grid" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
backgroundColor: appLauncher.viewMode === "grid" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
onClicked: {
appLauncher.setViewMode("grid");
}
}
}
}
Rectangle {
width: parent.width
height: {
let usedHeight = 40 + Theme.spacingL;
usedHeight += 52 + Theme.spacingL;
usedHeight += (searchField.text.length === 0 ? 40 + Theme.spacingL : 0);
return parent.height - usedHeight;
}
radius: Theme.cornerRadiusLarge
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
DankListView {
id: appList
anchors.fill: parent
anchors.margins: Theme.spacingS
visible: appLauncher.viewMode === "list"
model: appLauncher.model
currentIndex: appLauncher.selectedIndex
itemHeight: 72
iconSize: 56
showDescription: true
hoverUpdatesSelection: false
keyboardNavigationActive: appLauncher.keyboardNavigationActive
onItemClicked: function(index, modelData) {
appLauncher.launchApp(modelData);
}
onItemHovered: function(index) {
appLauncher.selectedIndex = index;
}
onItemRightClicked: function(index, modelData, mouseX, mouseY) {
contextMenu.show(mouseX, mouseY, modelData);
}
onKeyboardNavigationReset: {
appLauncher.keyboardNavigationActive = false;
}
}
DankGridView {
id: appGrid
anchors.fill: parent
anchors.margins: Theme.spacingS
visible: appLauncher.viewMode === "grid"
model: appLauncher.model
columns: 4
adaptiveColumns: false
currentIndex: appLauncher.selectedIndex
hoverUpdatesSelection: false
keyboardNavigationActive: appLauncher.keyboardNavigationActive
onItemClicked: function(index, modelData) {
appLauncher.launchApp(modelData);
}
onItemHovered: function(index) {
appLauncher.selectedIndex = index;
}
onItemRightClicked: function(index, modelData, mouseX, mouseY) {
contextMenu.show(mouseX, mouseY, modelData);
}
onKeyboardNavigationReset: {
appLauncher.keyboardNavigationActive = false;
}
}
}
}
}
}
}
Popup {
id: contextMenu
property var currentApp: null
function show(x, y, app) {
currentApp = app;
if (!contextMenu.parent && typeof Overlay !== "undefined" && Overlay.overlay)
contextMenu.parent = Overlay.overlay;
const menuWidth = 180;
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2;
const screenWidth = Screen.width;
const screenHeight = Screen.height;
let finalX = x;
let finalY = y;
if (x + menuWidth > screenWidth - 20)
finalX = x - menuWidth;
if (y + menuHeight > screenHeight - 20)
finalY = y - menuHeight;
contextMenu.x = Math.max(20, finalX);
contextMenu.y = Math.max(20, finalY);
open();
}
width: 180
height: menuColumn.implicitHeight + Theme.spacingS * 2
padding: 0
modal: false
closePolicy: Popup.CloseOnEscape
onClosed: {
closePolicy = Popup.CloseOnEscape;
}
onOpened: {
outsideClickTimer.start();
}
Timer {
id: outsideClickTimer
interval: 100
onTriggered: {
contextMenu.closePolicy = Popup.CloseOnEscape | Popup.CloseOnPressOutside;
}
}
background: Rectangle {
color: "transparent"
}
contentItem: Rectangle {
color: Theme.popupBackground()
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
Column {
id: menuColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 1
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadiusSmall
color: pinMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: {
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry)
return "push_pin";
var appId = contextMenu.currentApp.desktopEntry.id || contextMenu.currentApp.desktopEntry.execString || "";
return Prefs.isPinnedApp(appId) ? "keep_off" : "push_pin";
}
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: {
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry)
return "Pin to Dock";
var appId = contextMenu.currentApp.desktopEntry.id || contextMenu.currentApp.desktopEntry.execString || "";
return Prefs.isPinnedApp(appId) ? "Unpin from Dock" : "Pin to Dock";
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: pinMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry)
return ;
var appId = contextMenu.currentApp.desktopEntry.id || contextMenu.currentApp.desktopEntry.execString || "";
if (Prefs.isPinnedApp(appId))
Prefs.removePinnedApp(appId);
else
Prefs.addPinnedApp(appId);
contextMenu.close();
}
}
}
Rectangle {
width: parent.width - Theme.spacingS * 2
height: 5
anchors.horizontalCenter: parent.horizontalCenter
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
}
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadiusSmall
color: launchMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "launch"
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Launch"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: launchMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (contextMenu.currentApp)
appLauncher.launchApp(contextMenu.currentApp);
contextMenu.close();
}
}
}
}
}
}
}
+185
View File
@@ -0,0 +1,185 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property string searchQuery: ""
property string selectedCategory: "All"
property string viewMode: "list" // "list" or "grid"
property int selectedIndex: 0
property int maxResults: 50
property int gridColumns: 4
property bool debounceSearch: true
property int debounceInterval: 50
property bool keyboardNavigationActive: false
property var categories: {
var allCategories = AppSearchService.getAllCategories().filter((cat) => {
return cat !== "Education" && cat !== "Science";
});
var result = ["All"];
return result.concat(allCategories.filter((cat) => {
return cat !== "All";
}));
}
property var categoryIcons: categories.map((category) => {
return AppSearchService.getCategoryIcon(category);
})
property var appUsageRanking: Prefs.appUsageRanking
property alias model: filteredModel
property var _watchApplications: AppSearchService.applications
signal appLaunched(var app)
signal categorySelected(string category)
signal viewModeSelected(string mode)
function updateFilteredModel() {
filteredModel.clear();
selectedIndex = 0;
keyboardNavigationActive = false;
var apps = [];
if (searchQuery.length === 0) {
if (selectedCategory === "All") {
apps = AppSearchService.applications || [];
} else {
var categoryApps = AppSearchService.getAppsInCategory(selectedCategory);
apps = categoryApps.slice(0, maxResults);
}
} else {
if (selectedCategory === "All") {
apps = AppSearchService.searchApplications(searchQuery);
} else {
var categoryApps = AppSearchService.getAppsInCategory(selectedCategory);
if (categoryApps.length > 0) {
var allSearchResults = AppSearchService.searchApplications(searchQuery);
var categoryNames = new Set(categoryApps.map((app) => {
return app.name;
}));
apps = allSearchResults.filter((searchApp) => {
return categoryNames.has(searchApp.name);
}).slice(0, maxResults);
} else {
apps = [];
}
}
}
if (searchQuery.length === 0)
apps = apps.sort(function(a, b) {
var aId = a.id || (a.execString || a.exec || "");
var bId = b.id || (b.execString || b.exec || "");
var aUsage = appUsageRanking[aId] ? appUsageRanking[aId].usageCount : 0;
var bUsage = appUsageRanking[bId] ? appUsageRanking[bId].usageCount : 0;
if (aUsage !== bUsage)
return bUsage - aUsage;
return (a.name || "").localeCompare(b.name || "");
});
apps.forEach((app) => {
if (app)
filteredModel.append({
"name": app.name || "",
"exec": app.execString || "",
"icon": app.icon || "application-x-executable",
"comment": app.comment || "",
"categories": app.categories || [],
"desktopEntry": app
});
});
}
function selectNext() {
if (filteredModel.count > 0) {
keyboardNavigationActive = true;
if (viewMode === "grid") {
var newIndex = Math.min(selectedIndex + gridColumns, filteredModel.count - 1);
selectedIndex = newIndex;
} else {
selectedIndex = (selectedIndex + 1) % filteredModel.count;
}
}
}
function selectPrevious() {
if (filteredModel.count > 0) {
keyboardNavigationActive = true;
if (viewMode === "grid") {
var newIndex = Math.max(selectedIndex - gridColumns, 0);
selectedIndex = newIndex;
} else {
selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : filteredModel.count - 1;
}
}
}
function selectNextInRow() {
if (filteredModel.count > 0 && viewMode === "grid") {
keyboardNavigationActive = true;
selectedIndex = Math.min(selectedIndex + 1, filteredModel.count - 1);
}
}
function selectPreviousInRow() {
if (filteredModel.count > 0 && viewMode === "grid") {
keyboardNavigationActive = true;
selectedIndex = Math.max(selectedIndex - 1, 0);
}
}
function launchSelected() {
if (filteredModel.count > 0 && selectedIndex >= 0 && selectedIndex < filteredModel.count) {
var selectedApp = filteredModel.get(selectedIndex);
launchApp(selectedApp);
}
}
function launchApp(appData) {
if (!appData)
return ;
appData.desktopEntry.execute();
appLaunched(appData);
Prefs.addAppUsage(appData.desktopEntry);
}
function setCategory(category) {
selectedCategory = category;
categorySelected(category);
}
function setViewMode(mode) {
viewMode = mode;
viewModeSelected(mode);
}
onSearchQueryChanged: {
if (debounceSearch)
searchDebounceTimer.restart();
else
updateFilteredModel();
}
onSelectedCategoryChanged: updateFilteredModel()
onAppUsageRankingChanged: updateFilteredModel()
on_WatchApplicationsChanged: updateFilteredModel()
Component.onCompleted: {
updateFilteredModel();
}
ListModel {
id: filteredModel
}
Timer {
id: searchDebounceTimer
interval: root.debounceInterval
repeat: false
onTriggered: updateFilteredModel()
}
}
+150
View File
@@ -0,0 +1,150 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Widgets
Item {
id: root
property var categories: []
property string selectedCategory: "All"
property bool compact: false // For different layout styles
signal categorySelected(string category)
height: compact ? 36 : (72 + Theme.spacingS) // Single row vs two rows
Row {
visible: compact
width: parent.width
spacing: Theme.spacingS
Repeater {
model: categories.slice(0, Math.min(categories.length, 8)) // Limit for space
Rectangle {
height: 36
width: (parent.width - (Math.min(categories.length, 8) - 1) * Theme.spacingS) / Math.min(categories.length, 8)
radius: Theme.cornerRadiusLarge
color: selectedCategory === modelData ? Theme.primary : "transparent"
border.color: selectedCategory === modelData ? "transparent" : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
StyledText {
anchors.centerIn: parent
text: modelData
color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
elide: Text.ElideRight
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedCategory = modelData;
categorySelected(modelData);
}
}
}
}
}
Column {
visible: !compact
width: parent.width
spacing: Theme.spacingS
Row {
property var topRowCategories: ["All", "Development", "Graphics", "Games"]
width: parent.width
spacing: Theme.spacingS
Repeater {
model: parent.topRowCategories.filter((cat) => {
return categories.includes(cat);
})
Rectangle {
height: 36
width: (parent.width - (parent.topRowCategories.length - 1) * Theme.spacingS) / parent.topRowCategories.length
radius: Theme.cornerRadiusLarge
color: selectedCategory === modelData ? Theme.primary : "transparent"
border.color: selectedCategory === modelData ? "transparent" : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
StyledText {
anchors.centerIn: parent
text: modelData
color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
elide: Text.ElideRight
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedCategory = modelData;
categorySelected(modelData);
}
}
}
}
}
Row {
property var bottomRowCategories: ["Internet", "Media", "Office", "Settings", "System"]
width: parent.width
spacing: Theme.spacingS
Repeater {
model: parent.bottomRowCategories.filter((cat) => {
return categories.includes(cat);
})
Rectangle {
height: 36
width: (parent.width - (parent.bottomRowCategories.length - 1) * Theme.spacingS) / parent.bottomRowCategories.length
radius: Theme.cornerRadiusLarge
color: selectedCategory === modelData ? Theme.primary : "transparent"
border.color: selectedCategory === modelData ? "transparent" : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
StyledText {
anchors.centerIn: parent
text: modelData
color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
elide: Text.ElideRight
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedCategory = modelData;
categorySelected(modelData);
}
}
}
}
}
}
}
+269
View File
@@ -0,0 +1,269 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import qs.Common
import qs.Services
import qs.Widgets
Column {
id: calendarGrid
property date displayDate: new Date()
property date selectedDate: new Date()
function loadEventsForMonth() {
if (!CalendarService || !CalendarService.khalAvailable)
return ;
let firstDay = new Date(displayDate.getFullYear(), displayDate.getMonth(), 1);
let dayOfWeek = firstDay.getDay();
let startDate = new Date(firstDay);
startDate.setDate(startDate.getDate() - dayOfWeek - 7); // Extra week padding
let lastDay = new Date(displayDate.getFullYear(), displayDate.getMonth() + 1, 0);
let endDate = new Date(lastDay);
endDate.setDate(endDate.getDate() + (6 - lastDay.getDay()) + 7); // Extra week padding
CalendarService.loadEvents(startDate, endDate);
}
spacing: Theme.spacingM
onDisplayDateChanged: {
loadEventsForMonth();
}
Component.onCompleted: {
loadEventsForMonth();
}
Connections {
function onKhalAvailableChanged() {
if (CalendarService && CalendarService.khalAvailable)
loadEventsForMonth();
}
target: CalendarService
enabled: CalendarService !== null
}
Row {
width: parent.width
height: 40
Rectangle {
width: 40
height: 40
radius: Theme.cornerRadius
color: prevMonthArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "chevron_left"
size: Theme.iconSize
color: Theme.primary
}
MouseArea {
id: prevMonthArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
let newDate = new Date(displayDate);
newDate.setMonth(newDate.getMonth() - 1);
displayDate = newDate;
}
}
}
StyledText {
width: parent.width - 80
height: 40
text: Qt.formatDate(displayDate, "MMMM yyyy")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
Rectangle {
width: 40
height: 40
radius: Theme.cornerRadius
color: nextMonthArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "chevron_right"
size: Theme.iconSize
color: Theme.primary
}
MouseArea {
id: nextMonthArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
let newDate = new Date(displayDate);
newDate.setMonth(newDate.getMonth() + 1);
displayDate = newDate;
}
}
}
}
Row {
width: parent.width
height: 32
Repeater {
model: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
Rectangle {
width: parent.width / 7
height: 32
color: "transparent"
StyledText {
anchors.centerIn: parent
text: modelData
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
font.weight: Font.Medium
}
}
}
}
Grid {
property date firstDay: {
let date = new Date(displayDate.getFullYear(), displayDate.getMonth(), 1);
let dayOfWeek = date.getDay();
date.setDate(date.getDate() - dayOfWeek);
return date;
}
width: parent.width
height: 200 // Fixed height for calendar
columns: 7
rows: 6
Repeater {
model: 42
Rectangle {
property date dayDate: {
let date = new Date(parent.firstDay);
date.setDate(date.getDate() + index);
return date;
}
property bool isCurrentMonth: dayDate.getMonth() === displayDate.getMonth()
property bool isToday: dayDate.toDateString() === new Date().toDateString()
property bool isSelected: dayDate.toDateString() === selectedDate.toDateString()
width: parent.width / 7
height: parent.height / 6
color: "transparent"
clip: true
Rectangle {
anchors.centerIn: parent
width: parent.width - 4
height: parent.height - 4
color: isSelected ? Theme.primary : isToday ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : dayArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: Theme.cornerRadiusSmall
clip: true
StyledText {
anchors.centerIn: parent
text: dayDate.getDate()
font.pixelSize: Theme.fontSizeMedium
color: isSelected ? Theme.surface : isToday ? Theme.primary : isCurrentMonth ? Theme.surfaceText : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
font.weight: isToday || isSelected ? Font.Medium : Font.Normal
}
Rectangle {
id: eventIndicator
anchors.fill: parent
radius: parent.radius
visible: CalendarService && CalendarService.khalAvailable && CalendarService.hasEventsForDate(dayDate)
opacity: {
if (isSelected)
return 0.9;
else if (isToday)
return 0.8;
else
return 0.6;
}
gradient: Gradient {
GradientStop {
position: 0.89
color: "transparent"
}
GradientStop {
position: 0.9
color: {
if (isSelected)
return Qt.lighter(Theme.primary, 1.3);
else if (isToday)
return Theme.primary;
else
return Theme.primary;
}
}
GradientStop {
position: 1
color: {
if (isSelected)
return Qt.lighter(Theme.primary, 1.3);
else if (isToday)
return Theme.primary;
else
return Theme.primary;
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
MouseArea {
id: dayArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedDate = dayDate;
}
}
}
}
}
}
+259
View File
@@ -0,0 +1,259 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Services.Mpris
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Modules.CentcomCenter
import qs.Services
PanelWindow {
id: root
readonly property bool hasActiveMedia: MprisController.activePlayer !== null
property bool calendarVisible: false
property bool internalVisible: false
visible: internalVisible
onCalendarVisibleChanged: {
if (calendarVisible) {
internalVisible = true;
Qt.callLater(() => {
internalVisible = true;
calendarGrid.loadEventsForMonth();
});
} else {
internalVisible = false;
}
}
onVisibleChanged: {
if (visible && calendarGrid)
calendarGrid.loadEventsForMonth();
}
implicitWidth: 480
implicitHeight: 600
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
Rectangle {
id: mainContainer
readonly property real targetWidth: Math.min(Screen.width * 0.9, 600)
function calculateWidth() {
let baseWidth = 320;
if (leftWidgets.hasAnyWidgets)
return Math.min(parent.width * 0.9, 600);
return Math.min(parent.width * 0.7, 400);
}
function calculateHeight() {
let contentHeight = Theme.spacingM * 2; // margins
let widgetHeight = 160;
widgetHeight += 140 + Theme.spacingM;
let calendarHeight = 300;
let mainRowHeight = Math.max(widgetHeight, calendarHeight);
contentHeight += mainRowHeight + Theme.spacingM;
if (CalendarService && CalendarService.khalAvailable) {
let hasEvents = events.selectedDateEvents && events.selectedDateEvents.length > 0;
let eventsHeight = hasEvents ? Math.min(300, 80 + events.selectedDateEvents.length * 60) : 120;
contentHeight += eventsHeight;
} else {
contentHeight -= Theme.spacingM;
}
return Math.min(contentHeight, parent.height * 0.9);
}
width: targetWidth
height: calculateHeight()
color: Theme.surfaceContainer
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
layer.enabled: true
opacity: calendarVisible ? 1 : 0
scale: calendarVisible ? 1 : 0.9
x: (Screen.width - targetWidth) / 2
y: Theme.barHeight + 4
onOpacityChanged: {
if (opacity === 1)
Qt.callLater(() => {
height = calculateHeight();
});
}
Connections {
function onEventsByDateChanged() {
if (mainContainer.opacity === 1)
mainContainer.height = mainContainer.calculateHeight();
}
function onKhalAvailableChanged() {
if (mainContainer.opacity === 1)
mainContainer.height = mainContainer.calculateHeight();
}
target: CalendarService
enabled: CalendarService !== null
}
Connections {
function onSelectedDateEventsChanged() {
if (mainContainer.opacity === 1)
mainContainer.height = mainContainer.calculateHeight();
}
target: events
enabled: events !== null
}
Rectangle {
anchors.fill: parent
color: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g, Theme.surfaceTint.b, 0.04)
radius: parent.radius
SequentialAnimation on opacity {
running: calendarVisible
loops: Animation.Infinite
NumberAnimation {
to: 0.08
duration: Theme.extraLongDuration
easing.type: Theme.standardEasing
}
NumberAnimation {
to: 0.02
duration: Theme.extraLongDuration
easing.type: Theme.standardEasing
}
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
Row {
width: parent.width
height: {
let widgetHeight = 160; // Media widget
widgetHeight += 140 + Theme.spacingM; // Weather widget with spacing
let calendarHeight = 300; // Calendar
return Math.max(widgetHeight, calendarHeight);
}
spacing: Theme.spacingM
Column {
id: leftWidgets
property bool hasAnyWidgets: true
width: hasAnyWidgets ? parent.width * 0.42 : 0 // Slightly narrower for better proportions
height: childrenRect.height
spacing: Theme.spacingM
visible: hasAnyWidgets
anchors.top: parent.top
MediaPlayer {
width: parent.width
height: 160
}
Weather {
width: parent.width
height: 140
}
}
Rectangle {
width: leftWidgets.hasAnyWidgets ? parent.width - leftWidgets.width - Theme.spacingM : parent.width
height: parent.height
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.2)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
CalendarGrid {
id: calendarGrid
anchors.fill: parent
anchors.margins: Theme.spacingS
}
}
}
Events {
id: events
width: parent.width
selectedDate: calendarGrid.selectedDate
}
}
Behavior on opacity {
NumberAnimation {
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
Behavior on scale {
NumberAnimation {
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 4
shadowBlur: 0.5
shadowColor: Qt.rgba(0, 0, 0, 0.15)
shadowOpacity: 0.15
}
}
MouseArea {
anchors.fill: parent
z: -1
enabled: calendarVisible
onClicked: function(mouse) {
var localPos = mapToItem(mainContainer, mouse.x, mouse.y);
if (localPos.x < 0 || localPos.x > mainContainer.width || localPos.y < 0 || localPos.y > mainContainer.height)
calendarVisible = false;
}
}
}
+307
View File
@@ -0,0 +1,307 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: events
property date selectedDate: new Date()
property var selectedDateEvents: []
property bool hasEvents: selectedDateEvents && selectedDateEvents.length > 0
property bool shouldShow: CalendarService && CalendarService.khalAvailable
function updateSelectedDateEvents() {
if (CalendarService && CalendarService.khalAvailable) {
let events = CalendarService.getEventsForDate(selectedDate);
selectedDateEvents = events;
} else {
selectedDateEvents = [];
}
}
onSelectedDateEventsChanged: {
eventsList.model = selectedDateEvents;
}
width: parent.width
height: shouldShow ? (hasEvents ? Math.min(300, 80 + selectedDateEvents.length * 60) : 120) : 0
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
visible: shouldShow
layer.enabled: true
Component.onCompleted: {
updateSelectedDateEvents();
}
onSelectedDateChanged: {
updateSelectedDateEvents();
}
Connections {
function onEventsByDateChanged() {
updateSelectedDateEvents();
}
function onKhalAvailableChanged() {
updateSelectedDateEvents();
}
target: CalendarService
enabled: CalendarService !== null
}
Row {
id: headerRow
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacingL
spacing: Theme.spacingS
DankIcon {
name: "event"
size: Theme.iconSize - 2
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: hasEvents ? (Qt.formatDate(selectedDate, "MMM d") + " • " + (selectedDateEvents.length === 1 ? "1 event" : selectedDateEvents.length + " events")) : Qt.formatDate(selectedDate, "MMM d")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
visible: !hasEvents
DankIcon {
name: "event_busy"
size: Theme.iconSize + 8
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3)
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: "No events"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
font.weight: Font.Normal
anchors.horizontalCenter: parent.horizontalCenter
}
}
ListView {
id: eventsList
anchors.top: headerRow.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: Theme.spacingL
anchors.topMargin: Theme.spacingM
visible: opacity > 0
opacity: hasEvents ? 1 : 0
clip: true
spacing: Theme.spacingS
boundsMovement: Flickable.StopAtBounds
boundsBehavior: Flickable.StopAtBounds
ScrollBar.vertical: ScrollBar {
policy: eventsList.contentHeight > eventsList.height ? ScrollBar.AsNeeded : ScrollBar.AlwaysOff
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
delegate: Rectangle {
width: eventsList.width
height: eventContent.implicitHeight + Theme.spacingM
radius: Theme.cornerRadius
color: {
if (modelData.url && eventMouseArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
else if (eventMouseArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06);
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.06);
}
border.color: {
if (modelData.url && eventMouseArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3);
else if (eventMouseArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15);
return "transparent";
}
border.width: 1
Rectangle {
width: 4
height: parent.height - 8
anchors.left: parent.left
anchors.leftMargin: 4
anchors.verticalCenter: parent.verticalCenter
radius: 2
color: Theme.primary
opacity: 0.8
}
Column {
id: eventContent
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingL + 4
anchors.rightMargin: Theme.spacingM
spacing: 6
StyledText {
width: parent.width
text: modelData.title
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
wrapMode: Text.Wrap
maximumLineCount: 2
}
Item {
width: parent.width
height: Math.max(timeRow.height, locationRow.height)
Row {
id: timeRow
spacing: 4
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
DankIcon {
name: "schedule"
size: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: {
if (modelData.allDay) {
return "All day";
} else {
let timeFormat = Prefs.use24HourClock ? "H:mm" : "h:mm AP";
let startTime = Qt.formatTime(modelData.start, timeFormat);
if (modelData.start.toDateString() !== modelData.end.toDateString() || modelData.start.getTime() !== modelData.end.getTime())
return startTime + " " + Qt.formatTime(modelData.end, timeFormat);
return startTime;
}
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
id: locationRow
spacing: 4
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
visible: modelData.location !== ""
DankIcon {
name: "location_on"
size: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: modelData.location
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
maximumLineCount: 1
width: Math.min(implicitWidth, 200)
}
}
}
}
MouseArea {
id: eventMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: modelData.url ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: modelData.url !== ""
onClicked: {
if (modelData.url && modelData.url !== "") {
if (Qt.openUrlExternally(modelData.url) === false)
console.warn("Failed to open URL: " + modelData.url);
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 2
shadowBlur: 0.25
shadowColor: Qt.rgba(0, 0, 0, 0.1)
shadowOpacity: 0.1
}
Behavior on height {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
+442
View File
@@ -0,0 +1,442 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Services.Mpris
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: mediaPlayer
property MprisPlayer activePlayer: MprisController.activePlayer
property string lastValidTitle: ""
property string lastValidArtist: ""
property string lastValidAlbum: ""
property string lastValidArtUrl: ""
property real currentPosition: 0
function ratio() {
return activePlayer && activePlayer.length > 0 ? currentPosition / activePlayer.length : 0;
}
onActivePlayerChanged: {
if (!activePlayer)
updateTimer.start();
else
updateTimer.stop();
}
width: parent.width
height: parent.height
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.4)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
layer.enabled: true
Timer {
id: updateTimer
interval: 2000
running: {
return (!activePlayer) || (activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing && activePlayer.length > 0 && !progressMouseArea.isSeeking);
}
repeat: true
onTriggered: {
if (!activePlayer) {
lastValidTitle = "";
lastValidArtist = "";
lastValidAlbum = "";
lastValidArtUrl = "";
stop(); // Stop after clearing cache
} else if (activePlayer.playbackState === MprisPlaybackState.Playing && !progressMouseArea.isSeeking) {
currentPosition = activePlayer.position;
}
}
}
Connections {
function onPositionChanged() {
if (!progressMouseArea.isSeeking)
currentPosition = activePlayer.position;
}
function onPostTrackChanged() {
currentPosition = activePlayer && activePlayer.position || 0;
}
function onTrackTitleChanged() {
currentPosition = activePlayer && activePlayer.position || 0;
}
target: activePlayer
}
Item {
anchors.fill: parent
anchors.margins: Theme.spacingS
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
visible: (!activePlayer && !lastValidTitle) || (activePlayer && activePlayer.trackTitle === "" && lastValidTitle === "")
DankIcon {
name: "music_note"
size: Theme.iconSize + 8
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: "No Media Playing"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
}
Column {
anchors.fill: parent
spacing: Theme.spacingS
visible: activePlayer && activePlayer.trackTitle !== "" || lastValidTitle !== ""
Row {
width: parent.width
height: 60
spacing: Theme.spacingM
Rectangle {
width: 60
height: 60
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
Item {
anchors.fill: parent
clip: true
Image {
id: albumArt
anchors.fill: parent
source: activePlayer && activePlayer.trackArtUrl || lastValidArtUrl || ""
onSourceChanged: {
if (activePlayer && activePlayer.trackArtUrl)
lastValidArtUrl = activePlayer.trackArtUrl;
}
fillMode: Image.PreserveAspectCrop
smooth: true
cache: true
}
Rectangle {
anchors.fill: parent
visible: albumArt.status !== Image.Ready
color: "transparent"
DankIcon {
anchors.centerIn: parent
name: "album"
size: 28
color: Theme.surfaceVariantText
}
}
}
}
Column {
width: parent.width - 60 - Theme.spacingM
height: parent.height
spacing: Theme.spacingXS
StyledText {
text: activePlayer && activePlayer.trackTitle || lastValidTitle || "Unknown Track"
onTextChanged: {
if (activePlayer && activePlayer.trackTitle)
lastValidTitle = activePlayer.trackTitle;
}
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
wrapMode: Text.NoWrap
maximumLineCount: 1
}
StyledText {
text: activePlayer && activePlayer.trackArtist || lastValidArtist || "Unknown Artist"
onTextChanged: {
if (activePlayer && activePlayer.trackArtist)
lastValidArtist = activePlayer.trackArtist;
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
width: parent.width
elide: Text.ElideRight
wrapMode: Text.NoWrap
maximumLineCount: 1
}
StyledText {
text: activePlayer && activePlayer.trackAlbum || lastValidAlbum || ""
onTextChanged: {
if (activePlayer && activePlayer.trackAlbum)
lastValidAlbum = activePlayer.trackAlbum;
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
width: parent.width
elide: Text.ElideRight
wrapMode: Text.NoWrap
maximumLineCount: 1
visible: text.length > 0
}
}
}
Item {
id: progressBarContainer
width: parent.width
height: 24
Rectangle {
id: progressBarBackground
width: parent.width
height: 6
radius: 3
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
visible: activePlayer !== null
anchors.verticalCenter: parent.verticalCenter
Rectangle {
id: progressFill
height: parent.height
radius: parent.radius
color: Theme.primary
width: parent.width * ratio()
Behavior on width {
NumberAnimation {
duration: 100
}
}
}
Rectangle {
id: progressHandle
width: 12
height: 12
radius: 6
color: Theme.primary
border.color: Qt.lighter(Theme.primary, 1.3)
border.width: 1
x: Math.max(0, Math.min(parent.width - width, progressFill.width - width / 2))
anchors.verticalCenter: parent.verticalCenter
visible: activePlayer && activePlayer.length > 0
scale: progressMouseArea.containsMouse || progressMouseArea.pressed ? 1.2 : 1
Behavior on scale {
NumberAnimation {
duration: 150
}
}
}
}
MouseArea {
id: progressMouseArea
property bool isSeeking: false
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: activePlayer && activePlayer.length > 0 && activePlayer.canSeek
preventStealing: true
onPressed: function(mouse) {
isSeeking = true;
if (activePlayer && activePlayer.length > 0) {
let ratio = Math.max(0, Math.min(1, mouse.x / progressBarBackground.width));
let seekPosition = ratio * activePlayer.length;
activePlayer.position = seekPosition;
currentPosition = seekPosition;
}
}
onReleased: {
isSeeking = false;
}
onPositionChanged: function(mouse) {
if (pressed && isSeeking && activePlayer && activePlayer.length > 0) {
let ratio = Math.max(0, Math.min(1, mouse.x / progressBarBackground.width));
let seekPosition = ratio * activePlayer.length;
activePlayer.position = seekPosition;
currentPosition = seekPosition;
}
}
onClicked: function(mouse) {
if (activePlayer && activePlayer.length > 0) {
let ratio = Math.max(0, Math.min(1, mouse.x / progressBarBackground.width));
let seekPosition = ratio * activePlayer.length;
activePlayer.position = seekPosition;
currentPosition = seekPosition;
}
}
}
MouseArea {
id: progressGlobalMouseArea
x: 0
y: 0
width: mediaPlayer.width
height: mediaPlayer.height
enabled: progressMouseArea.isSeeking
visible: false
preventStealing: true
onPositionChanged: function(mouse) {
if (progressMouseArea.isSeeking && activePlayer && activePlayer.length > 0) {
let globalPos = mapToItem(progressBarBackground, mouse.x, mouse.y);
let ratio = Math.max(0, Math.min(1, globalPos.x / progressBarBackground.width));
let seekPosition = ratio * activePlayer.length;
activePlayer.position = seekPosition;
currentPosition = seekPosition;
}
}
onReleased: {
progressMouseArea.isSeeking = false;
}
}
}
Item {
width: parent.width
height: 32
visible: activePlayer !== null
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
height: parent.height
Rectangle {
width: 28
height: 28
radius: 14
color: prevBtnArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "skip_previous"
size: 16
color: Theme.surfaceText
}
MouseArea {
id: prevBtnArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!activePlayer)
return ;
if (currentPosition > 8 && activePlayer.canSeek) {
activePlayer.position = 0;
currentPosition = 0;
} else {
activePlayer.previous();
}
}
}
}
Rectangle {
width: 32
height: 32
radius: 16
color: Theme.primary
DankIcon {
anchors.centerIn: parent
name: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
size: 20
color: Theme.background
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: activePlayer && activePlayer.togglePlaying()
}
}
Rectangle {
width: 28
height: 28
radius: 14
color: nextBtnArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "skip_next"
size: 16
color: Theme.surfaceText
}
MouseArea {
id: nextBtnArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: activePlayer && activePlayer.next()
}
}
}
}
}
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 2
shadowBlur: 0.5
shadowColor: Qt.rgba(0, 0, 0, 0.1)
shadowOpacity: 0.1
}
}
+196
View File
@@ -0,0 +1,196 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: weather
width: parent.width
height: parent.height
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.4)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
layer.enabled: true
Ref {
service: WeatherService
}
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
visible: !WeatherService.weather.available || WeatherService.weather.temp === 0
DankIcon {
name: "cloud_off"
size: Theme.iconSize + 8
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: "No Weather Data"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingS
visible: WeatherService.weather.available && WeatherService.weather.temp !== 0
Item {
width: parent.width
height: 60
Row {
anchors.centerIn: parent
spacing: Theme.spacingL
DankIcon {
name: WeatherService.getWeatherIcon(WeatherService.weather.wCode)
size: Theme.iconSize + 8
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: (Prefs.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp) + "°" + (Prefs.useFahrenheit ? "F" : "C")
font.pixelSize: Theme.fontSizeXLarge
color: Theme.surfaceText
font.weight: Font.Light
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (WeatherService.weather.available)
Prefs.setTemperatureUnit(!Prefs.useFahrenheit);
}
enabled: WeatherService.weather.available
}
}
StyledText {
text: WeatherService.weather.city || ""
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: text.length > 0
}
}
}
}
Grid {
columns: 2
spacing: Theme.spacingM
anchors.horizontalCenter: parent.horizontalCenter
Row {
spacing: Theme.spacingXS
DankIcon {
name: "humidity_low"
size: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: WeatherService.weather.humidity ? WeatherService.weather.humidity + "%" : "--"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
spacing: Theme.spacingXS
DankIcon {
name: "air"
size: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: WeatherService.weather.wind || "--"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
spacing: Theme.spacingXS
DankIcon {
name: "wb_twilight"
size: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: WeatherService.weather.sunrise || "--"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
spacing: Theme.spacingXS
DankIcon {
name: "bedtime"
size: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: WeatherService.weather.sunset || "--"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 2
shadowBlur: 0.5
shadowColor: Qt.rgba(0, 0, 0, 0.1)
shadowOpacity: 0.1
}
}
@@ -0,0 +1,150 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import Quickshell.Services.Pipewire
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Column {
id: root
property string currentSinkDisplayName: AudioService.sink ? AudioService.displayName(AudioService.sink) : ""
width: parent.width
spacing: Theme.spacingM
StyledText {
text: "Output Device"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
Rectangle {
width: parent.width
height: 35
radius: Theme.cornerRadius
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
border.width: 1
visible: AudioService.sink !== null
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "check_circle"
size: Theme.iconSize - 4
color: Theme.primary
}
StyledText {
text: "Current: " + (root.currentSinkDisplayName || "None")
font.pixelSize: Theme.fontSizeMedium
color: Theme.primary
font.weight: Font.Medium
}
}
}
Repeater {
model: {
if (!Pipewire.ready || !Pipewire.nodes || !Pipewire.nodes.values)
return [];
let sinks = [];
for (let i = 0; i < Pipewire.nodes.values.length; i++) {
let node = Pipewire.nodes.values[i];
if (!node || node.isStream)
continue;
if ((node.type & PwNodeType.AudioSink) === PwNodeType.AudioSink)
sinks.push(node);
}
return sinks;
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: deviceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : (modelData === AudioService.sink ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
border.color: modelData === AudioService.sink ? Theme.primary : "transparent"
border.width: 1
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: {
if (modelData.name.includes("bluez"))
return "headset";
else if (modelData.name.includes("hdmi"))
return "tv";
else if (modelData.name.includes("usb"))
return "headset";
else
return "speaker";
}
size: Theme.iconSize
color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: AudioService.displayName(modelData)
font.pixelSize: Theme.fontSizeMedium
color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText
font.weight: modelData === AudioService.sink ? Font.Medium : Font.Normal
}
StyledText {
text: {
if (AudioService.subtitle(modelData.name) && AudioService.subtitle(modelData.name) !== "")
return AudioService.subtitle(modelData.name) + (modelData === AudioService.sink ? " • Selected" : "");
else
return modelData === AudioService.sink ? "Selected" : "";
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: text !== ""
}
}
}
MouseArea {
id: deviceArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData)
Pipewire.preferredDefaultAudioSink = modelData;
}
}
}
}
}
@@ -0,0 +1,148 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import Quickshell.Services.Pipewire
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Column {
id: root
property string currentSourceDisplayName: AudioService.source ? AudioService.displayName(AudioService.source) : ""
width: parent.width
spacing: Theme.spacingM
StyledText {
text: "Input Device"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
Rectangle {
width: parent.width
height: 35
radius: Theme.cornerRadius
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
border.width: 1
visible: AudioService.source !== null
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "check_circle"
size: Theme.iconSize - 4
color: Theme.primary
}
StyledText {
text: "Current: " + (root.currentSourceDisplayName || "None")
font.pixelSize: Theme.fontSizeMedium
color: Theme.primary
font.weight: Font.Medium
}
}
}
Repeater {
model: {
if (!Pipewire.ready || !Pipewire.nodes || !Pipewire.nodes.values)
return [];
let sources = [];
for (let i = 0; i < Pipewire.nodes.values.length; i++) {
let node = Pipewire.nodes.values[i];
if (!node || node.isStream)
continue;
if ((node.type & PwNodeType.AudioSource) === PwNodeType.AudioSource && !node.name.includes(".monitor"))
sources.push(node);
}
return sources;
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: sourceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : (modelData === AudioService.source ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
border.color: modelData === AudioService.source ? Theme.primary : "transparent"
border.width: 1
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: {
if (modelData.name.includes("bluez"))
return "headset_mic";
else if (modelData.name.includes("usb"))
return "headset_mic";
else
return "mic";
}
size: Theme.iconSize
color: modelData === AudioService.source ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: AudioService.displayName(modelData)
font.pixelSize: Theme.fontSizeMedium
color: modelData === AudioService.source ? Theme.primary : Theme.surfaceText
font.weight: modelData === AudioService.source ? Font.Medium : Font.Normal
}
StyledText {
text: {
if (AudioService.subtitle(modelData.name) && AudioService.subtitle(modelData.name) !== "")
return AudioService.subtitle(modelData.name) + (modelData === AudioService.source ? " • Selected" : "");
else
return modelData === AudioService.source ? "Selected" : "";
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: text !== ""
}
}
}
MouseArea {
id: sourceArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData)
Pipewire.preferredDefaultAudioSource = modelData;
}
}
}
}
}
@@ -0,0 +1,223 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Column {
id: root
property real micLevel: Math.min(100, (AudioService.source && AudioService.source.audio && AudioService.source.audio.volume * 100) || 0)
property bool micMuted: (AudioService.source && AudioService.source.audio && AudioService.source.audio.muted) || false
width: parent.width
spacing: Theme.spacingM
StyledText {
text: "Microphone Level"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: root.micMuted ? "mic_off" : "mic"
size: Theme.iconSize
color: root.micMuted ? Theme.error : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (AudioService.source && AudioService.source.audio)
AudioService.source.audio.muted = !AudioService.source.audio.muted;
}
}
}
Item {
id: micSliderContainer
width: parent.width - 80
height: 32
anchors.verticalCenter: parent.verticalCenter
Rectangle {
id: micSliderTrack
width: parent.width
height: 8
radius: 4
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
anchors.verticalCenter: parent.verticalCenter
Rectangle {
id: micSliderFill
width: parent.width * (root.micLevel / 100)
height: parent.height
radius: parent.radius
color: Theme.primary
Behavior on width {
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standardDecel
}
}
}
Rectangle {
id: micHandle
width: 18
height: 18
radius: 9
color: Theme.primary
border.color: Qt.lighter(Theme.primary, 1.3)
border.width: 2
x: Math.max(0, Math.min(parent.width - width, micSliderFill.width - width / 2))
anchors.verticalCenter: parent.verticalCenter
scale: micMouseArea.containsMouse || micMouseArea.pressed ? 1.2 : 1
Rectangle {
id: micTooltip
width: tooltipText.contentWidth + Theme.spacingS * 2
height: tooltipText.contentHeight + Theme.spacingXS * 2
radius: Theme.cornerRadiusSmall
color: Theme.surfaceContainer
border.color: Theme.outline
border.width: 1
anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
visible: (micMouseArea.containsMouse && !root.micMuted) || micMouseArea.isDragging
opacity: visible ? 1 : 0
StyledText {
id: tooltipText
text: Math.round(root.micLevel) + "%"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.centerIn: parent
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Behavior on scale {
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
}
}
}
MouseArea {
id: micMouseArea
property bool isDragging: false
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
preventStealing: true
onPressed: (mouse) => {
isDragging = true;
let ratio = Math.max(0, Math.min(1, mouse.x / micSliderTrack.width));
let newMicLevel = Math.round(ratio * 100);
if (AudioService.source && AudioService.source.audio) {
AudioService.source.audio.muted = false;
AudioService.source.audio.volume = newMicLevel / 100;
}
}
onReleased: {
isDragging = false;
}
onPositionChanged: (mouse) => {
if (pressed && isDragging) {
let ratio = Math.max(0, Math.min(1, mouse.x / micSliderTrack.width));
let newMicLevel = Math.max(0, Math.min(100, Math.round(ratio * 100)));
if (AudioService.source && AudioService.source.audio) {
AudioService.source.audio.muted = false;
AudioService.source.audio.volume = newMicLevel / 100;
}
}
}
onClicked: (mouse) => {
let ratio = Math.max(0, Math.min(1, mouse.x / micSliderTrack.width));
let newMicLevel = Math.round(ratio * 100);
if (AudioService.source && AudioService.source.audio) {
AudioService.source.audio.muted = false;
AudioService.source.audio.volume = newMicLevel / 100;
}
}
}
MouseArea {
id: micGlobalMouseArea
x: 0
y: 0
width: root.parent ? root.parent.width : 0
height: root.parent ? root.parent.height : 0
enabled: micMouseArea.isDragging
visible: false
preventStealing: true
onPositionChanged: (mouse) => {
if (micMouseArea.isDragging) {
let globalPos = mapToItem(micSliderTrack, mouse.x, mouse.y);
let ratio = Math.max(0, Math.min(1, globalPos.x / micSliderTrack.width));
let newMicLevel = Math.max(0, Math.min(100, Math.round(ratio * 100)));
if (AudioService.source && AudioService.source.audio) {
AudioService.source.audio.muted = false;
AudioService.source.audio.volume = newMicLevel / 100;
}
}
}
onReleased: {
micMouseArea.isDragging = false;
}
}
}
DankIcon {
name: "mic"
size: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
}
@@ -0,0 +1,63 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Column {
id: root
property real volumeLevel: Math.min(100, (AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.volume * 100) || 0)
property bool volumeMuted: (AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.muted) || false
width: parent.width
spacing: Theme.spacingM
StyledText {
text: "Volume"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
DankSlider {
id: volumeSlider
width: parent.width
minimum: 0
maximum: 100
leftIcon: root.volumeMuted ? "volume_off" : "volume_down"
rightIcon: "volume_up"
enabled: !root.volumeMuted
showValue: true
unit: "%"
Connections {
target: AudioService.sink && AudioService.sink.audio ? AudioService.sink.audio : null
function onVolumeChanged() {
volumeSlider.value = Math.round(AudioService.sink.audio.volume * 100);
}
}
Component.onCompleted: {
if (AudioService.sink && AudioService.sink.audio) {
value = Math.round(AudioService.sink.audio.volume * 100);
}
let leftIconItem = volumeSlider.children[0].children[0];
if (leftIconItem) {
let mouseArea = Qt.createQmlObject(
'import QtQuick 2.15; MouseArea { anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onClicked: { if (AudioService.sink && AudioService.sink.audio) AudioService.sink.audio.muted = !AudioService.sink.audio.muted; } }',
leftIconItem,
"dynamicMouseArea"
);
}
}
onSliderValueChanged: (newValue) => {
if (AudioService.sink && AudioService.sink.audio) {
AudioService.sink.audio.muted = false;
AudioService.sink.audio.volume = newValue / 100;
}
}
}
}
+78
View File
@@ -0,0 +1,78 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import Quickshell.Services.Pipewire
import Quickshell.Widgets
import qs.Common
import qs.Modules.ControlCenter.Audio
import qs.Services
import qs.Widgets
Item {
id: audioTab
property int audioSubTab: 0
Column {
anchors.fill: parent
spacing: Theme.spacingM
DankTabBar {
width: parent.width
tabHeight: 40
currentIndex: audioTab.audioSubTab
showIcons: false
model: [{
"text": "Output"
}, {
"text": "Input"
}]
onTabClicked: function(index) {
audioTab.audioSubTab = index;
}
}
ScrollView {
width: parent.width
height: parent.height - 48
visible: audioTab.audioSubTab === 0
clip: true
Column {
width: parent.width
spacing: Theme.spacingL
VolumeControl {
}
AudioDevicesList {
}
}
}
ScrollView {
width: parent.width
height: parent.height - 48
visible: audioTab.audioSubTab === 1
clip: true
Column {
width: parent.width
spacing: Theme.spacingL
MicrophoneControl {
}
AudioInputDevicesList {
}
}
}
}
}
@@ -0,0 +1,406 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Bluetooth
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Column {
id: root
width: parent.width
spacing: Theme.spacingM
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
Row {
width: parent.width
spacing: Theme.spacingM
StyledText {
text: "Available Devices"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - scanButton.width - parent.spacing - 150 // Spacer to push button right
height: 1
}
Rectangle {
id: scanButton
width: Math.max(100, scanText.contentWidth + Theme.spacingL * 2)
height: 32
radius: Theme.cornerRadius
color: scanArea.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.color: Theme.primary
border.width: 1
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop" : "bluetooth_searching"
size: Theme.iconSize - 6
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
id: scanText
text: BluetoothService.adapter && BluetoothService.adapter.discovering ? "Stop Scanning" : "Scan"
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: scanArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (BluetoothService.adapter)
BluetoothService.adapter.discovering = !BluetoothService.adapter.discovering;
}
}
}
}
Rectangle {
width: parent.width
height: noteColumn.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.08)
border.color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.2)
border.width: 1
Column {
id: noteColumn
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
Row {
width: parent.width
spacing: Theme.spacingS
DankIcon {
name: "info"
size: Theme.iconSize - 2
color: Theme.warning
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Pairing Limitation"
font.pixelSize: Theme.fontSizeMedium
color: Theme.warning
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: "Quickshell does not support pairing devices that require pin or confirmation."
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
wrapMode: Text.WordWrap
width: parent.width
}
}
}
Repeater {
model: {
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices)
return [];
var filtered = Bluetooth.devices.values.filter((dev) => {
return dev && !dev.paired && !dev.pairing && !dev.blocked && (dev.signalStrength === undefined || dev.signalStrength > 0);
});
return BluetoothService.sortDevices(filtered);
}
Rectangle {
property bool canConnect: BluetoothService.canConnect(modelData)
property bool isBusy: BluetoothService.isDeviceBusy(modelData)
width: parent.width
height: 70
radius: Theme.cornerRadius
color: {
if (availableDeviceArea.containsMouse && !isBusy)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12);
if (modelData.blocked)
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08);
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
}
border.color: {
if (modelData.pairing)
return Theme.warning;
if (modelData.blocked)
return Theme.error;
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2);
}
border.width: 1
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: BluetoothService.getDeviceIcon(modelData)
size: Theme.iconSize
color: {
if (modelData.pairing)
return Theme.warning;
if (modelData.blocked)
return Theme.error;
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: modelData.name || modelData.deviceName
font.pixelSize: Theme.fontSizeMedium
color: {
if (modelData.pairing)
return Theme.warning;
if (modelData.blocked)
return Theme.error;
return Theme.surfaceText;
}
font.weight: modelData.pairing ? Font.Medium : Font.Normal
}
Row {
spacing: Theme.spacingXS
Row {
spacing: Theme.spacingS
StyledText {
text: {
if (modelData.pairing)
return "Pairing...";
if (modelData.blocked)
return "Blocked";
return BluetoothService.getSignalStrength(modelData);
}
font.pixelSize: Theme.fontSizeSmall
color: {
if (modelData.pairing)
return Theme.warning;
if (modelData.blocked)
return Theme.error;
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7);
}
}
DankIcon {
name: BluetoothService.getSignalIcon(modelData)
size: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0 && !modelData.pairing && !modelData.blocked
}
StyledText {
text: (modelData.signalStrength !== undefined && modelData.signalStrength > 0) ? modelData.signalStrength + "%" : ""
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0 && !modelData.pairing && !modelData.blocked
}
}
}
}
}
Rectangle {
width: 80
height: 28
radius: Theme.cornerRadiusSmall
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
visible: modelData.state !== BluetoothDeviceState.Connecting
color: {
if (!canConnect && !isBusy)
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3);
if (actionButtonArea.containsMouse && !isBusy)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
return "transparent";
}
border.color: canConnect || isBusy ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 1
opacity: canConnect || isBusy ? 1 : 0.5
StyledText {
anchors.centerIn: parent
text: {
if (modelData.pairing)
return "Pairing...";
if (modelData.blocked)
return "Blocked";
return "Connect";
}
font.pixelSize: Theme.fontSizeSmall
color: canConnect || isBusy ? Theme.primary : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
font.weight: Font.Medium
}
MouseArea {
id: actionButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: canConnect && !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor)
enabled: canConnect && !isBusy
onClicked: {
if (modelData)
BluetoothService.connectDeviceWithTrust(modelData);
}
}
}
MouseArea {
id: availableDeviceArea
anchors.fill: parent
anchors.rightMargin: 90
hoverEnabled: true
cursorShape: canConnect && !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor)
enabled: canConnect && !isBusy
onClicked: {
if (modelData)
BluetoothService.connectDeviceWithTrust(modelData);
}
}
}
}
Column {
width: parent.width
spacing: Theme.spacingM
visible: {
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices)
return false;
var availableCount = Bluetooth.devices.values.filter((dev) => {
return dev && !dev.paired && !dev.pairing && !dev.blocked && (dev.signalStrength === undefined || dev.signalStrength > 0);
}).length;
return availableCount === 0;
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
DankIcon {
name: "sync"
size: Theme.iconSizeLarge
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
RotationAnimation on rotation {
running: true
loops: Animation.Infinite
from: 0
to: 360
duration: 2000
}
}
StyledText {
text: "Scanning for devices..."
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: "Make sure your device is in pairing mode"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
}
StyledText {
text: "No devices found. Put your device in pairing mode and click Start Scanning."
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: {
if (!BluetoothService.adapter || !Bluetooth.devices)
return true;
var availableCount = Bluetooth.devices.values.filter((dev) => {
return dev && !dev.paired && !dev.pairing && !dev.blocked && (dev.signalStrength === undefined || dev.signalStrength > 0);
}).length;
return availableCount === 0 && !BluetoothService.adapter.discovering;
}
wrapMode: Text.WordWrap
width: parent.width
horizontalAlignment: Text.AlignHCenter
}
}
@@ -0,0 +1,211 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Bluetooth
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
property var deviceData: null
property bool menuVisible: false
property var parentItem
function show(x, y) {
const menuWidth = 160;
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2;
let finalX = x - menuWidth / 2;
let finalY = y;
finalX = Math.max(0, Math.min(finalX, parentItem.width - menuWidth));
finalY = Math.max(0, Math.min(finalY, parentItem.height - menuHeight));
root.x = finalX;
root.y = finalY;
root.visible = true;
root.menuVisible = true;
}
function hide() {
root.menuVisible = false;
Qt.callLater(() => {
root.visible = false;
});
}
visible: false
width: 160
height: menuColumn.implicitHeight + Theme.spacingS * 2
radius: Theme.cornerRadiusLarge
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
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
}
Column {
id: menuColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 1
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadiusSmall
color: connectArea.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: root.deviceData && root.deviceData.connected ? "link_off" : "link"
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: root.deviceData && root.deviceData.connected ? "Disconnect" : "Connect"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: connectArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.deviceData) {
if (root.deviceData.connected)
root.deviceData.disconnect();
else
BluetoothService.connectDeviceWithTrust(root.deviceData);
}
root.hide();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Rectangle {
width: parent.width - Theme.spacingS * 2
height: 5
anchors.horizontalCenter: parent.horizontalCenter
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
}
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadiusSmall
color: forgetArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "delete"
size: Theme.iconSize - 2
color: forgetArea.containsMouse ? Theme.error : Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Forget Device"
font.pixelSize: Theme.fontSizeSmall
color: forgetArea.containsMouse ? Theme.error : Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: forgetArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.deviceData)
root.deviceData.forget();
root.hide();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
@@ -0,0 +1,68 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Bluetooth
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
width: parent.width
height: 60
radius: Theme.cornerRadius
color: bluetoothToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : (BluetoothService.adapter && BluetoothService.adapter.enabled ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12))
border.color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : "transparent"
border.width: 2
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingL
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "bluetooth"
size: Theme.iconSizeLarge
color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "Bluetooth"
font.pixelSize: Theme.fontSizeLarge
color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: BluetoothService.adapter && BluetoothService.adapter.enabled ? "Enabled" : "Disabled"
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
}
}
}
MouseArea {
id: bluetoothToggle
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (BluetoothService.adapter)
BluetoothService.adapter.enabled = !BluetoothService.adapter.enabled;
}
}
}
@@ -0,0 +1,157 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Bluetooth
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Column {
id: root
property var bluetoothContextMenuWindow
width: parent.width
spacing: Theme.spacingM
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
StyledText {
text: "Paired Devices"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
Repeater {
model: BluetoothService.adapter && BluetoothService.adapter.devices ? BluetoothService.adapter.devices.values.filter((dev) => {
return dev && (dev.paired || dev.trusted);
}) : []
Rectangle {
width: parent.width
height: 60
radius: Theme.cornerRadius
color: btDeviceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : (modelData.connected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
border.color: modelData.connected ? Theme.primary : "transparent"
border.width: 1
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: BluetoothService.getDeviceIcon(modelData)
size: Theme.iconSize
color: modelData.connected ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: modelData.name || modelData.deviceName
font.pixelSize: Theme.fontSizeMedium
color: modelData.connected ? Theme.primary : Theme.surfaceText
font.weight: modelData.connected ? Font.Medium : Font.Normal
}
Row {
spacing: Theme.spacingXS
StyledText {
text: BluetoothDeviceState.toString(modelData.state)
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
}
StyledText {
text: {
if (modelData.batteryAvailable && modelData.battery > 0)
return "• " + Math.round(modelData.battery * 100) + "%";
var btBattery = BatteryService.bluetoothDevices.find((dev) => {
return dev.name === (modelData.name || modelData.deviceName) || dev.name.toLowerCase().includes((modelData.name || modelData.deviceName).toLowerCase()) || (modelData.name || modelData.deviceName).toLowerCase().includes(dev.name.toLowerCase());
});
return btBattery ? "• " + btBattery.percentage + "%" : "";
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: text.length > 0
}
}
}
}
Rectangle {
id: btMenuButton
width: 32
height: 32
radius: Theme.cornerRadius
color: btMenuButtonArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
DankIcon {
name: "more_vert"
size: Theme.iconSize
color: Theme.surfaceText
opacity: 0.6
anchors.centerIn: parent
}
MouseArea {
id: btMenuButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (bluetoothContextMenuWindow) {
bluetoothContextMenuWindow.deviceData = modelData;
let localPos = btMenuButtonArea.mapToItem(bluetoothContextMenuWindow.parentItem, btMenuButtonArea.width / 2, btMenuButtonArea.height);
bluetoothContextMenuWindow.show(localPos.x, localPos.y);
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
MouseArea {
id: btDeviceArea
anchors.fill: parent
anchors.rightMargin: 40
hoverEnabled: true
enabled: !BluetoothService.isDeviceBusy(modelData)
cursorShape: enabled ? Qt.PointingHandCursor : Qt.BusyCursor
onClicked: {
if (modelData.connected)
modelData.disconnect();
else
BluetoothService.connectDeviceWithTrust(modelData);
}
}
}
}
}
+63
View File
@@ -0,0 +1,63 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Bluetooth
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Modules.ControlCenter.Bluetooth
import qs.Services
import qs.Widgets
Item {
id: bluetoothTab
ScrollView {
anchors.fill: parent
clip: true
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
Column {
width: parent.width
spacing: Theme.spacingL
BluetoothToggle {
}
PairedDevicesList {
bluetoothContextMenuWindow: bluetoothContextMenuWindow
}
AvailableDevicesList {
}
}
}
BluetoothContextMenu {
id: bluetoothContextMenuWindow
parentItem: bluetoothTab
}
MouseArea {
anchors.fill: parent
visible: bluetoothContextMenuWindow.visible
onClicked: {
bluetoothContextMenuWindow.hide();
}
MouseArea {
x: bluetoothContextMenuWindow.x
y: bluetoothContextMenuWindow.y
width: bluetoothContextMenuWindow.width
height: bluetoothContextMenuWindow.height
onClicked: {
}
}
}
}
@@ -0,0 +1,767 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Modules.ControlCenter
import qs.Services
import qs.Widgets
PanelWindow {
id: root
property bool controlCenterVisible: false
property string currentTab: "network" // "network", "audio", "bluetooth", "display"
property bool powerOptionsExpanded: false
signal powerActionRequested(string action, string title, string message)
signal lockRequested()
visible: controlCenterVisible
onVisibleChanged: {
NetworkService.autoRefreshEnabled = visible && NetworkService.wifiEnabled;
if (!visible && BluetoothService.adapter && BluetoothService.adapter.discovering)
BluetoothService.adapter.discovering = false;
if (visible && UserInfoService)
UserInfoService.getUptime();
}
implicitWidth: 600
implicitHeight: 500
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
Loader {
id: contentLoader
readonly property real targetWidth: Math.min(600, Screen.width - Theme.spacingL * 2)
asynchronous: true
active: controlCenterVisible
width: targetWidth
height: root.powerOptionsExpanded ? 570 : 500
y: Theme.barHeight + Theme.spacingXS
x: Math.max(Theme.spacingL, Screen.width - targetWidth - Theme.spacingL)
opacity: controlCenterVisible ? 1 : 0
scale: controlCenterVisible ? 1 : 0.9
Behavior on opacity {
NumberAnimation {
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
Behavior on scale {
NumberAnimation {
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
sourceComponent: Rectangle {
color: Theme.popupBackground()
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
antialiasing: true
smooth: true
ColumnLayout {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Column {
width: parent.width
spacing: Theme.spacingL
Rectangle {
width: parent.width
height: 90
radius: Theme.cornerRadiusLarge
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: 1
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingL
anchors.rightMargin: Theme.spacingL
spacing: Theme.spacingL
Item {
id: avatarContainer
property bool hasImage: profileImageLoader.status === Image.Ready
width: 64
height: 64
Rectangle {
anchors.fill: parent
radius: width / 2
color: "transparent"
border.color: Theme.primary
border.width: 1 // The ring is 1px thick.
visible: parent.hasImage
}
Image {
id: profileImageLoader
source: {
if (Prefs.profileImage === "")
return "";
if (Prefs.profileImage.startsWith("/"))
return "file://" + Prefs.profileImage;
return Prefs.profileImage;
}
smooth: true
asynchronous: true
mipmap: true
cache: true
visible: false // This item is never shown directly.
}
MultiEffect {
anchors.fill: parent
anchors.margins: 5
source: profileImageLoader
maskEnabled: true
maskSource: circularMask
visible: avatarContainer.hasImage
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
Item {
id: circularMask
width: 64 - 10
height: 64 - 10
layer.enabled: true
layer.smooth: true
visible: false
Rectangle {
anchors.fill: parent
radius: width / 2
color: "black"
antialiasing: true
}
}
Rectangle {
anchors.fill: parent
radius: width / 2
color: Theme.primary
visible: !parent.hasImage
DankIcon {
anchors.centerIn: parent
name: "person"
size: Theme.iconSize + 8
color: Theme.primaryText
}
}
DankIcon {
anchors.centerIn: parent
name: "warning"
size: Theme.iconSize + 8
color: Theme.primaryText
visible: Prefs.profileImage !== "" && profileImageLoader.status === Image.Error
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
StyledText {
text: UserInfoService.fullName || UserInfoService.username || "User"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: "Uptime: " + (UserInfoService.uptime || "Unknown")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
font.weight: Font.Normal
}
}
}
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: Theme.spacingL
spacing: Theme.spacingS
DankActionButton {
buttonSize: 40
iconName: "lock"
iconSize: Theme.iconSize - 2
iconColor: Theme.surfaceText
backgroundColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
hoverColor: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
onClicked: {
controlCenterVisible = false;
root.lockRequested();
}
}
Rectangle {
width: 40
height: 40
radius: 20
color: powerButton.containsMouse || root.powerOptionsExpanded ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
Rectangle {
anchors.centerIn: parent
width: parent.width
height: parent.height
radius: parent.radius
color: "transparent"
clip: true
DankIcon {
id: dankIcon
anchors.centerIn: parent
name: root.powerOptionsExpanded ? "expand_less" : "power_settings_new"
size: Theme.iconSize - 2
color: powerButton.containsMouse || root.powerOptionsExpanded ? Theme.error : Theme.surfaceText
Behavior on name {
SequentialAnimation {
NumberAnimation {
target: dankIcon
property: "opacity"
to: 0
duration: Theme.shortDuration / 2
easing.type: Theme.standardEasing
}
PropertyAction {
target: dankIcon
property: "name"
}
NumberAnimation {
target: dankIcon
property: "opacity"
to: 1
duration: Theme.shortDuration / 2
easing.type: Theme.standardEasing
}
}
}
}
}
MouseArea {
id: powerButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.powerOptionsExpanded = !root.powerOptionsExpanded;
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
DankActionButton {
buttonSize: 40
iconName: "settings"
iconSize: Theme.iconSize - 2
iconColor: Theme.surfaceText
backgroundColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
hoverColor: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
onClicked: {
controlCenterVisible = false;
settingsModal.settingsVisible = true;
}
}
}
}
Rectangle {
width: parent.width
height: root.powerOptionsExpanded ? 60 : 0
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.powerOptionsExpanded ? 1 : 0
opacity: root.powerOptionsExpanded ? 1 : 0
clip: true
Row {
anchors.centerIn: parent
spacing: Theme.spacingL
visible: root.powerOptionsExpanded
Rectangle {
width: 100
height: 34
radius: Theme.cornerRadius
color: logoutButton.containsMouse ? Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "logout"
size: Theme.fontSizeSmall
color: logoutButton.containsMouse ? Theme.warning : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Logout"
font.pixelSize: Theme.fontSizeSmall
color: logoutButton.containsMouse ? Theme.warning : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: logoutButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.powerOptionsExpanded = false;
root.powerActionRequested("logout", "Logout", "Are you sure you want to logout?");
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Rectangle {
width: 100
height: 34
radius: Theme.cornerRadius
color: rebootButton.containsMouse ? Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "restart_alt"
size: Theme.fontSizeSmall
color: rebootButton.containsMouse ? Theme.warning : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Restart"
font.pixelSize: Theme.fontSizeSmall
color: rebootButton.containsMouse ? Theme.warning : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: rebootButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.powerOptionsExpanded = false;
root.powerActionRequested("reboot", "Restart", "Are you sure you want to restart?");
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Rectangle {
width: 100
height: 34
radius: Theme.cornerRadius
color: suspendButton.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "bedtime"
size: Theme.fontSizeSmall
color: suspendButton.containsMouse ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Suspend"
font.pixelSize: Theme.fontSizeSmall
color: suspendButton.containsMouse ? Theme.primary : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: suspendButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.powerOptionsExpanded = false;
root.powerActionRequested("suspend", "Suspend", "Are you sure you want to suspend?");
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Rectangle {
width: 100
height: 34
radius: Theme.cornerRadius
color: shutdownButton.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "power_settings_new"
size: Theme.fontSizeSmall
color: shutdownButton.containsMouse ? Theme.error : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Shutdown"
font.pixelSize: Theme.fontSizeSmall
color: shutdownButton.containsMouse ? Theme.error : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: shutdownButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.powerOptionsExpanded = false;
root.powerActionRequested("poweroff", "Shutdown", "Are you sure you want to shutdown?");
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Rectangle {
width: parent.width
height: tabBar.height + Theme.spacingM * 2
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.15)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06)
border.width: 1
DankTabBar {
id: tabBar
anchors.centerIn: parent
width: parent.width - Theme.spacingM * 2
tabHeight: 40
currentIndex: {
let tabs = ["network", "audio"];
if (BluetoothService.available)
tabs.push("bluetooth");
tabs.push("display");
return tabs.indexOf(root.currentTab);
}
model: {
let tabs = [{
"text": "Network",
"icon": "wifi",
"id": "network"
}];
tabs.push({
"text": "Audio",
"icon": "volume_up",
"id": "audio"
});
if (BluetoothService.available)
tabs.push({
"text": "Bluetooth",
"icon": "bluetooth",
"id": "bluetooth"
});
tabs.push({
"text": "Display",
"icon": "brightness_6",
"id": "display"
});
return tabs;
}
onTabClicked: function(index) {
let tabs = ["network", "audio"];
if (BluetoothService.available)
tabs.push("bluetooth");
tabs.push("display");
root.currentTab = tabs[index];
}
}
}
}
Rectangle {
width: parent.width
Layout.fillHeight: true
radius: Theme.cornerRadiusLarge
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
NetworkTab {
anchors.fill: parent
anchors.margins: Theme.spacingS
visible: root.currentTab === "network"
}
AudioTab {
anchors.fill: parent
anchors.margins: Theme.spacingS
visible: root.currentTab === "audio"
}
BluetoothTab {
anchors.fill: parent
anchors.margins: Theme.spacingS
visible: BluetoothService.available && root.currentTab === "bluetooth"
}
Column {
property var brightnessDebounceTimer
brightnessDebounceTimer: Timer {
property int pendingValue: 0
interval: BrightnessService.ddcAvailable ? 500 : 50
repeat: false
onTriggered: {
BrightnessService.setBrightness(pendingValue);
}
}
anchors.fill: parent
anchors.margins: Theme.spacingS
visible: root.currentTab === "display"
spacing: Theme.spacingL
Column {
width: parent.width
spacing: Theme.spacingS
visible: BrightnessService.brightnessAvailable
StyledText {
text: "Brightness"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
DankSlider {
width: parent.width
height: 24
value: BrightnessService.brightnessLevel
leftIcon: "brightness_low"
rightIcon: "brightness_high"
enabled: BrightnessService.brightnessAvailable
showValue: true
onSliderValueChanged: function(newValue) {
parent.parent.brightnessDebounceTimer.pendingValue = newValue;
parent.parent.brightnessDebounceTimer.restart();
}
onSliderDragFinished: function(finalValue) {
parent.parent.brightnessDebounceTimer.stop();
BrightnessService.setBrightness(finalValue);
}
}
StyledText {
text: "ddc changes can be slow to respond"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
visible: BrightnessService.ddcAvailable && !BrightnessService.laptopBacklightAvailable
anchors.horizontalCenter: parent.horizontalCenter
}
}
DankToggle {
width: parent.width
text: "Night Mode"
description: "Apply warm color temperature to reduce eye strain"
checked: Prefs.nightModeEnabled
onToggled: (checked) => {
Prefs.setNightModeEnabled(checked);
}
}
DankToggle {
width: parent.width
text: "Light Mode"
description: "Use light theme instead of dark theme"
checked: Prefs.isLightMode
onToggled: (checked) => {
Prefs.setLightMode(checked);
Theme.isLightMode = checked;
}
}
}
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration // Faster for height changes
easing.type: Theme.standardEasing
}
}
}
}
MouseArea {
anchors.fill: parent
z: -1
onClicked: function(mouse) {
var localPos = mapToItem(contentLoader, mouse.x, mouse.y);
if (localPos.x < 0 || localPos.x > contentLoader.width || localPos.y < 0 || localPos.y > contentLoader.height)
controlCenterVisible = false;
}
}
}
+216
View File
@@ -0,0 +1,216 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Modules
import qs.Services
import qs.Widgets
ScrollView {
id: displayTab
property var brightnessDebounceTimer
brightnessDebounceTimer: Timer {
property int pendingValue: 0
interval: BrightnessService.ddcAvailable ? 500 : 50 // 500ms for slow DDC (i2c), 50ms for fast laptop backlight
repeat: false
onTriggered: {
BrightnessService.setBrightness(pendingValue);
}
}
clip: true
Column {
width: parent.width
spacing: Theme.spacingL
Column {
width: parent.width
spacing: Theme.spacingM
visible: BrightnessService.brightnessAvailable
StyledText {
text: "Brightness"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
DankSlider {
width: parent.width
value: BrightnessService.brightnessLevel
leftIcon: "brightness_low"
rightIcon: "brightness_high"
enabled: BrightnessService.brightnessAvailable
onSliderValueChanged: function(newValue) {
brightnessDebounceTimer.pendingValue = newValue;
brightnessDebounceTimer.restart();
}
onSliderDragFinished: function(finalValue) {
brightnessDebounceTimer.stop();
BrightnessService.setBrightness(finalValue);
}
}
StyledText {
text: "using ddc - changes may take a moment to apply"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
visible: BrightnessService.ddcAvailable && !BrightnessService.laptopBacklightAvailable
anchors.horizontalCenter: parent.horizontalCenter
}
}
Column {
width: parent.width
spacing: Theme.spacingM
StyledText {
text: "Display Settings"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
Row {
width: parent.width
spacing: Theme.spacingM
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: 80
radius: Theme.cornerRadius
color: Prefs.nightModeEnabled ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : (nightModeToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
border.color: Prefs.nightModeEnabled ? Theme.primary : "transparent"
border.width: Prefs.nightModeEnabled ? 1 : 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: Prefs.nightModeEnabled ? "nightlight" : "dark_mode"
size: Theme.iconSizeLarge
color: Prefs.nightModeEnabled ? Theme.primary : Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: "Night Mode"
font.pixelSize: Theme.fontSizeMedium
color: Prefs.nightModeEnabled ? Theme.primary : Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
}
MouseArea {
id: nightModeToggle
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (Prefs.nightModeEnabled) {
nightModeDisableProcess.running = true;
Prefs.setNightModeEnabled(false);
} else {
nightModeEnableProcess.running = true;
Prefs.setNightModeEnabled(true);
}
}
}
}
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: 80
radius: Theme.cornerRadius
color: Theme.isLightMode ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : (lightModeToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
border.color: Theme.isLightMode ? Theme.primary : "transparent"
border.width: Theme.isLightMode ? 1 : 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: Theme.isLightMode ? "light_mode" : "palette"
size: Theme.iconSizeLarge
color: Theme.isLightMode ? Theme.primary : Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: Theme.isLightMode ? "Light Mode" : "Dark Mode"
font.pixelSize: Theme.fontSizeMedium
color: Theme.isLightMode ? Theme.primary : Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
}
MouseArea {
id: lightModeToggle
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
Theme.toggleLightMode();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
Process {
id: nightModeEnableProcess
command: ["bash", "-c", "if command -v wlsunset > /dev/null; then pkill wlsunset; wlsunset -t 3000 & elif command -v redshift > /dev/null; then pkill redshift; redshift -P -O 3000 & else echo 'No night mode tool available'; fi"]
running: false
onExited: (exitCode) => {
if (exitCode !== 0) {
Prefs.setNightModeEnabled(false);
}
}
}
Process {
id: nightModeDisableProcess
command: ["bash", "-c", "pkill wlsunset; pkill redshift; if command -v wlsunset > /dev/null; then wlsunset -t 6500 -T 6500 & sleep 1; pkill wlsunset; elif command -v redshift > /dev/null; then redshift -P -O 6500; redshift -x; fi"]
running: false
onExited: (exitCode) => {
if (exitCode !== 0)
}
}
}
@@ -0,0 +1,128 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: ethernetCard
width: parent.width
height: 80
radius: Theme.cornerRadius
color: {
if (ethernetPreferenceArea.containsMouse && NetworkService.ethernetConnected && NetworkService.wifiEnabled && NetworkService.networkStatus !== "ethernet")
return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.8);
return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.5);
}
border.color: NetworkService.networkStatus === "ethernet" ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: NetworkService.networkStatus === "ethernet" ? 2 : 1
Column {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
anchors.right: ethernetToggle.left
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingS
Row {
spacing: Theme.spacingM
DankIcon {
name: "lan"
size: Theme.iconSize
color: NetworkService.networkStatus === "ethernet" ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Ethernet"
font.pixelSize: Theme.fontSizeMedium
color: NetworkService.networkStatus === "ethernet" ? Theme.primary : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
}
}
StyledText {
text: NetworkService.ethernetConnected ? (NetworkService.ethernetIP || "Connected") : "Disconnected"
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
leftPadding: Theme.iconSize + Theme.spacingM
elide: Text.ElideRight
}
}
DankIcon {
id: ethernetLoadingSpinner
name: "refresh"
size: Theme.iconSize - 4
color: Theme.primary
anchors.right: ethernetToggle.left
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
visible: NetworkService.changingPreference && NetworkService.targetPreference === "ethernet"
z: 10
RotationAnimation {
target: ethernetLoadingSpinner
property: "rotation"
running: ethernetLoadingSpinner.visible
from: 0
to: 360
duration: 1000
loops: Animation.Infinite
}
}
DankToggle {
id: ethernetToggle
checked: NetworkService.ethernetConnected
enabled: true
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
onClicked: {
NetworkService.toggleNetworkConnection("ethernet");
}
}
MouseArea {
id: ethernetPreferenceArea
anchors.fill: parent
anchors.rightMargin: 60 // Exclude toggle area
hoverEnabled: true
cursorShape: (NetworkService.ethernetConnected && NetworkService.wifiEnabled && NetworkService.networkStatus !== "ethernet") ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: NetworkService.ethernetConnected && NetworkService.wifiEnabled && NetworkService.networkStatus !== "ethernet" && !NetworkService.changingNetworkPreference
onClicked: {
if (NetworkService.ethernetConnected && NetworkService.wifiEnabled) {
if (NetworkService.networkStatus !== "ethernet")
NetworkService.setNetworkPreference("ethernet");
else
NetworkService.setNetworkPreference("auto");
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
+181
View File
@@ -0,0 +1,181 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: wifiCard
property var refreshTimer
function getWiFiSignalIcon(signalStrength) {
switch (signalStrength) {
case "excellent":
return "wifi";
case "good":
return "wifi_2_bar";
case "fair":
return "wifi_1_bar";
case "poor":
return "signal_wifi_0_bar";
default:
return "wifi";
}
}
width: parent.width
height: 80
radius: Theme.cornerRadius
color: {
if (wifiPreferenceArea.containsMouse && NetworkService.ethernetConnected && NetworkService.wifiEnabled && NetworkService.networkStatus !== "wifi")
return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.8);
return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.5);
}
border.color: NetworkService.networkStatus === "wifi" ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: NetworkService.networkStatus === "wifi" ? 2 : 1
visible: NetworkService.wifiAvailable
Column {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
anchors.right: wifiToggle.left
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingS
Row {
spacing: Theme.spacingM
DankIcon {
name: {
if (!NetworkService.wifiEnabled)
return "wifi_off";
else if (NetworkService.currentWifiSSID !== "")
return getWiFiSignalIcon(NetworkService.wifiSignalStrength);
else
return "wifi";
}
size: Theme.iconSize
color: NetworkService.networkStatus === "wifi" ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: {
if (!NetworkService.wifiEnabled)
return "WiFi is off";
else if (NetworkService.wifiEnabled && NetworkService.currentWifiSSID)
return NetworkService.currentWifiSSID || "Connected";
else
return "Not Connected";
}
font.pixelSize: Theme.fontSizeMedium
color: NetworkService.networkStatus === "wifi" ? Theme.primary : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
}
}
StyledText {
text: {
if (!NetworkService.wifiEnabled)
return "Turn on WiFi to see networks";
else if (NetworkService.wifiEnabled && NetworkService.currentWifiSSID)
return NetworkService.wifiIP || "Connected";
else
return "Select a network below";
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
leftPadding: Theme.iconSize + Theme.spacingM
elide: Text.ElideRight
}
}
DankIcon {
id: wifiLoadingSpinner
name: "refresh"
size: Theme.iconSize - 4
color: Theme.primary
anchors.right: wifiToggle.left
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
visible: NetworkService.changingPreference && NetworkService.targetPreference === "wifi"
z: 10
RotationAnimation {
target: wifiLoadingSpinner
property: "rotation"
running: wifiLoadingSpinner.visible
from: 0
to: 360
duration: 1000
loops: Animation.Infinite
}
}
DankToggle {
id: wifiToggle
checked: NetworkService.wifiEnabled
enabled: true
toggling: NetworkService.wifiToggling
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
onClicked: {
if (NetworkService.wifiEnabled) {
NetworkService.currentWifiSSID = "";
NetworkService.wifiSignalStrength = "excellent";
NetworkService.wifiNetworks = [];
NetworkService.savedWifiNetworks = [];
NetworkService.connectionStatus = "";
NetworkService.connectingSSID = "";
NetworkService.isScanning = false;
NetworkService.refreshNetworkStatus();
}
NetworkService.toggleWifiRadio();
if (refreshTimer)
refreshTimer.triggered = true;
}
}
MouseArea {
id: wifiPreferenceArea
anchors.fill: parent
anchors.rightMargin: 60 // Exclude toggle area
hoverEnabled: true
cursorShape: (NetworkService.ethernetConnected && NetworkService.wifiEnabled && NetworkService.networkStatus !== "wifi") ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: NetworkService.ethernetConnected && NetworkService.wifiEnabled && NetworkService.networkStatus !== "wifi" && !NetworkService.changingNetworkPreference
onClicked: {
if (NetworkService.ethernetConnected && NetworkService.wifiEnabled) {
if (NetworkService.networkStatus !== "wifi")
NetworkService.setNetworkPreference("wifi");
else
NetworkService.setNetworkPreference("auto");
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
@@ -0,0 +1,288 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: wifiContextMenuWindow
property var networkData: null
property bool menuVisible: false
property var parentItem
property var wifiPasswordModalRef
property var networkInfoModalRef
function show(x, y) {
const menuWidth = 160;
wifiContextMenuWindow.visible = true;
Qt.callLater(() => {
const menuHeight = wifiMenuColumn.implicitHeight + Theme.spacingS * 2;
let finalX = x - menuWidth / 2;
let finalY = y + 4;
finalX = Math.max(Theme.spacingS, Math.min(finalX, parentItem.width - menuWidth - Theme.spacingS));
finalY = Math.max(Theme.spacingS, Math.min(finalY, parentItem.height - menuHeight - Theme.spacingS));
if (finalY + menuHeight > parentItem.height - Theme.spacingS) {
finalY = y - menuHeight - 4;
finalY = Math.max(Theme.spacingS, finalY);
}
wifiContextMenuWindow.x = finalX;
wifiContextMenuWindow.y = finalY;
wifiContextMenuWindow.menuVisible = true;
});
}
function hide() {
wifiContextMenuWindow.menuVisible = false;
Qt.callLater(() => {
wifiContextMenuWindow.visible = false;
});
}
visible: false
width: 160
height: wifiMenuColumn.implicitHeight + Theme.spacingS * 2
radius: Theme.cornerRadiusLarge
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
Component.onCompleted: {
menuVisible = false;
visible = 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
}
Column {
id: wifiMenuColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 1
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadiusSmall
color: connectWifiArea.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: wifiContextMenuWindow.networkData && wifiContextMenuWindow.networkData.connected ? "wifi_off" : "wifi"
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: wifiContextMenuWindow.networkData && wifiContextMenuWindow.networkData.connected ? "Disconnect" : "Connect"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: connectWifiArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (wifiContextMenuWindow.networkData) {
if (wifiContextMenuWindow.networkData.connected) {
NetworkService.disconnectWifi();
} else {
if (wifiContextMenuWindow.networkData.saved) {
NetworkService.connectToWifi(wifiContextMenuWindow.networkData.ssid);
} else if (wifiContextMenuWindow.networkData.secured) {
if (wifiPasswordModalRef) {
wifiPasswordModalRef.wifiPasswordSSID = wifiContextMenuWindow.networkData.ssid;
wifiPasswordModalRef.wifiPasswordInput = "";
wifiPasswordModalRef.wifiPasswordModalVisible = true;
}
} else {
NetworkService.connectToWifi(wifiContextMenuWindow.networkData.ssid);
}
}
}
wifiContextMenuWindow.hide();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Rectangle {
width: parent.width - Theme.spacingS * 2
height: 5
anchors.horizontalCenter: parent.horizontalCenter
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
}
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadiusSmall
color: forgetWifiArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
visible: wifiContextMenuWindow.networkData && (wifiContextMenuWindow.networkData.saved || wifiContextMenuWindow.networkData.connected)
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "delete"
size: Theme.iconSize - 2
color: forgetWifiArea.containsMouse ? Theme.error : Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Forget Network"
font.pixelSize: Theme.fontSizeSmall
color: forgetWifiArea.containsMouse ? Theme.error : Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: forgetWifiArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (wifiContextMenuWindow.networkData)
NetworkService.forgetWifiNetwork(wifiContextMenuWindow.networkData.ssid);
wifiContextMenuWindow.hide();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadiusSmall
color: infoWifiArea.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: "info"
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Network Info"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: infoWifiArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (wifiContextMenuWindow.networkData && networkInfoModalRef)
networkInfoModalRef.showNetworkInfo(wifiContextMenuWindow.networkData.ssid, wifiContextMenuWindow.networkData);
wifiContextMenuWindow.hide();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
@@ -0,0 +1,299 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Column {
id: root
property var wifiContextMenuWindow
property var sortedWifiNetworks
property var wifiPasswordModalRef
function getWiFiSignalIcon(signalStrength) {
switch (signalStrength) {
case "excellent":
return "wifi";
case "good":
return "wifi_2_bar";
case "fair":
return "wifi_1_bar";
case "poor":
return "signal_wifi_0_bar";
default:
return "wifi";
}
}
anchors.top: parent.top
anchors.topMargin: 100
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
visible: NetworkService.wifiEnabled
spacing: Theme.spacingS
Row {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: "Available Networks"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - 170
height: 1
}
Rectangle {
width: 28
height: 28
radius: 14
color: refreshAreaSpan.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : NetworkService.isScanning ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06) : "transparent"
DankIcon {
id: refreshIconSpan
anchors.centerIn: parent
name: "refresh"
size: Theme.iconSize - 6
color: refreshAreaSpan.containsMouse ? Theme.primary : Theme.surfaceText
rotation: NetworkService.isScanning ? refreshIconSpan.rotation : 0
RotationAnimation {
target: refreshIconSpan
property: "rotation"
running: NetworkService.isScanning
from: 0
to: 360
duration: 1000
loops: Animation.Infinite
}
Behavior on rotation {
RotationAnimation {
duration: 200
easing.type: Easing.OutQuad
}
}
}
MouseArea {
id: refreshAreaSpan
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!NetworkService.isScanning) {
refreshIconSpan.rotation += 30;
NetworkService.scanWifi();
}
}
}
}
}
Flickable {
width: parent.width
height: parent.height - 40
clip: true
contentWidth: width
contentHeight: spanningNetworksColumn.height
boundsBehavior: Flickable.DragAndOvershootBounds
flickDeceleration: 8000
maximumFlickVelocity: 15000
Column {
id: spanningNetworksColumn
width: parent.width
spacing: Theme.spacingXS
Repeater {
model: NetworkService.wifiAvailable && NetworkService.wifiEnabled ? sortedWifiNetworks : []
Rectangle {
width: spanningNetworksColumn.width
height: 38
radius: Theme.cornerRadiusSmall
color: networkArea2.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : modelData.connected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
border.color: modelData.connected ? Theme.primary : "transparent"
border.width: modelData.connected ? 1 : 0
Item {
anchors.fill: parent
anchors.margins: Theme.spacingXS
anchors.rightMargin: Theme.spacingM // Extra right margin for scrollbar
DankIcon {
id: signalIcon2
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
name: getWiFiSignalIcon(modelData.signalStrength)
size: Theme.iconSize - 2
color: modelData.connected ? Theme.primary : Theme.surfaceText
}
Column {
anchors.left: signalIcon2.right
anchors.leftMargin: Theme.spacingXS
anchors.right: rightIcons2.left
anchors.rightMargin: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
width: parent.width
text: modelData.ssid
font.pixelSize: Theme.fontSizeSmall
color: modelData.connected ? Theme.primary : Theme.surfaceText
font.weight: modelData.connected ? Font.Medium : Font.Normal
elide: Text.ElideRight
}
StyledText {
width: parent.width
text: {
if (modelData.connected)
return "Connected";
if (NetworkService.connectionStatus === "connecting" && NetworkService.connectingSSID === modelData.ssid)
return "Connecting...";
if (NetworkService.connectionStatus === "invalid_password" && NetworkService.connectingSSID === modelData.ssid)
return "Invalid password";
if (modelData.saved)
return "Saved" + (modelData.secured ? " • Secured" : " • Open");
return modelData.secured ? "Secured" : "Open";
}
font.pixelSize: Theme.fontSizeSmall - 1
color: {
if (NetworkService.connectionStatus === "connecting" && NetworkService.connectingSSID === modelData.ssid)
return Theme.primary;
if (NetworkService.connectionStatus === "invalid_password" && NetworkService.connectingSSID === modelData.ssid)
return Theme.error;
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7);
}
elide: Text.ElideRight
}
}
Row {
id: rightIcons2
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
DankIcon {
name: "lock"
size: Theme.iconSize - 8
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
visible: modelData.secured
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
id: wifiMenuButton
width: 24
height: 24
radius: 12
color: wifiMenuButtonArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
DankIcon {
name: "more_vert"
size: Theme.iconSize - 8
color: Theme.surfaceText
opacity: 0.6
anchors.centerIn: parent
}
MouseArea {
id: wifiMenuButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
wifiContextMenuWindow.networkData = modelData;
let buttonCenter = wifiMenuButtonArea.width / 2;
let buttonBottom = wifiMenuButtonArea.height;
let globalPos = wifiMenuButtonArea.mapToItem(wifiContextMenuWindow.parentItem, buttonCenter, buttonBottom);
Qt.callLater(() => {
wifiContextMenuWindow.show(globalPos.x, globalPos.y);
});
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
}
}
MouseArea {
id: networkArea2
anchors.fill: parent
anchors.rightMargin: 32 // Exclude menu button area
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData.connected)
return ;
if (modelData.saved) {
NetworkService.connectToWifi(modelData.ssid);
} else if (modelData.secured) {
if (wifiPasswordModalRef) {
wifiPasswordModalRef.wifiPasswordSSID = modelData.ssid;
wifiPasswordModalRef.wifiPasswordInput = "";
wifiPasswordModalRef.wifiPasswordModalVisible = true;
}
} else {
NetworkService.connectToWifi(modelData.ssid);
}
}
}
}
}
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
}
}
+304
View File
@@ -0,0 +1,304 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.ControlCenter.Network
Item {
id: networkTab
property var wifiPasswordModalRef: wifiPasswordModal
property var networkInfoModalRef: networkInfoModal
property var sortedWifiNetworks: {
if (!NetworkService.wifiAvailable || !NetworkService.wifiEnabled) {
return [];
}
var allNetworks = NetworkService.wifiNetworks;
var savedNetworks = NetworkService.savedWifiNetworks;
var currentSSID = NetworkService.currentWifiSSID;
var signalStrength = NetworkService.wifiSignalStrength;
var refreshTrigger = forceRefresh; // Force recalculation
var networks = [...allNetworks];
networks.forEach(function(network) {
network.connected = (network.ssid === currentSSID);
network.saved = savedNetworks.some(function(saved) {
return saved.ssid === network.ssid;
});
if (network.connected && signalStrength) {
network.signalStrength = signalStrength;
}
});
networks.sort(function(a, b) {
if (a.connected && !b.connected) return -1;
if (!a.connected && b.connected) return 1;
return b.signal - a.signal;
});
return networks;
}
property int forceRefresh: 0
Connections {
target: NetworkService
function onNetworksUpdated() {
forceRefresh++;
}
}
Component.onCompleted: {
NetworkService.addRef();
NetworkService.autoRefreshEnabled = true;
if (NetworkService.wifiEnabled)
NetworkService.scanWifi();
wifiMonitorTimer.start();
}
Component.onDestruction: {
NetworkService.removeRef();
NetworkService.autoRefreshEnabled = false;
}
Row {
anchors.fill: parent
spacing: Theme.spacingM
Column {
width: (parent.width - Theme.spacingM) / 2
height: parent.height
spacing: Theme.spacingS
Flickable {
width: parent.width
height: parent.height - 30
clip: true
contentWidth: width
contentHeight: wifiContent.height
boundsBehavior: Flickable.DragAndOvershootBounds
flickDeceleration: 8000
maximumFlickVelocity: 15000
Column {
id: wifiContent
width: parent.width
spacing: Theme.spacingM
WiFiCard {
refreshTimer: refreshTimer
}
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
}
}
Column {
width: (parent.width - Theme.spacingM) / 2
height: parent.height
spacing: Theme.spacingS
Flickable {
width: parent.width
height: parent.height - 30
clip: true
contentWidth: width
contentHeight: ethernetContent.height
boundsBehavior: Flickable.StopAtBounds
flickDeceleration: 8000
maximumFlickVelocity: 15000
Column {
id: ethernetContent
width: parent.width
spacing: Theme.spacingM
EthernetCard {
}
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
}
}
}
Rectangle {
anchors.top: parent.top
anchors.topMargin: 100
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
color: "transparent"
visible: !NetworkService.wifiEnabled
Column {
anchors.centerIn: parent
spacing: Theme.spacingM
DankIcon {
anchors.horizontalCenter: parent.horizontalCenter
name: "wifi_off"
size: 48
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3)
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: "WiFi is turned off"
font.pixelSize: Theme.fontSizeLarge
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
font.weight: Font.Medium
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: "Turn on WiFi to see networks"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
}
}
}
WiFiNetworksList {
wifiContextMenuWindow: wifiContextMenuWindow
sortedWifiNetworks: networkTab.sortedWifiNetworks
wifiPasswordModalRef: networkTab.wifiPasswordModalRef
}
Timer {
id: refreshTimer
interval: 2000
running: visible && refreshTimer.triggered
property bool triggered: false
onTriggered: {
NetworkService.refreshNetworkStatus();
if (NetworkService.wifiEnabled && !NetworkService.isScanning) {
NetworkService.scanWifi();
}
triggered = false;
}
}
Connections {
target: NetworkService
function onWifiEnabledChanged() {
if (NetworkService.wifiEnabled && visible) {
wifiScanDelayTimer.start();
wifiMonitorTimer.start();
} else {
NetworkService.currentWifiSSID = "";
NetworkService.wifiSignalStrength = "excellent";
NetworkService.wifiNetworks = [];
NetworkService.savedWifiNetworks = [];
NetworkService.connectionStatus = "";
NetworkService.connectingSSID = "";
NetworkService.isScanning = false;
NetworkService.refreshNetworkStatus();
wifiMonitorTimer.stop();
}
}
}
Timer {
id: wifiScanDelayTimer
interval: 1500
running: false
repeat: false
onTriggered: {
if (NetworkService.wifiEnabled && visible) {
if (!NetworkService.isScanning) {
NetworkService.scanWifi();
} else {
wifiRetryTimer.start();
}
}
}
}
Timer {
id: wifiRetryTimer
interval: 2000
running: false
repeat: false
onTriggered: {
if (NetworkService.wifiEnabled && visible && NetworkService.wifiNetworks.length === 0) {
if (!NetworkService.isScanning) {
NetworkService.scanWifi();
}
}
}
}
Timer {
id: wifiMonitorTimer
interval: 8000 // Check every 8 seconds
running: false
repeat: true
onTriggered: {
if (!visible || !NetworkService.wifiEnabled) {
running = false;
return;
}
var shouldScan = false;
var reason = "";
if (NetworkService.networkStatus !== "wifi") {
shouldScan = true;
reason = "not connected to WiFi";
}
else if (NetworkService.wifiNetworks.length === 0) {
shouldScan = true;
reason = "no networks cached";
}
if (shouldScan && !NetworkService.isScanning) {
NetworkService.scanWifi();
}
}
}
onVisibleChanged: {
if (visible && NetworkService.wifiEnabled) {
wifiMonitorTimer.start();
} else {
wifiMonitorTimer.stop();
}
}
WiFiContextMenu {
id: wifiContextMenuWindow
parentItem: networkTab
wifiPasswordModalRef: networkTab.wifiPasswordModalRef
networkInfoModalRef: networkTab.networkInfoModalRef
}
MouseArea {
anchors.fill: parent
visible: wifiContextMenuWindow.visible
onClicked: {
wifiContextMenuWindow.hide();
}
MouseArea {
x: wifiContextMenuWindow.x
y: wifiContextMenuWindow.y
width: wifiContextMenuWindow.width
height: wifiContextMenuWindow.height
onClicked: {
}
}
}
}
+303
View File
@@ -0,0 +1,303 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Widgets
PanelWindow {
id: root
property bool powerMenuVisible: false
property bool powerConfirmVisible: false
property string powerConfirmAction: ""
property string powerConfirmTitle: ""
property string powerConfirmMessage: ""
visible: powerMenuVisible
implicitWidth: 400
implicitHeight: 320
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
MouseArea {
anchors.fill: parent
onClicked: {
powerMenuVisible = false;
}
}
Rectangle {
width: Math.min(320, parent.width - Theme.spacingL * 2)
height: 320 // Fixed height to prevent cropping
x: Math.max(Theme.spacingL, parent.width - width - Theme.spacingL)
y: Theme.barHeight + Theme.spacingXS
color: Theme.popupBackground()
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
opacity: powerMenuVisible ? 1 : 0
scale: powerMenuVisible ? 1 : 0.85
MouseArea {
anchors.fill: parent
onClicked: {
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
StyledText {
text: "Power Options"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - 150
height: 1
}
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
hoverColor: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
onClicked: {
powerMenuVisible = false;
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: logoutArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "logout"
size: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Log Out"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: logoutArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerMenuVisible = false;
root.powerConfirmAction = "logout";
root.powerConfirmTitle = "Log Out";
root.powerConfirmMessage = "Are you sure you want to log out?";
root.powerConfirmVisible = true;
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: suspendArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "bedtime"
size: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Suspend"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: suspendArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerMenuVisible = false;
root.powerConfirmAction = "suspend";
root.powerConfirmTitle = "Suspend";
root.powerConfirmMessage = "Are you sure you want to suspend the system?";
root.powerConfirmVisible = true;
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: rebootArea.containsMouse ? Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "restart_alt"
size: Theme.iconSize
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Reboot"
font.pixelSize: Theme.fontSizeMedium
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: rebootArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerMenuVisible = false;
root.powerConfirmAction = "reboot";
root.powerConfirmTitle = "Reboot";
root.powerConfirmMessage = "Are you sure you want to reboot the system?";
root.powerConfirmVisible = true;
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: powerOffArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "power_settings_new"
size: Theme.iconSize
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Power Off"
font.pixelSize: Theme.fontSizeMedium
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: powerOffArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerMenuVisible = false;
root.powerConfirmAction = "poweroff";
root.powerConfirmTitle = "Power Off";
root.powerConfirmMessage = "Are you sure you want to power off the system?";
root.powerConfirmVisible = true;
}
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
+188
View File
@@ -0,0 +1,188 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
PanelWindow {
id: dock
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
property var modelData
property var contextMenu
property var windowsMenu
property bool autoHide: Prefs.dockAutoHide
property real backgroundTransparency: Prefs.dockTransparency
property bool contextMenuOpen: (contextMenu && contextMenu.visible && contextMenu.screen === modelData) || (windowsMenu && windowsMenu.visible && windowsMenu.screen === modelData)
property bool windowIsFullscreen: {
if (!NiriService.focusedWindowId || !NiriService.niriAvailable) return false
var focusedWindow = NiriService.windows.find(w => w.id === NiriService.focusedWindowId)
if (!focusedWindow) return false
var fullscreenApps = ["vlc", "mpv", "kodi", "steam", "lutris", "wine", "dosbox"]
return fullscreenApps.some(app => focusedWindow.app_id && focusedWindow.app_id.toLowerCase().includes(app))
}
property bool reveal: (!autoHide || dockMouseArea.containsMouse || dockApps.requestDockShow || contextMenuOpen) && !windowIsFullscreen
Connections {
target: Prefs
function onDockTransparencyChanged() {
dock.backgroundTransparency = Prefs.dockTransparency;
}
}
screen: modelData
visible: Prefs.showDock
color: "transparent"
anchors {
bottom: true
left: true
right: true
}
margins {
left: 0
right: 0
}
implicitHeight: 100
exclusiveZone: autoHide ? -1 : 65 - 16
mask: Region {
item: dockMouseArea
}
MouseArea {
id: dockMouseArea
height: dock.reveal ? 65 : 12
anchors {
bottom: parent.bottom
horizontalCenter: parent.horizontalCenter
}
implicitWidth: dock.reveal ? dockBackground.width + 32 : (dockBackground.width + 32)
hoverEnabled: true
Behavior on height {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
Item {
id: dockContainer
anchors.fill: parent
transform: Translate {
id: dockSlide
y: dock.reveal ? 0 : 60
Behavior on y {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
}
Rectangle {
id: dockBackground
objectName: "dockBackground"
anchors {
top: parent.top
bottom: parent.bottom
horizontalCenter: parent.horizontalCenter
}
width: dockApps.implicitWidth + 12
height: parent.height - 8
anchors.topMargin: 4
anchors.bottomMargin: 1
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, backgroundTransparency)
radius: Theme.cornerRadiusXLarge
border.width: 1
border.color: Theme.outlineMedium
layer.enabled: true
Rectangle {
anchors.fill: parent
color: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g, Theme.surfaceTint.b, 0.04)
radius: parent.radius
}
DockApps {
id: dockApps
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: 4
anchors.bottomMargin: 4
contextMenu: dock.contextMenu
windowsMenu: dock.windowsMenu
}
}
Rectangle {
id: appTooltip
property var hoveredButton: {
if (!dockApps.children[0]) return null
var row = dockApps.children[0]
var repeater = null
for (var i = 0; i < row.children.length; i++) {
var child = row.children[i]
if (child && typeof child.count !== "undefined" && typeof child.itemAt === "function") {
repeater = child
break
}
}
if (!repeater || !repeater.itemAt) return null
for (var i = 0; i < repeater.count; i++) {
var item = repeater.itemAt(i)
if (item && item.dockButton && item.dockButton.showTooltip) {
return item.dockButton
}
}
return null
}
property string tooltipText: hoveredButton ? hoveredButton.tooltipText : ""
visible: hoveredButton !== null && tooltipText !== ""
width: tooltipLabel.implicitWidth + 24
height: tooltipLabel.implicitHeight + 12
color: Theme.surfaceContainer
radius: Theme.cornerRadius
border.width: 1
border.color: Theme.outlineMedium
y: -height - 8
x: hoveredButton ? hoveredButton.mapToItem(dockContainer, hoveredButton.width/2, 0).x - width/2 : 0
StyledText {
id: tooltipLabel
anchors.centerIn: parent
text: appTooltip.tooltipText
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
}
}
}
}
}
+289
View File
@@ -0,0 +1,289 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property var appData
property var contextMenu: null
property var windowsMenu: null
property var dockApps: null
property int index: -1
property bool longPressing: false
property bool dragging: false
property point dragStartPos: Qt.point(0, 0)
property point dragOffset: Qt.point(0, 0)
property int targetIndex: -1
property int originalIndex: -1
width: 40
height: 40
property bool isHovered: mouseArea.containsMouse && !dragging
transform: Translate {
id: translateY
y: 0
}
SequentialAnimation {
id: bounceAnimation
running: false
NumberAnimation {
target: translateY
property: "y"
to: -10
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedAccel
}
NumberAnimation {
target: translateY
property: "y"
to: -8
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedDecel
}
}
NumberAnimation {
id: exitAnimation
running: false
target: translateY
property: "y"
to: 0
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedDecel
}
onIsHoveredChanged: {
if (isHovered) {
exitAnimation.stop()
if (!bounceAnimation.running) {
bounceAnimation.restart()
}
} else {
bounceAnimation.stop()
exitAnimation.restart()
}
}
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
border.width: 2
border.color: Theme.primary
visible: dragging
z: -1
}
Timer {
id: longPressTimer
interval: 500
repeat: false
onTriggered: {
if (appData && appData.isPinned) {
longPressing = true
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
anchors.bottomMargin: -20
hoverEnabled: true
cursorShape: longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onPressed: (mouse) => {
if (mouse.button === Qt.LeftButton && appData && appData.isPinned) {
dragStartPos = Qt.point(mouse.x, mouse.y)
longPressTimer.start()
}
}
onReleased: (mouse) => {
longPressTimer.stop()
if (longPressing) {
if (dragging && targetIndex >= 0 && targetIndex !== originalIndex && dockApps) {
dockApps.movePinnedApp(originalIndex, targetIndex)
}
longPressing = false
dragging = false
dragOffset = Qt.point(0, 0)
targetIndex = -1
originalIndex = -1
}
}
onPositionChanged: (mouse) => {
if (longPressing && !dragging) {
var distance = Math.sqrt(Math.pow(mouse.x - dragStartPos.x, 2) + Math.pow(mouse.y - dragStartPos.y, 2))
if (distance > 5) {
dragging = true
targetIndex = index
originalIndex = index
}
}
if (dragging) {
dragOffset = Qt.point(mouse.x - dragStartPos.x, mouse.y - dragStartPos.y)
if (dockApps) {
var threshold = 40
var newTargetIndex = targetIndex
if (dragOffset.x > threshold && targetIndex < dockApps.pinnedAppCount - 1) {
newTargetIndex = targetIndex + 1
} else if (dragOffset.x < -threshold && targetIndex > 0) {
newTargetIndex = targetIndex - 1
}
if (newTargetIndex !== targetIndex) {
targetIndex = newTargetIndex
dragStartPos = Qt.point(mouse.x, mouse.y)
}
}
}
}
onClicked: (mouse) => {
if (!appData || longPressing) return
if (mouse.button === Qt.LeftButton) {
var windowCount = appData.windows ? appData.windows.count : 0
if (windowCount === 0) {
if (appData && appData.appId) {
var desktopEntry = DesktopEntries.byId(appData.appId)
if (desktopEntry) {
Prefs.addAppUsage({
id: appData.appId,
name: desktopEntry.name || appData.appId,
icon: desktopEntry.icon || "",
exec: desktopEntry.exec || "",
comment: desktopEntry.comment || ""
})
}
Quickshell.execDetached(["gtk-launch", appData.appId])
}
} else if (windowCount === 1) {
var window = appData.windows.get(0)
NiriService.focusWindow(window.id)
} else {
windowsMenu.showForButton(root, appData, 40)
}
} else if (mouse.button === Qt.MiddleButton) {
if (appData && appData.appId) {
var desktopEntry = DesktopEntries.byId(appData.appId)
if (desktopEntry) {
Prefs.addAppUsage({
id: appData.appId,
name: desktopEntry.name || appData.appId,
icon: desktopEntry.icon || "",
exec: desktopEntry.exec || "",
comment: desktopEntry.comment || ""
})
}
Quickshell.execDetached(["gtk-launch", appData.appId])
}
} else if (mouse.button === Qt.RightButton) {
if (contextMenu) {
contextMenu.showForButton(root, appData, 40)
}
}
}
}
property bool showTooltip: mouseArea.containsMouse && !dragging
property string tooltipText: {
if (!appData || !appData.appId) return ""
var desktopEntry = DesktopEntries.byId(appData.appId)
return desktopEntry && desktopEntry.name ? desktopEntry.name : appData.appId
}
IconImage {
id: iconImg
width: 40
height: 40
anchors.centerIn: parent
source: {
if (!appData || !appData.appId) return ""
var desktopEntry = DesktopEntries.byId(appData.appId)
if (desktopEntry && desktopEntry.icon) {
var iconPath = Quickshell.iconPath(desktopEntry.icon, Prefs.iconTheme === "System Default" ? "" : Prefs.iconTheme)
return iconPath
}
return ""
}
smooth: true
mipmap: true
asynchronous: true
visible: status === Image.Ready
implicitSize: 40
}
Rectangle {
width: 40
height: 40
anchors.centerIn: parent
visible: !iconImg.visible
color: Theme.surfaceLight
radius: Theme.cornerRadiusLarge
border.width: 1
border.color: Theme.primarySelected
Text {
anchors.centerIn: parent
text: {
if (!appData || !appData.appId) return "?"
var desktopEntry = DesktopEntries.byId(appData.appId)
if (desktopEntry && desktopEntry.name) {
return desktopEntry.name.charAt(0).toUpperCase()
}
return appData.appId.charAt(0).toUpperCase()
}
font.pixelSize: 14
color: Theme.primary
font.weight: Font.Bold
}
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: -2
spacing: 2
Repeater {
model: appData && appData.windows ? Math.min(appData.windows.count, 4) : 0
Rectangle {
width: appData && appData.windows && appData.windows.count <= 3 ? 5 : 3
height: 2
radius: 1
color: {
if (!appData || !appData.windows || appData.windows.count === 0) return "transparent"
var window = appData.windows.get(index)
return window && window.id == NiriService.focusedWindowId ? Theme.primary :
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
}
}
}
}
}
+157
View File
@@ -0,0 +1,157 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property var contextMenu: null
property var windowsMenu: null
property bool requestDockShow: false
property int pinnedAppCount: 0
implicitWidth: row.width
implicitHeight: row.height
function movePinnedApp(fromIndex, toIndex) {
if (fromIndex === toIndex) return
var currentPinned = [...Prefs.pinnedApps]
if (fromIndex < 0 || fromIndex >= currentPinned.length || toIndex < 0 || toIndex >= currentPinned.length) return
var movedApp = currentPinned.splice(fromIndex, 1)[0]
currentPinned.splice(toIndex, 0, movedApp)
Prefs.setPinnedApps(currentPinned)
}
Row {
id: row
spacing: 2
anchors.centerIn: parent
height: 40
Repeater {
id: repeater
model: ListModel {
id: dockModel
Component.onCompleted: updateModel()
function updateModel() {
clear()
var items = []
var runningApps = NiriService.getRunningAppIds()
var pinnedApps = [...Prefs.pinnedApps]
var addedApps = new Set()
pinnedApps.forEach(appId => {
var lowerAppId = appId.toLowerCase()
if (!addedApps.has(lowerAppId)) {
var windows = NiriService.getWindowsByAppId(appId)
items.push({
appId: appId,
windows: windows,
isPinned: true,
isRunning: windows.length > 0
})
addedApps.add(lowerAppId)
}
})
root.pinnedAppCount = pinnedApps.length
var appUsageRanking = Prefs.appUsageRanking || {}
var allUnpinnedApps = []
for (var appId in appUsageRanking) {
var lowerAppId = appId.toLowerCase()
if (!addedApps.has(lowerAppId)) {
allUnpinnedApps.push({
appId: appId,
lastUsed: appUsageRanking[appId].lastUsed || 0,
usageCount: appUsageRanking[appId].usageCount || 0
})
}
}
allUnpinnedApps.sort((a, b) => b.lastUsed - a.lastUsed)
var unpinnedApps = []
var recentToAdd = Math.min(3, allUnpinnedApps.length)
for (var i = 0; i < recentToAdd; i++) {
var appId = allUnpinnedApps[i].appId
var lowerAppId = appId.toLowerCase()
unpinnedApps.push(appId)
addedApps.add(lowerAppId)
}
if (pinnedApps.length > 0 && unpinnedApps.length > 0) {
items.push({
appId: "__SEPARATOR__",
windows: [],
isPinned: false,
isRunning: false
})
}
unpinnedApps.forEach(appId => {
var windows = NiriService.getWindowsByAppId(appId)
items.push({
appId: appId,
windows: windows,
isPinned: false,
isRunning: windows.length > 0
})
})
items.forEach(item => {
append(item)
})
}
}
delegate: Item {
id: delegateItem
property alias dockButton: button
width: model.appId === "__SEPARATOR__" ? 16 : 40
height: 40
Rectangle {
visible: model.appId === "__SEPARATOR__"
width: 2
height: 20
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
radius: 1
anchors.centerIn: parent
}
DockAppButton {
id: button
visible: model.appId !== "__SEPARATOR__"
anchors.centerIn: parent
width: 40
height: 40
appData: model
contextMenu: root.contextMenu
windowsMenu: root.windowsMenu
dockApps: root
index: model.index
}
}
}
}
Connections {
target: NiriService
function onWindowsChanged() { dockModel.updateModel() }
function onWindowOpenedOrChanged() { dockModel.updateModel() }
}
Connections {
target: Prefs
function onPinnedAppsChanged() { dockModel.updateModel() }
}
}
+283
View File
@@ -0,0 +1,283 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
PanelWindow {
id: root
property bool showContextMenu: false
property var appData: null
property var anchorItem: null
property real dockVisibleHeight: 40
property int margin: 10
function showForButton(button, data, dockHeight) {
anchorItem = button
appData = data
dockVisibleHeight = dockHeight || 40
var dockWindow = button.Window.window
if (dockWindow) {
for (var i = 0; i < Quickshell.screens.length; i++) {
var s = Quickshell.screens[i]
if (dockWindow.x >= s.x && dockWindow.x < s.x + s.width) {
root.screen = s
break
}
}
}
showContextMenu = true
}
function close() { showContextMenu = false }
screen: Quickshell.screens[0]
visible: showContextMenu
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors { top: true; left: true; right: true; bottom: true }
property point anchorPos: Qt.point(screen.width/2, screen.height - 100)
onAnchorItemChanged: updatePosition()
onVisibleChanged: if (visible) updatePosition()
function updatePosition() {
if (!anchorItem) {
anchorPos = Qt.point(screen.width/2, screen.height - 100)
return
}
var dockWindow = anchorItem.Window.window
if (!dockWindow) {
anchorPos = Qt.point(screen.width/2, screen.height - 100)
return
}
var buttonPosInDock = anchorItem.mapToItem(dockWindow.contentItem, 0, 0)
var actualDockHeight = root.dockVisibleHeight // fallback
function findDockBackground(item) {
if (item.objectName === "dockBackground") {
return item
}
for (var i = 0; i < item.children.length; i++) {
var found = findDockBackground(item.children[i])
if (found) return found
}
return null
}
var dockBackground = findDockBackground(dockWindow.contentItem)
if (dockBackground) {
actualDockHeight = dockBackground.height
}
var dockBottomMargin = 16 // The dock has bottom margin
var buttonScreenY = root.screen.height - actualDockHeight - dockBottomMargin - 20
var dockContentWidth = dockWindow.width
var screenWidth = root.screen.width
var dockLeftMargin = Math.round((screenWidth - dockContentWidth) / 2)
var buttonScreenX = dockLeftMargin + buttonPosInDock.x + anchorItem.width/2
anchorPos = Qt.point(buttonScreenX, buttonScreenY)
}
Rectangle {
id: menuContainer
width: Math.min(400, Math.max(200, menuColumn.implicitWidth + Theme.spacingS * 2))
height: Math.max(60, menuColumn.implicitHeight + Theme.spacingS * 2)
x: {
var left = 10
var right = root.width - width - 10
var want = root.anchorPos.x - width/2
return Math.max(left, Math.min(right, want))
}
y: Math.max(10, root.anchorPos.y - height + 30)
color: Theme.popupBackground()
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
opacity: showContextMenu ? 1 : 0
scale: showContextMenu ? 1 : 0.85
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
}
Column {
id: menuColumn
width: parent.width - Theme.spacingS * 2
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: Theme.spacingS
spacing: 1
Rectangle {
width: parent.width
height: 28
radius: Theme.cornerRadiusSmall
color: pinArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: root.appData && root.appData.isPinned ? "Unpin from Dock" : "Pin to Dock"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
MouseArea {
id: pinArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!root.appData) return
if (root.appData.isPinned) {
Prefs.removePinnedApp(root.appData.appId)
} else {
Prefs.addPinnedApp(root.appData.appId)
}
root.close()
}
}
}
Rectangle {
visible: root.appData && root.appData.windows && root.appData.windows.count > 0
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
Repeater {
model: root.appData && root.appData.windows ? root.appData.windows : null
Rectangle {
required property var model
width: menuColumn.width
height: 28
radius: Theme.cornerRadiusSmall
color: windowArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: model.title || "Untitled Window"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
MouseArea {
id: windowArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
NiriService.focusWindow(model.id)
root.close()
}
}
}
}
Rectangle {
visible: root.appData && root.appData.windows && root.appData.windows.count > 1
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
Rectangle {
visible: root.appData && root.appData.windows && root.appData.windows.count > 1
width: parent.width
height: 28
radius: Theme.cornerRadiusSmall
color: closeAllArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: "Close All Windows"
font.pixelSize: Theme.fontSizeSmall
color: closeAllArea.containsMouse ? Theme.error : Theme.surfaceText
font.weight: Font.Normal
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
MouseArea {
id: closeAllArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!root.appData || !root.appData.windows) return
for (var i = 0; i < root.appData.windows.count; i++) {
var window = root.appData.windows.get(i)
NiriService.closeWindow(window.id)
}
root.close()
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
MouseArea {
anchors.fill: parent
z: -1
onClicked: {
root.close()
}
}
}
+200
View File
@@ -0,0 +1,200 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
PanelWindow {
id: root
property bool showWindowsMenu: false
property var appData: null
property var anchorItem: null
property real dockVisibleHeight: 40
property int margin: 10
function showForButton(button, data, dockHeight) {
anchorItem = button
appData = data
dockVisibleHeight = dockHeight || 40
var dockWindow = button.Window.window
if (dockWindow) {
for (var i = 0; i < Quickshell.screens.length; i++) {
var s = Quickshell.screens[i]
if (dockWindow.x >= s.x && dockWindow.x < s.x + s.width) {
root.screen = s
break
}
}
}
showWindowsMenu = true
}
function close() { showWindowsMenu = false }
screen: Quickshell.screens[0]
visible: showWindowsMenu
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors { top: true; left: true; right: true; bottom: true }
property point anchorPos: Qt.point(screen.width/2, screen.height - 100)
onAnchorItemChanged: updatePosition()
onVisibleChanged: if (visible) updatePosition()
function updatePosition() {
if (!anchorItem) {
anchorPos = Qt.point(screen.width/2, screen.height - 100)
return
}
var dockWindow = anchorItem.Window.window
if (!dockWindow) {
anchorPos = Qt.point(screen.width/2, screen.height - 100)
return
}
var buttonPosInDock = anchorItem.mapToItem(dockWindow.contentItem, 0, 0)
var actualDockHeight = root.dockVisibleHeight // fallback
function findDockBackground(item) {
if (item.objectName === "dockBackground") {
return item
}
for (var i = 0; i < item.children.length; i++) {
var found = findDockBackground(item.children[i])
if (found) return found
}
return null
}
var dockBackground = findDockBackground(dockWindow.contentItem)
if (dockBackground) {
actualDockHeight = dockBackground.height
}
var dockBottomMargin = 16 // The dock has bottom margin
var buttonScreenY = root.screen.height - actualDockHeight - dockBottomMargin - 20
var dockContentWidth = dockWindow.width
var screenWidth = root.screen.width
var dockLeftMargin = Math.round((screenWidth - dockContentWidth) / 2)
var buttonScreenX = dockLeftMargin + buttonPosInDock.x + anchorItem.width/2
anchorPos = Qt.point(buttonScreenX, buttonScreenY)
}
Rectangle {
id: menuContainer
width: Math.min(600, Math.max(250, windowColumn.implicitWidth + Theme.spacingS * 2))
height: Math.max(60, windowColumn.implicitHeight + Theme.spacingS * 2)
x: {
var left = 10
var right = root.width - width - 10
var want = root.anchorPos.x - width/2
return Math.max(left, Math.min(right, want))
}
y: Math.max(10, root.anchorPos.y - height + 30)
color: Theme.popupBackground()
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
opacity: showWindowsMenu ? 1 : 0
scale: showWindowsMenu ? 1 : 0.85
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
}
Column {
id: windowColumn
width: parent.width - Theme.spacingS * 2
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: Theme.spacingS
spacing: 1
Repeater {
model: root.appData && root.appData.windows ? root.appData.windows : null
Rectangle {
required property var model
width: windowColumn.width
height: 32
radius: Theme.cornerRadiusSmall
color: windowArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: model.title || "Untitled Window"
font.pixelSize: Theme.fontSizeSmall
color: model.is_focused ? Theme.primary : Theme.surfaceText
font.weight: model.is_focused ? Font.Medium : Font.Normal
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
MouseArea {
id: windowArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
NiriService.focusWindow(model.id)
root.close()
}
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
}
MouseArea {
anchors.fill: parent
z: -1
hoverEnabled: false
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: {
root.close()
}
}
}
+63
View File
@@ -0,0 +1,63 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import qs.Common
Item {
id: root
function activate() {
loader.activeAsync = true
}
LazyLoader {
id: loader
WlSessionLock {
id: lock
property bool unlocked: false
property string sharedPasswordBuffer: ""
locked: true
onLockedChanged: {
if (!locked)
loader.active = false
}
LockSurface {
lock: lock
sharedPasswordBuffer: lock.sharedPasswordBuffer
onPasswordChanged: (newPassword) => {
lock.sharedPasswordBuffer = newPassword
}
}
}
}
LockScreenDemo {
id: demoWindow
}
IpcHandler {
target: "lock"
function lock(): void {
console.log("Lock screen requested via IPC")
loader.activeAsync = true
}
function demo(): void {
console.log("Lock screen DEMO mode requested via IPC")
demoWindow.showDemo()
}
function isLocked(): bool {
return loader.active
}
}
}
+932
View File
@@ -0,0 +1,932 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Effects
import Quickshell
import Quickshell.Services.Pam
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modals
Item {
id: root
property string passwordBuffer: ""
property bool demoMode: false
property bool unlocking: false
property var powerModal: null
property string confirmAction: ""
property var facts: [
"Light takes 100,000 years to escape the Sun's core, then just 8 minutes to reach us.",
"A teaspoon of neutron star matter would weigh a billion tons here on Earth.",
"Right now, 100 trillion solar neutrinos are passing through your body every second.",
"The Sun burns 4 million tons of itself into pure energy every second.",
"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.",
"Some cosmic radio bursts release more energy in milliseconds than our Sun does in 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.",
"A black hole the mass of our Sun would take longer to evaporate than the current age of the universe... times a trillion trillion trillion trillion trillion.",
"The fastest spinning star we know rotates 716 times per second.",
"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."
]
property string randomFact: ""
signal unlockRequested()
Component.onCompleted: {
pickRandomFact()
WeatherService.addRef()
UserInfoService.refreshUserInfo()
}
function pickRandomFact() {
randomFact = facts[Math.floor(Math.random() * facts.length)]
}
onDemoModeChanged: {
if (demoMode) {
pickRandomFact()
}
}
Component.onDestruction: {
WeatherService.removeRef()
}
Image {
id: wallpaperBackground
anchors.fill: parent
source: Prefs.wallpaperPath || ""
fillMode: Image.PreserveAspectCrop
smooth: true
asynchronous: true
cache: true
visible: source !== ""
layer.enabled: true
layer.effect: MultiEffect {
autoPaddingEnabled: false
blurEnabled: true
blur: 0.8
blurMax: 32
blurMultiplier: 1.0
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
}
Rectangle {
anchors.fill: parent
color: "black"
opacity: 0.4
}
Rectangle {
anchors.fill: parent
color: "transparent"
Item {
anchors.centerIn: parent
anchors.verticalCenterOffset: -100
width: 400
height: 140
StyledText {
id: clockText
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
text: Prefs.use24HourClock ? Qt.formatTime(new Date(), "H:mm") : Qt.formatTime(new Date(), "h:mm AP")
font.pixelSize: 120
font.weight: Font.Light
color: "white"
lineHeight: 0.8
Timer {
interval: 1000
running: true
repeat: true
onTriggered: parent.text = Prefs.use24HourClock ? Qt.formatTime(new Date(), "H:mm") : Qt.formatTime(new Date(), "h:mm AP")
}
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: clockText.bottom
anchors.topMargin: -20
text: Qt.formatDate(new Date(), "dddd, MMMM d")
font.pixelSize: Theme.fontSizeXLarge
color: "white"
opacity: 0.9
}
}
ColumnLayout {
anchors.centerIn: parent
anchors.verticalCenterOffset: 50
spacing: Theme.spacingM
width: 380
RowLayout {
spacing: Theme.spacingL
Layout.fillWidth: true
Item {
id: avatarContainer
Layout.preferredWidth: 60
Layout.preferredHeight: 60
property bool hasImage: profileImageLoader.status === Image.Ready
Rectangle {
anchors.fill: parent
radius: width / 2
color: "transparent"
border.color: Theme.primary
border.width: 1
visible: parent.hasImage
}
Image {
id: profileImageLoader
source: {
if (Prefs.profileImage === "")
return ""
if (Prefs.profileImage.startsWith("/"))
return "file://" + Prefs.profileImage
return Prefs.profileImage
}
smooth: true
asynchronous: true
mipmap: true
cache: true
visible: false
}
MultiEffect {
anchors.fill: parent
anchors.margins: 5
source: profileImageLoader
maskEnabled: true
maskSource: circularMask
visible: avatarContainer.hasImage
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
Item {
id: circularMask
width: 60 - 10
height: 60 - 10
layer.enabled: true
layer.smooth: true
visible: false
Rectangle {
anchors.fill: parent
radius: width / 2
color: "black"
antialiasing: true
}
}
Rectangle {
anchors.fill: parent
radius: width / 2
color: Theme.primary
visible: !parent.hasImage
DankIcon {
anchors.centerIn: parent
name: "person"
size: Theme.iconSize + 4
color: Theme.primaryText
}
}
DankIcon {
anchors.centerIn: parent
name: "warning"
size: Theme.iconSize + 4
color: Theme.primaryText
visible: Prefs.profileImage !== "" && profileImageLoader.status === Image.Error
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 60
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.9)
border.color: passwordField.activeFocus ? Theme.primary : Qt.rgba(1, 1, 1, 0.3)
border.width: passwordField.activeFocus ? 2 : 1
property bool showPassword: false
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
DankIcon {
id: lockIcon
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
name: "lock"
size: 20
color: passwordField.activeFocus ? Theme.primary : Theme.surfaceVariantText
}
TextInput {
id: passwordField
anchors.fill: parent
anchors.leftMargin: lockIcon.width + Theme.spacingM * 2
anchors.rightMargin: (revealButton.visible ? revealButton.width + Theme.spacingM : 0) + (enterButton.visible ? enterButton.width + Theme.spacingM : 0) + (loadingSpinner.visible ? loadingSpinner.width + Theme.spacingM : Theme.spacingM)
opacity: 0
focus: !demoMode
enabled: !demoMode
echoMode: parent.showPassword ? TextInput.Normal : TextInput.Password
Timer {
id: focusTimer
interval: 100
running: !demoMode
onTriggered: passwordField.forceActiveFocus()
}
onTextChanged: {
if (!demoMode) {
root.passwordBuffer = text
}
}
onAccepted: {
if (!demoMode && root.passwordBuffer.length > 0 && !pam.active) {
console.log("Enter pressed, starting PAM authentication")
pam.start()
}
}
Keys.onPressed: event => {
if (demoMode) return
if (pam.active) {
console.log("PAM is active, ignoring input")
event.accepted = true
return
}
}
}
StyledText {
id: placeholder
anchors.left: lockIcon.right
anchors.leftMargin: Theme.spacingM
anchors.right: (revealButton.visible ? revealButton.left : (enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right)))
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
property string pamState: ""
text: {
if (demoMode) return ""
if (root.unlocking) return "Unlocking..."
if (pam.active) return "Authenticating..."
return "hunter2"
}
color: root.unlocking ? Theme.primary : (pam.active ? Theme.primary : Theme.outline)
font.pixelSize: Theme.fontSizeMedium
opacity: (demoMode || root.passwordBuffer.length === 0) ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
StyledText {
anchors.left: lockIcon.right
anchors.leftMargin: Theme.spacingM
anchors.right: (revealButton.visible ? revealButton.left : (enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right)))
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: {
if (demoMode) {
return "••••••••"
} else if (parent.showPassword) {
return root.passwordBuffer
} else {
return "•".repeat(Math.min(root.passwordBuffer.length, 25))
}
}
color: Theme.surfaceText
font.pixelSize: parent.showPassword ? Theme.fontSizeMedium : Theme.fontSizeLarge
opacity: (demoMode || root.passwordBuffer.length > 0) ? 1 : 0
elide: Text.ElideRight
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
}
DankActionButton {
id: revealButton
anchors.right: enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right)
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
iconName: parent.showPassword ? "visibility_off" : "visibility"
buttonSize: 32
visible: !demoMode && root.passwordBuffer.length > 0 && !pam.active && !root.unlocking
enabled: visible
onClicked: parent.showPassword = !parent.showPassword
}
Rectangle {
id: loadingSpinner
anchors.right: enterButton.visible ? enterButton.left : parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
width: 24
height: 24
radius: 12
color: "transparent"
visible: !demoMode && (pam.active || root.unlocking)
DankIcon {
anchors.centerIn: parent
name: "check_circle"
size: 20
color: Theme.primary
visible: root.unlocking
SequentialAnimation on scale {
running: root.unlocking
NumberAnimation { from: 0; to: 1.2; duration: Anims.durShort; easing.type: Easing.BezierSpline; easing.bezierCurve: Anims.emphasizedDecel }
NumberAnimation { from: 1.2; to: 1; duration: Anims.durShort; easing.type: Easing.BezierSpline; easing.bezierCurve: Anims.emphasizedAccel }
}
}
Item {
anchors.fill: parent
visible: pam.active && !root.unlocking
Rectangle {
width: 20
height: 20
radius: 10
anchors.centerIn: parent
color: "transparent"
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
border.width: 2
}
Rectangle {
width: 20
height: 20
radius: 10
anchors.centerIn: parent
color: "transparent"
border.color: Theme.primary
border.width: 2
Rectangle {
width: parent.width
height: parent.height / 2
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.9)
}
RotationAnimation on rotation {
running: pam.active && !root.unlocking
loops: Animation.Infinite
duration: Anims.durLong
from: 0
to: 360
}
}
}
}
DankActionButton {
id: enterButton
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
iconName: "keyboard_return"
buttonSize: 36
visible: (demoMode || (root.passwordBuffer.length > 0 && !pam.active && !root.unlocking))
enabled: !demoMode
onClicked: {
if (!demoMode) {
console.log("Enter button clicked, starting PAM authentication")
pam.start()
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
StyledText {
Layout.fillWidth: true
Layout.preferredHeight: placeholder.pamState ? 20 : 0
text: {
if (placeholder.pamState === "error") return "Authentication error - try again"
if (placeholder.pamState === "max") return "Too many attempts - locked out"
if (placeholder.pamState === "fail") return "Incorrect password - try again"
return ""
}
color: Theme.error
font.pixelSize: Theme.fontSizeSmall
horizontalAlignment: Text.AlignHCenter
visible: placeholder.pamState !== ""
opacity: placeholder.pamState !== "" ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on Layout.preferredHeight {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
StyledText {
anchors.top: parent.top
anchors.left: parent.left
anchors.margins: Theme.spacingXL
text: "DEMO MODE - Click anywhere to exit"
font.pixelSize: Theme.fontSizeSmall
color: "white"
opacity: 0.7
visible: demoMode
}
StyledText {
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: Theme.spacingXL
text: WeatherService.weather.available && WeatherService.weather.city && WeatherService.weather.city !== "Unknown" ?
`${WeatherService.weather.city} ${(Prefs.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp)}°${(Prefs.useFahrenheit ? "F" : "C")}` :
""
font.pixelSize: Theme.fontSizeMedium
color: "white"
horizontalAlignment: Text.AlignRight
visible: text !== ""
}
StyledText {
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.margins: Theme.spacingXL
text: BatteryService.batteryAvailable ? `Battery: ${BatteryService.batteryLevel}%` : ""
font.pixelSize: Theme.fontSizeMedium
color: "white"
visible: text !== ""
}
Row {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.margins: Theme.spacingXL
spacing: Theme.spacingL
DankActionButton {
iconName: "power_settings_new"
iconColor: Theme.error
buttonSize: 40
onClicked: {
if (demoMode) {
console.log("Demo: Power")
} else {
powerDialog.open()
}
}
}
DankActionButton {
iconName: "refresh"
buttonSize: 40
onClicked: {
if (demoMode) {
console.log("Demo: Reboot")
} else {
rebootDialog.open()
}
}
}
DankActionButton {
iconName: "logout"
buttonSize: 40
onClicked: {
if (demoMode) {
console.log("Demo: Logout")
} else {
logoutDialog.open()
}
}
}
}
StyledText {
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.margins: Theme.spacingL
width: Math.min(parent.width - Theme.spacingXL * 2, implicitWidth)
text: randomFact
font.pixelSize: Theme.fontSizeSmall
color: "white"
opacity: 0.8
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.NoWrap
visible: randomFact !== ""
}
}
PamContext {
id: pam
onResponseRequiredChanged: {
if (demoMode) return
console.log("PAM response required:", responseRequired)
if (!responseRequired) return
console.log("Responding to PAM with password buffer length:", root.passwordBuffer.length)
respond(root.passwordBuffer)
}
onCompleted: res => {
if (demoMode) return
console.log("PAM authentication completed with result:", res)
if (res === PamResult.Success) {
console.log("Authentication successful, unlocking")
root.unlocking = true
passwordField.text = ""
root.passwordBuffer = ""
root.unlockRequested()
return
}
console.log("Authentication failed:", res)
if (res === PamResult.Error)
placeholder.pamState = "error"
else if (res === PamResult.MaxTries)
placeholder.pamState = "max"
else if (res === PamResult.Failed)
placeholder.pamState = "fail"
placeholderDelay.restart()
}
}
Timer {
id: placeholderDelay
interval: 4000
onTriggered: placeholder.pamState = ""
}
MouseArea {
anchors.fill: parent
enabled: demoMode
onClicked: root.unlockRequested()
}
Rectangle {
id: powerDialog
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.8)
visible: false
z: 1000
function open() { visible = true }
function close() { visible = false }
Rectangle {
anchors.centerIn: parent
width: 320
height: 180
radius: Theme.cornerRadiusLarge
color: Theme.surfaceContainer
border.color: Theme.outline
border.width: 1
Column {
anchors.centerIn: parent
spacing: Theme.spacingXL
DankIcon {
anchors.horizontalCenter: parent.horizontalCenter
name: "power_settings_new"
size: 32
color: Theme.error
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: "Power off this computer?"
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
Rectangle {
width: 100
height: 40
radius: Theme.cornerRadius
color: cancelMouse1.pressed ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.7) :
cancelMouse1.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.9) : Theme.surfaceVariant
StyledText {
anchors.centerIn: parent
text: "Cancel"
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
}
MouseArea {
id: cancelMouse1
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: powerDialog.close()
}
}
Rectangle {
width: 100
height: 40
radius: Theme.cornerRadius
color: powerMouse.pressed ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.8) :
powerMouse.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 1.0) : Theme.error
StyledText {
anchors.centerIn: parent
text: "Power Off"
color: "white"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
}
MouseArea {
id: powerMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerDialog.close()
Quickshell.execDetached(["systemctl", "poweroff"])
}
}
}
}
}
}
}
Rectangle {
id: rebootDialog
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.8)
visible: false
z: 1000
function open() { visible = true }
function close() { visible = false }
Rectangle {
anchors.centerIn: parent
width: 320
height: 180
radius: Theme.cornerRadiusLarge
color: Theme.surfaceContainer
border.color: Theme.outline
border.width: 1
Column {
anchors.centerIn: parent
spacing: Theme.spacingXL
DankIcon {
anchors.horizontalCenter: parent.horizontalCenter
name: "refresh"
size: 32
color: Theme.primary
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: "Restart this computer?"
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
Rectangle {
width: 100
height: 40
radius: Theme.cornerRadius
color: cancelMouse2.pressed ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.7) :
cancelMouse2.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.9) : Theme.surfaceVariant
StyledText {
anchors.centerIn: parent
text: "Cancel"
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
}
MouseArea {
id: cancelMouse2
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: rebootDialog.close()
}
}
Rectangle {
width: 100
height: 40
radius: Theme.cornerRadius
color: rebootMouse.pressed ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.8) :
rebootMouse.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 1.0) : Theme.primary
StyledText {
anchors.centerIn: parent
text: "Restart"
color: "white"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
}
MouseArea {
id: rebootMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
rebootDialog.close()
Quickshell.execDetached(["systemctl", "reboot"])
}
}
}
}
}
}
}
Rectangle {
id: logoutDialog
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.8)
visible: false
z: 1000
function open() { visible = true }
function close() { visible = false }
Rectangle {
anchors.centerIn: parent
width: 320
height: 180
radius: Theme.cornerRadiusLarge
color: Theme.surfaceContainer
border.color: Theme.outline
border.width: 1
Column {
anchors.centerIn: parent
spacing: Theme.spacingXL
DankIcon {
anchors.horizontalCenter: parent.horizontalCenter
name: "logout"
size: 32
color: Theme.primary
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: "End this session?"
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
Rectangle {
width: 100
height: 40
radius: Theme.cornerRadius
color: cancelMouse3.pressed ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.7) :
cancelMouse3.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.9) : Theme.surfaceVariant
StyledText {
anchors.centerIn: parent
text: "Cancel"
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
}
MouseArea {
id: cancelMouse3
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: logoutDialog.close()
}
}
Rectangle {
width: 100
height: 40
radius: Theme.cornerRadius
color: logoutMouse.pressed ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.8) :
logoutMouse.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 1.0) : Theme.primary
StyledText {
anchors.centerIn: parent
text: "Log Out"
color: "white"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
}
MouseArea {
id: logoutMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
logoutDialog.close()
Quickshell.execDetached(["niri", "msg", "action", "quit", "-s"])
}
}
}
}
}
}
}
}
+52
View File
@@ -0,0 +1,52 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Modals
PanelWindow {
id: root
property bool demoActive: false
visible: demoActive
anchors {
top: true
bottom: true
left: true
right: true
}
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
color: "transparent"
function showDemo(): void {
console.log("Showing lock screen demo")
demoActive = true
}
function hideDemo(): void {
console.log("Hiding lock screen demo")
demoActive = false
}
PowerConfirmModal {
id: powerModal
}
Loader {
anchors.fill: parent
active: demoActive
sourceComponent: LockScreenContent {
demoMode: true
powerModal: powerModal
onUnlockRequested: root.hideDemo()
}
}
}
+56
View File
@@ -0,0 +1,56 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Modals
WlSessionLockSurface {
id: root
required property WlSessionLock lock
required property string sharedPasswordBuffer
signal passwordChanged(string newPassword)
property bool thisLocked: false
readonly property bool locked: thisLocked && !lock.unlocked
function unlock(): void {
console.log("LockSurface.unlock() called")
lock.unlocked = true
animDelay.start()
}
Component.onCompleted: {
thisLocked = true
}
color: "transparent"
Timer {
id: animDelay
interval: 1500 // Longer delay for success feedback
onTriggered: root.lock.locked = false
}
PowerConfirmModal {
id: powerModal
}
Loader {
anchors.fill: parent
sourceComponent: LockScreenContent {
demoMode: false
powerModal: powerModal
passwordBuffer: root.sharedPasswordBuffer
onUnlockRequested: root.unlock()
onPasswordBufferChanged: {
if (root.sharedPasswordBuffer !== passwordBuffer) {
root.passwordChanged(passwordBuffer)
}
}
}
}
}
@@ -0,0 +1,655 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
property var notificationGroup
property bool expanded: NotificationService.expandedGroups[notificationGroup?.key] || false
property bool descriptionExpanded: NotificationService.expandedMessages[notificationGroup?.latestNotification?.notification?.id + "_desc"] || false
property bool userInitiatedExpansion: false
width: parent ? parent.width : 400
height: {
if (expanded) {
return expandedContent.height + 28;
}
const baseHeight = 116;
if (descriptionExpanded) {
return baseHeight + descriptionText.contentHeight - (descriptionText.font.pixelSize * 1.2 * 2);
}
return baseHeight;
}
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.1)
border.color: notificationGroup?.latestNotification?.urgency === 2 ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05)
border.width: notificationGroup?.latestNotification?.urgency === 2 ? 2 : 1
clip: true
Rectangle {
anchors.fill: parent
radius: parent.radius
visible: notificationGroup?.latestNotification?.urgency === 2
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop {
position: 0.0
color: Theme.primary
}
GradientStop {
position: 0.02
color: Theme.primary
}
GradientStop {
position: 0.021
color: "transparent"
}
}
opacity: 1.0
}
Item {
id: collapsedContent
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 12
anchors.leftMargin: 16
anchors.rightMargin: 56
height: 92
visible: !expanded
Rectangle {
id: iconContainer
readonly property bool hasNotificationImage: notificationGroup?.latestNotification?.image && notificationGroup.latestNotification.image !== ""
width: 55
height: 55
radius: 27.5
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
border.color: "transparent"
border.width: 0
anchors.left: parent.left
anchors.top: parent.top
anchors.topMargin: 18
IconImage {
anchors.fill: parent
anchors.margins: 2
source: {
if (parent.hasNotificationImage)
return notificationGroup.latestNotification.cleanImage;
if (notificationGroup?.latestNotification?.appIcon) {
const appIcon = notificationGroup.latestNotification.appIcon;
if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://"))
return appIcon;
return Quickshell.iconPath(appIcon, "");
}
return "";
}
visible: status === Image.Ready
}
StyledText {
anchors.centerIn: parent
visible: !parent.hasNotificationImage && (!notificationGroup?.latestNotification?.appIcon || notificationGroup.latestNotification.appIcon === "")
text: {
const appName = notificationGroup?.appName || "?";
return appName.charAt(0).toUpperCase();
}
font.pixelSize: 20
font.weight: Font.Bold
color: Theme.primaryText
}
Rectangle {
width: 18
height: 18
radius: 9
color: Theme.primary
anchors.top: parent.top
anchors.right: parent.right
anchors.topMargin: -2
anchors.rightMargin: -2
visible: (notificationGroup?.count || 0) > 1
StyledText {
anchors.centerIn: parent
text: (notificationGroup?.count || 0) > 99 ? "99+" : (notificationGroup?.count || 0).toString()
color: Theme.primaryText
font.pixelSize: 9
font.weight: Font.Bold
}
}
}
Rectangle {
id: textContainer
anchors.left: iconContainer.right
anchors.leftMargin: 12
anchors.right: parent.right
anchors.rightMargin: 0
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.bottomMargin: 8
color: "transparent"
Item {
width: parent.width
height: parent.height
anchors.top: parent.top
anchors.topMargin: -2
Column {
width: parent.width
spacing: 2
StyledText {
width: parent.width
text: {
const timeStr = notificationGroup?.latestNotification?.timeStr || "";
if (timeStr.length > 0)
return (notificationGroup?.appName || "") + " • " + timeStr;
else
return notificationGroup?.appName || "";
}
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
elide: Text.ElideRight
maximumLineCount: 1
}
StyledText {
text: notificationGroup?.latestNotification?.summary || ""
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
width: parent.width
elide: Text.ElideRight
maximumLineCount: 1
visible: text.length > 0
}
StyledText {
id: descriptionText
property string fullText: notificationGroup?.latestNotification?.htmlBody || ""
property bool hasMoreText: truncated
text: fullText
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
width: parent.width
elide: Text.ElideRight
maximumLineCount: descriptionExpanded ? -1 : 2
wrapMode: Text.WordWrap
visible: text.length > 0
linkColor: Theme.primary
onLinkActivated: (link) => Qt.openUrlExternally(link)
MouseArea {
anchors.fill: parent
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor :
(parent.hasMoreText || descriptionExpanded) ? Qt.PointingHandCursor :
Qt.ArrowCursor
onClicked: mouse => {
if (!parent.hoveredLink && (parent.hasMoreText || descriptionExpanded)) {
const messageId = notificationGroup?.latestNotification?.notification?.id + "_desc";
NotificationService.toggleMessageExpansion(messageId);
}
}
propagateComposedEvents: true
onPressed: mouse => {
if (parent.hoveredLink) {
mouse.accepted = false;
}
}
onReleased: mouse => {
if (parent.hoveredLink) {
mouse.accepted = false;
}
}
}
}
}
}
}
}
Column {
id: expandedContent
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 14
anchors.bottomMargin: 14
anchors.leftMargin: Theme.spacingL
anchors.rightMargin: Theme.spacingL
spacing: -1
visible: expanded
Item {
width: parent.width
height: 40
Row {
anchors.left: parent.left
anchors.right: parent.right
anchors.rightMargin: 56
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
StyledText {
text: notificationGroup?.appName || ""
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
maximumLineCount: 1
}
Rectangle {
width: 18
height: 18
radius: 9
color: Theme.primary
visible: (notificationGroup?.count || 0) > 1
anchors.verticalCenter: parent.verticalCenter
StyledText {
anchors.centerIn: parent
text: (notificationGroup?.count || 0) > 99 ? "99+" : (notificationGroup?.count || 0).toString()
color: Theme.primaryText
font.pixelSize: 9
font.weight: Font.Bold
}
}
}
}
Column {
width: parent.width
spacing: 16
Repeater {
model: notificationGroup?.notifications?.slice(0, 10) || []
delegate: Rectangle {
required property var modelData
readonly property bool messageExpanded: NotificationService.expandedMessages[modelData?.notification?.id] || false
width: parent.width
height: {
const baseHeight = 120;
if (messageExpanded) {
const twoLineHeight = bodyText.font.pixelSize * 1.2 * 2;
if (bodyText.implicitHeight > twoLineHeight + 2) {
const extraHeight = bodyText.implicitHeight - twoLineHeight;
return baseHeight + extraHeight;
}
}
return baseHeight;
}
radius: Theme.cornerRadiusLarge
color: "transparent"
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05)
border.width: 1
Behavior on height {
enabled: false
}
Item {
anchors.fill: parent
anchors.margins: 12
anchors.bottomMargin: 8
Rectangle {
id: messageIcon
readonly property bool hasNotificationImage: modelData?.image && modelData.image !== ""
width: 32
height: 32
radius: 16
anchors.left: parent.left
anchors.top: parent.top
anchors.topMargin: 32
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
border.width: 1
IconImage {
anchors.fill: parent
anchors.margins: 1
source: {
if (parent.hasNotificationImage)
return modelData.cleanImage;
if (modelData?.appIcon) {
const appIcon = modelData.appIcon;
if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://"))
return appIcon;
return Quickshell.iconPath(appIcon, "");
}
return "";
}
visible: status === Image.Ready
}
StyledText {
anchors.centerIn: parent
visible: !parent.hasNotificationImage && (!modelData?.appIcon || modelData.appIcon === "")
text: {
const appName = modelData?.appName || "?";
return appName.charAt(0).toUpperCase();
}
font.pixelSize: 12
font.weight: Font.Bold
color: Theme.primaryText
}
}
Item {
anchors.left: messageIcon.right
anchors.leftMargin: 12
anchors.right: parent.right
anchors.rightMargin: 12
anchors.top: parent.top
anchors.bottom: parent.bottom
Column {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: buttonArea.top
anchors.bottomMargin: 4
spacing: 2
StyledText {
width: parent.width
text: modelData?.timeStr || ""
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
elide: Text.ElideRight
maximumLineCount: 1
visible: text.length > 0
}
StyledText {
width: parent.width
text: modelData?.summary || ""
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
elide: Text.ElideRight
maximumLineCount: 1
visible: text.length > 0
}
StyledText {
id: bodyText
property bool hasMoreText: truncated
text: modelData?.htmlBody || ""
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
width: parent.width
elide: messageExpanded ? Text.ElideNone : Text.ElideRight
maximumLineCount: messageExpanded ? -1 : 2
wrapMode: Text.WordWrap
visible: text.length > 0
linkColor: Theme.primary
onLinkActivated: (link) => Qt.openUrlExternally(link)
MouseArea {
anchors.fill: parent
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor :
(bodyText.hasMoreText || messageExpanded) ? Qt.PointingHandCursor :
Qt.ArrowCursor
onClicked: mouse => {
if (!parent.hoveredLink && (bodyText.hasMoreText || messageExpanded)) {
NotificationService.toggleMessageExpansion(modelData?.notification?.id || "");
}
}
propagateComposedEvents: true
onPressed: mouse => {
if (parent.hoveredLink) {
mouse.accepted = false;
}
}
onReleased: mouse => {
if (parent.hoveredLink) {
mouse.accepted = false;
}
}
}
}
}
Item {
id: buttonArea
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 30
Row {
anchors.right: parent.right
anchors.bottom: parent.bottom
spacing: 8
Repeater {
model: modelData?.actions || []
Rectangle {
property bool isHovered: false
width: Math.max(actionText.implicitWidth + 12, 50)
height: 24
radius: 4
color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent"
StyledText {
id: actionText
text: modelData.text || ""
color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
anchors.centerIn: parent
elide: Text.ElideRight
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: parent.isHovered = true
onExited: parent.isHovered = false
onClicked: {
if (modelData && modelData.invoke) {
modelData.invoke();
}
}
}
}
}
Rectangle {
property bool isHovered: false
width: Math.max(clearText.implicitWidth + 12, 50)
height: 24
radius: 4
color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent"
StyledText {
id: clearText
text: "Clear"
color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: parent.isHovered = true
onExited: parent.isHovered = false
onClicked: NotificationService.dismissNotification(modelData)
}
}
}
}
}
}
}
}
}
}
Row {
visible: !expanded
anchors.right: clearButton.left
anchors.rightMargin: 8
anchors.bottom: parent.bottom
anchors.bottomMargin: 8
spacing: 8
Repeater {
model: notificationGroup?.latestNotification?.actions || []
Rectangle {
property bool isHovered: false
width: Math.max(actionText.implicitWidth + 12, 50)
height: 24
radius: 4
color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent"
StyledText {
id: actionText
text: modelData.text || ""
color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
anchors.centerIn: parent
elide: Text.ElideRight
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: parent.isHovered = true
onExited: parent.isHovered = false
onClicked: {
if (modelData && modelData.invoke) {
modelData.invoke();
}
}
}
}
}
}
Rectangle {
id: clearButton
property bool isHovered: false
visible: !expanded
anchors.right: parent.right
anchors.rightMargin: 16
anchors.bottom: parent.bottom
anchors.bottomMargin: 8
width: clearText.width + 16
height: clearText.height + 8
radius: 6
color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent"
StyledText {
id: clearText
text: "Clear"
color: clearButton.isHovered ? Theme.primary : Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: clearButton.isHovered = true
onExited: clearButton.isHovered = false
onClicked: NotificationService.dismissGroup(notificationGroup?.key || "")
}
}
MouseArea {
anchors.fill: parent
visible: !expanded && (notificationGroup?.count || 0) > 1 && !descriptionExpanded
onClicked: {
root.userInitiatedExpansion = true
NotificationService.toggleGroupExpansion(notificationGroup?.key || "")
}
z: -1
}
Item {
id: fixedControls
anchors.top: parent.top
anchors.right: parent.right
anchors.topMargin: 12
anchors.rightMargin: 16
width: 60
height: 28
DankActionButton {
anchors.left: parent.left
anchors.top: parent.top
visible: (notificationGroup?.count || 0) > 1
iconName: expanded ? "expand_less" : "expand_more"
iconSize: 18
buttonSize: 28
onClicked: {
root.userInitiatedExpansion = true
NotificationService.toggleGroupExpansion(notificationGroup?.key || "")
}
}
DankActionButton {
anchors.right: parent.right
anchors.top: parent.top
iconName: "close"
iconSize: 18
buttonSize: 28
onClicked: NotificationService.dismissGroup(notificationGroup?.key || "")
}
}
Behavior on height {
enabled: root.userInitiatedExpansion
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
onFinished: root.userInitiatedExpansion = false
}
}
}
@@ -0,0 +1,141 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
PanelWindow {
id: root
property bool notificationHistoryVisible: false
visible: notificationHistoryVisible
onNotificationHistoryVisibleChanged: {
NotificationService.disablePopups(notificationHistoryVisible);
}
implicitWidth: 400
implicitHeight: Math.min(Screen.height * 0.8, 400)
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
MouseArea {
anchors.fill: parent
onClicked: {
notificationHistoryVisible = false;
}
}
Rectangle {
id: mainRect
function calculateHeight() {
let baseHeight = Theme.spacingL * 2;
baseHeight += notificationHeader.height;
baseHeight += Theme.spacingM;
let listHeight = notificationList.listContentHeight;
if (NotificationService.groupedNotifications.length === 0)
listHeight = 200;
baseHeight += Math.min(listHeight, 600);
return Math.max(300, baseHeight);
}
width: 400
height: calculateHeight()
x: Screen.width - width - Theme.spacingL
y: Theme.barHeight + Theme.spacingXS
color: Theme.popupBackground()
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
opacity: notificationHistoryVisible ? 1 : 0
scale: notificationHistoryVisible ? 1 : 0.9
MouseArea {
anchors.fill: parent
onClicked: {
}
}
Column {
id: contentColumn
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
NotificationHeader {
id: notificationHeader
}
NotificationList {
id: notificationList
width: parent.width
height: parent.height - notificationHeader.height - contentColumn.spacing
}
}
Connections {
function onNotificationsChanged() {
mainRect.height = mainRect.calculateHeight();
}
function onGroupedNotificationsChanged() {
mainRect.height = mainRect.calculateHeight();
}
function onExpandedGroupsChanged() {
mainRect.height = mainRect.calculateHeight();
}
function onExpandedMessagesChanged() {
mainRect.height = mainRect.calculateHeight();
}
target: NotificationService
}
Behavior on height {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on opacity {
NumberAnimation {
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
Behavior on scale {
NumberAnimation {
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
}
}
@@ -0,0 +1,36 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
width: parent.width
height: 200
visible: NotificationService.notifications.length === 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
width: parent.width * 0.8
DankIcon {
anchors.horizontalCenter: parent.horizontalCenter
name: "notifications_none"
size: Theme.iconSizeLarge + 16
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3)
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: "Nothing to see here"
font.pixelSize: Theme.fontSizeLarge
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3)
font.weight: Font.Medium
horizontalAlignment: Text.AlignHCenter
}
}
}
@@ -0,0 +1,136 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
width: parent.width
height: 32
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
StyledText {
text: "Notifications"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
DankActionButton {
id: doNotDisturbButton
iconName: Prefs.doNotDisturb ? "notifications_off" : "notifications"
iconColor: Prefs.doNotDisturb ? Theme.error : Theme.surfaceText
buttonSize: 28
anchors.verticalCenter: parent.verticalCenter
onClicked: Prefs.setDoNotDisturb(!Prefs.doNotDisturb)
Rectangle {
id: doNotDisturbTooltip
width: tooltipText.contentWidth + Theme.spacingS * 2
height: tooltipText.contentHeight + Theme.spacingXS * 2
radius: Theme.cornerRadiusSmall
color: Theme.surfaceContainer
border.color: Theme.outline
border.width: 1
anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
visible: doNotDisturbButton.children[1].containsMouse // Access StateLayer's containsMouse
opacity: visible ? 1 : 0
StyledText {
id: tooltipText
text: "Do Not Disturb"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.centerIn: parent
font.hintingPreference: Font.PreferFullHinting
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
Rectangle {
id: clearAllButton
width: 120
height: 28
radius: Theme.cornerRadiusLarge
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
visible: NotificationService.notifications.length > 0
color: clearArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
border.color: clearArea.containsMouse ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "delete_sweep"
size: Theme.iconSizeSmall
color: clearArea.containsMouse ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Clear All"
font.pixelSize: Theme.fontSizeSmall
color: clearArea.containsMouse ? Theme.primary : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: clearArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: NotificationService.clearAllNotifications()
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
@@ -0,0 +1,118 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
ListView {
id: root
property alias count: root.count
readonly property real listContentHeight: root.contentHeight
readonly property bool atYBeginning: root.contentY === 0
property real stableY: 0
property bool isUserScrolling: false
width: parent.width
height: parent.height
clip: true
model: NotificationService.groupedNotifications
spacing: Theme.spacingL
interactive: true
boundsBehavior: Flickable.StopAtBounds
flickDeceleration: 1500
maximumFlickVelocity: 2000
cacheBuffer: 1000
onMovementStarted: isUserScrolling = true
onMovementEnded: {
isUserScrolling = false;
if (contentY > 40)
stableY = contentY;
}
onContentYChanged: {
if (!isUserScrolling && visible && parent.visible && stableY > 40 && Math.abs(contentY - stableY) > 10)
contentY = stableY;
}
NotificationEmptyState {
visible: root.count === 0
anchors.centerIn: parent
}
add: Transition {
enabled: !root.isUserScrolling
ParallelAnimation {
NumberAnimation {
properties: "opacity"
from: 0
to: 1
duration: root.isUserScrolling ? 0 : Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
NumberAnimation {
properties: "height"
from: 0
duration: root.isUserScrolling ? 0 : Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
remove: Transition {
SequentialAnimation {
PauseAnimation {
duration: 50
}
ParallelAnimation {
NumberAnimation {
properties: "opacity"
to: 0
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
NumberAnimation {
properties: "height,anchors.topMargin,anchors.bottomMargin"
to: 0
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
displaced: Transition {
enabled: false
NumberAnimation {
properties: "y"
duration: 0
}
}
move: Transition {
enabled: false
NumberAnimation {
properties: "y"
duration: 0
easing.type: Theme.emphasizedEasing
}
}
delegate: NotificationCard {
notificationGroup: modelData
}
}
@@ -0,0 +1,606 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
PanelWindow {
id: win
required property var notificationData
required property string notificationId
readonly property bool hasValidData: notificationData && notificationData.notification
property int screenY: 0
property bool exiting: false
property bool _isDestroying: false
property bool _finalized: false
signal entered()
signal exitFinished()
function startExit() {
if (exiting || _isDestroying)
return ;
exiting = true;
exitAnim.restart();
exitWatchdog.restart();
if (NotificationService.removeFromVisibleNotifications)
NotificationService.removeFromVisibleNotifications(win.notificationData);
}
function forceExit() {
if (_isDestroying)
return ;
_isDestroying = true;
exiting = true;
visible = false;
exitWatchdog.stop();
finalizeExit("forced");
}
function finalizeExit(reason) {
if (_finalized)
return ;
_finalized = true;
_isDestroying = true;
exitWatchdog.stop();
wrapperConn.enabled = false;
wrapperConn.target = null;
win.exitFinished();
}
visible: hasValidData
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
implicitWidth: 400
implicitHeight: 122
onScreenYChanged: margins.top = Theme.barHeight + 4 + screenY
onHasValidDataChanged: {
if (!hasValidData && !exiting && !_isDestroying) {
forceExit();
}
}
Component.onCompleted: {
if (hasValidData) {
Qt.callLater(() => {
return enterX.restart();
});
} else {
forceExit();
}
}
onNotificationDataChanged: {
if (!_isDestroying) {
wrapperConn.target = win.notificationData || null;
notificationConn.target = (win.notificationData && win.notificationData.notification && win.notificationData.notification.Retainable) || null;
}
}
onEntered: {
if (!_isDestroying)
enterDelay.start();
}
Component.onDestruction: {
_isDestroying = true;
exitWatchdog.stop();
if (notificationData && notificationData.timer)
notificationData.timer.stop();
}
anchors {
top: true
right: true
}
margins {
top: Theme.barHeight + 4
right: 12
}
Item {
id: content
anchors.fill: parent
visible: win.hasValidData
layer.enabled: (enterX.running || exitAnim.running)
layer.smooth: true
Rectangle {
property var shadowLayers: [shadowLayer1, shadowLayer2, shadowLayer3]
anchors.fill: parent
anchors.margins: 4
radius: Theme.cornerRadiusLarge
color: Theme.popupBackground()
border.color: notificationData && notificationData.urgency === 2 ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: notificationData && notificationData.urgency === 2 ? 2 : 1
clip: true
Rectangle {
id: shadowLayer1
anchors.fill: parent
anchors.margins: -3
color: "transparent"
radius: parent.radius + 3
border.color: Qt.rgba(0, 0, 0, 0.05)
border.width: 1
z: -3
}
Rectangle {
id: shadowLayer2
anchors.fill: parent
anchors.margins: -2
color: "transparent"
radius: parent.radius + 2
border.color: Qt.rgba(0, 0, 0, 0.08)
border.width: 1
z: -2
}
Rectangle {
id: shadowLayer3
anchors.fill: parent
color: "transparent"
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 1
radius: parent.radius
z: -1
}
Rectangle {
anchors.fill: parent
radius: parent.radius
visible: notificationData && notificationData.urgency === 2
opacity: 1
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop {
position: 0
color: Theme.primary
}
GradientStop {
position: 0.02
color: Theme.primary
}
GradientStop {
position: 0.021
color: "transparent"
}
}
}
Item {
id: notificationContent
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 12
anchors.leftMargin: 16
anchors.rightMargin: 56
height: 98
Rectangle {
id: iconContainer
readonly property bool hasNotificationImage: notificationData && notificationData.image && notificationData.image !== ""
width: 55
height: 55
radius: 27.5
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
border.color: "transparent"
border.width: 0
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
IconImage {
id: iconImage
anchors.fill: parent
anchors.margins: 2
asynchronous: true
source: {
if (!notificationData)
return "";
if (parent.hasNotificationImage)
return notificationData.cleanImage || "";
if (notificationData.appIcon) {
const appIcon = notificationData.appIcon;
if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://"))
return appIcon;
return Quickshell.iconPath(appIcon, "");
}
return "";
}
visible: status === Image.Ready
}
StyledText {
anchors.centerIn: parent
visible: !parent.hasNotificationImage && (!notificationData || !notificationData.appIcon || notificationData.appIcon === "")
text: {
const appName = notificationData && notificationData.appName ? notificationData.appName : "?";
return appName.charAt(0).toUpperCase();
}
font.pixelSize: 20
font.weight: Font.Bold
color: Theme.primaryText
}
}
Rectangle {
id: textContainer
anchors.left: iconContainer.right
anchors.leftMargin: 12
anchors.right: parent.right
anchors.rightMargin: 0
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.bottomMargin: 8
color: "transparent"
Item {
width: parent.width
height: parent.height
anchors.top: parent.top
anchors.topMargin: -2
Column {
width: parent.width
spacing: 2
StyledText {
width: parent.width
text: {
if (!notificationData)
return "";
const appName = notificationData.appName || "";
const timeStr = notificationData.timeStr || "";
if (timeStr.length > 0)
return appName + " • " + timeStr;
else
return appName;
}
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
elide: Text.ElideRight
maximumLineCount: 1
}
StyledText {
text: notificationData ? (notificationData.summary || "") : ""
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
width: parent.width
elide: Text.ElideRight
maximumLineCount: 1
visible: text.length > 0
}
StyledText {
text: notificationData ? (notificationData.htmlBody || "") : ""
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
width: parent.width
elide: Text.ElideRight
maximumLineCount: 2
wrapMode: Text.WordWrap
visible: text.length > 0
linkColor: Theme.primary
onLinkActivated: (link) => {
return Qt.openUrlExternally(link);
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
}
}
}
}
}
}
DankActionButton {
id: closeButton
anchors.right: parent.right
anchors.top: parent.top
anchors.topMargin: 12
anchors.rightMargin: 16
iconName: "close"
iconSize: 18
buttonSize: 28
z: 15
onClicked: {
if (notificationData && !win.exiting)
notificationData.popup = false;
}
}
Row {
anchors.right: clearButton.left
anchors.rightMargin: 8
anchors.bottom: parent.bottom
anchors.bottomMargin: 8
spacing: 8
z: 20
Repeater {
model: notificationData ? (notificationData.actions || []) : []
Rectangle {
property bool isHovered: false
width: Math.max(actionText.implicitWidth + 12, 50)
height: 24
radius: 4
color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent"
StyledText {
id: actionText
text: modelData.text || ""
color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
anchors.centerIn: parent
elide: Text.ElideRight
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton
onEntered: parent.isHovered = true
onExited: parent.isHovered = false
onClicked: {
if (modelData && modelData.invoke)
modelData.invoke();
if (notificationData && !win.exiting)
notificationData.popup = false;
}
}
}
}
}
Rectangle {
id: clearButton
property bool isHovered: false
anchors.right: parent.right
anchors.rightMargin: 16
anchors.bottom: parent.bottom
anchors.bottomMargin: 8
width: Math.max(clearText.implicitWidth + 12, 50)
height: 24
radius: 4
color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent"
z: 20
StyledText {
id: clearText
text: "Clear"
color: clearButton.isHovered ? Theme.primary : Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton
onEntered: clearButton.isHovered = true
onExited: clearButton.isHovered = false
onClicked: {
if (notificationData && !win.exiting)
NotificationService.dismissNotification(notificationData);
}
}
}
MouseArea {
id: cardHoverArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton
propagateComposedEvents: true
z: -1
onEntered: {
if (notificationData && notificationData.timer)
notificationData.timer.stop();
}
onExited: {
if (notificationData && notificationData.popup && notificationData.timer)
notificationData.timer.restart();
}
onClicked: {
if (notificationData && !win.exiting)
notificationData.popup = false;
}
}
}
transform: Translate {
id: tx
x: Anims.slidePx
}
}
NumberAnimation {
id: enterX
target: tx
property: "x"
from: Anims.slidePx
to: 0
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedDecel
onStopped: {
if (!win.exiting && !win._isDestroying && Math.abs(tx.x) < 0.5) {
win.entered();
}
}
}
ParallelAnimation {
id: exitAnim
onStopped: finalizeExit("animStopped")
PropertyAnimation {
target: tx
property: "x"
from: 0
to: Anims.slidePx
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedAccel
}
NumberAnimation {
target: content
property: "opacity"
from: 1
to: 0
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standardAccel
}
NumberAnimation {
target: content
property: "scale"
from: 1
to: 0.98
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedAccel
}
}
Connections {
id: wrapperConn
function onPopupChanged() {
if (!win.notificationData || win._isDestroying)
return ;
if (!win.notificationData.popup && !win.exiting)
startExit();
}
target: win.notificationData || null
ignoreUnknownSignals: true
enabled: !win._isDestroying
}
Connections {
id: notificationConn
function onDropped() {
if (!win._isDestroying && !win.exiting)
forceExit();
}
target: (win.notificationData && win.notificationData.notification && win.notificationData.notification.Retainable) || null
ignoreUnknownSignals: true
enabled: !win._isDestroying
}
Timer {
id: enterDelay
interval: 160
repeat: false
onTriggered: {
if (notificationData && notificationData.timer && !exiting && !_isDestroying)
notificationData.timer.start();
}
}
Timer {
id: exitWatchdog
interval: 600
repeat: false
onTriggered: finalizeExit("watchdog")
}
Behavior on screenY {
id: screenYAnim
enabled: !exiting && !_isDestroying
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standardDecel
}
}
}
@@ -0,0 +1,245 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Services
QtObject {
id: manager
property var modelData
property int topMargin: 0
property int baseNotificationHeight: 120
property int maxTargetNotifications: 3
property var popupWindows: [] // strong refs to windows (live until exitFinished)
property var destroyingWindows: new Set()
property Component popupComponent
popupComponent: Component {
NotificationPopup {
onEntered: manager._onPopupEntered(this)
onExitFinished: manager._onPopupExitFinished(this)
}
}
property Connections notificationConnections
notificationConnections: Connections {
function onVisibleNotificationsChanged() {
manager._sync(NotificationService.visibleNotifications);
}
target: NotificationService
}
property Timer sweeper
sweeper: Timer {
interval: 2000
running: false // Not running by default
repeat: true
onTriggered: {
let toRemove = [];
for (let p of popupWindows) {
if (!p) {
toRemove.push(p);
continue;
}
const isZombie = p.status === Component.Null || (!p.visible && !p.exiting) || (!p.notificationData && !p._isDestroying) || (!p.hasValidData && !p._isDestroying);
if (isZombie) {
toRemove.push(p);
if (p.forceExit) {
p.forceExit();
} else if (p.destroy) {
try {
p.destroy();
} catch (e) {
}
}
}
}
if (toRemove.length > 0) {
for (let zombie of toRemove) {
const i = popupWindows.indexOf(zombie);
if (i !== -1)
popupWindows.splice(i, 1);
}
popupWindows = popupWindows.slice();
const survivors = _active().sort((a, b) => {
return a.screenY - b.screenY;
});
for (let k = 0; k < survivors.length; ++k) {
survivors[k].screenY = topMargin + k * baseNotificationHeight;
}
}
if (popupWindows.length === 0)
sweeper.stop();
}
}
function _hasWindowFor(w) {
return popupWindows.some((p) => {
return p && p.notificationData === w && !p._isDestroying && p.status !== Component.Null;
});
}
function _isValidWindow(p) {
return p && p.status !== Component.Null && !p._isDestroying && p.hasValidData;
}
function _sync(newWrappers) {
for (let w of newWrappers) {
if (w && !_hasWindowFor(w))
insertNewestAtTop(w);
}
for (let p of popupWindows.slice()) {
if (!_isValidWindow(p))
continue;
if (p.notificationData && newWrappers.indexOf(p.notificationData) === -1 && !p.exiting) {
p.notificationData.removedByLimit = true;
p.notificationData.popup = false;
}
}
}
function insertNewestAtTop(wrapper) {
if (!wrapper) {
return ;
}
for (let p of popupWindows) {
if (!_isValidWindow(p))
continue;
if (p.exiting)
continue;
p.screenY = p.screenY + baseNotificationHeight;
}
const notificationId = wrapper && wrapper.notification ? wrapper.notification.id : "";
const win = popupComponent.createObject(null, {
"notificationData": wrapper,
"notificationId": notificationId,
"screenY": topMargin,
"screen": manager.modelData
});
if (!win) {
return ;
}
if (!win.hasValidData) {
win.destroy();
return ;
}
popupWindows.push(win);
if (!sweeper.running)
sweeper.start();
_maybeStartOverflow();
}
function _active() {
return popupWindows.filter((p) => {
return _isValidWindow(p) && p.notificationData && p.notificationData.popup && !p.exiting;
});
}
function _bottom() {
let b = null, maxY = -1;
for (let p of _active()) {
if (p.screenY > maxY) {
maxY = p.screenY;
b = p;
}
}
return b;
}
function _maybeStartOverflow() {
const activeWindows = _active();
if (activeWindows.length <= maxTargetNotifications + 1)
return ;
const b = _bottom();
if (b && !b.exiting) {
b.notificationData.removedByLimit = true;
b.notificationData.popup = false;
}
}
function _onPopupEntered(p) {
if (_isValidWindow(p))
_maybeStartOverflow();
}
function _onPopupExitFinished(p) {
if (!p)
return ;
const windowId = p.toString();
if (destroyingWindows.has(windowId))
return ;
destroyingWindows.add(windowId);
const i = popupWindows.indexOf(p);
if (i !== -1) {
popupWindows.splice(i, 1);
popupWindows = popupWindows.slice();
}
if (NotificationService.releaseWrapper && p.notificationData)
NotificationService.releaseWrapper(p.notificationData);
Qt.callLater(() => {
if (p && p.destroy) {
try {
p.destroy();
} catch (e) {
}
}
Qt.callLater(() => {
destroyingWindows.delete(windowId);
});
});
const survivors = _active().sort((a, b) => {
return a.screenY - b.screenY;
});
for (let k = 0; k < survivors.length; ++k) {
survivors[k].screenY = topMargin + k * baseNotificationHeight;
}
_maybeStartOverflow();
}
function cleanupAllWindows() {
sweeper.stop();
for (let p of popupWindows.slice()) {
if (p) {
try {
if (p.forceExit)
p.forceExit();
else if (p.destroy)
p.destroy();
} catch (e) {
}
}
}
popupWindows = [];
destroyingWindows.clear();
}
onPopupWindowsChanged: {
if (popupWindows.length > 0 && !sweeper.running)
sweeper.start();
else if (popupWindows.length === 0 && sweeper.running)
sweeper.stop();
}
}
+480
View File
@@ -0,0 +1,480 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets
Column {
function formatNetworkSpeed(bytesPerSec) {
if (bytesPerSec < 1024)
return bytesPerSec.toFixed(0) + " B/s";
else if (bytesPerSec < 1024 * 1024)
return (bytesPerSec / 1024).toFixed(1) + " KB/s";
else if (bytesPerSec < 1024 * 1024 * 1024)
return (bytesPerSec / (1024 * 1024)).toFixed(1) + " MB/s";
else
return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(1) + " GB/s";
}
function formatDiskSpeed(bytesPerSec) {
if (bytesPerSec < 1024 * 1024)
return (bytesPerSec / 1024).toFixed(1) + " KB/s";
else if (bytesPerSec < 1024 * 1024 * 1024)
return (bytesPerSec / (1024 * 1024)).toFixed(1) + " MB/s";
else
return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(1) + " GB/s";
}
anchors.fill: parent
spacing: Theme.spacingM
Component.onCompleted: {
SysMonitorService.addRef();
}
Component.onDestruction: {
SysMonitorService.removeRef();
}
Rectangle {
width: parent.width
height: 200
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.04)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06)
border.width: 1
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
Row {
width: parent.width
height: 32
spacing: Theme.spacingM
StyledText {
text: "CPU"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
width: 80
height: 24
radius: Theme.cornerRadiusSmall
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: SysMonitorService.totalCpuUsage.toFixed(1) + "%"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.primary
anchors.centerIn: parent
}
}
Item {
width: parent.width - 280
height: 1
}
StyledText {
text: SysMonitorService.cpuCount + " cores"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
}
ScrollView {
width: parent.width
height: parent.height - 40
clip: true
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
Column {
width: parent.width
spacing: 6
Repeater {
model: SysMonitorService.perCoreCpuUsage
Row {
width: parent.width
height: 20
spacing: Theme.spacingS
StyledText {
text: "C" + index
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: 24
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
width: parent.width - 80
height: 6
radius: 3
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
anchors.verticalCenter: parent.verticalCenter
Rectangle {
width: parent.width * Math.min(1, modelData / 100)
height: parent.height
radius: parent.radius
color: {
const usage = modelData;
if (usage > 80)
return Theme.error;
if (usage > 60)
return Theme.warning;
return Theme.primary;
}
Behavior on width {
NumberAnimation {
duration: Theme.shortDuration
}
}
}
}
StyledText {
text: modelData ? modelData.toFixed(0) + "%" : "0%"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
width: 32
horizontalAlignment: Text.AlignRight
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
}
}
Rectangle {
width: parent.width
height: 80
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.04)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06)
border.width: 1
Row {
anchors.centerIn: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 4
StyledText {
text: "Memory"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
}
StyledText {
text: SysMonitorService.formatSystemMemory(SysMonitorService.usedMemoryKB) + " / " + SysMonitorService.formatSystemMemory(SysMonitorService.totalMemoryKB)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
Item {
width: Theme.spacingL
height: 1
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 4
width: 200
Rectangle {
width: parent.width
height: 16
radius: 8
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
Rectangle {
width: SysMonitorService.totalMemoryKB > 0 ? parent.width * (SysMonitorService.usedMemoryKB / SysMonitorService.totalMemoryKB) : 0
height: parent.height
radius: parent.radius
color: {
const usage = SysMonitorService.totalMemoryKB > 0 ? (SysMonitorService.usedMemoryKB / SysMonitorService.totalMemoryKB) : 0;
if (usage > 0.9)
return Theme.error;
if (usage > 0.7)
return Theme.warning;
return Theme.secondary;
}
Behavior on width {
NumberAnimation {
duration: Theme.mediumDuration
}
}
}
}
StyledText {
text: SysMonitorService.totalMemoryKB > 0 ? ((SysMonitorService.usedMemoryKB / SysMonitorService.totalMemoryKB) * 100).toFixed(1) + "% used" : "No data"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
Item {
width: Theme.spacingL
height: 1
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 4
StyledText {
text: "Swap"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
}
StyledText {
text: SysMonitorService.totalSwapKB > 0 ? SysMonitorService.formatSystemMemory(SysMonitorService.usedSwapKB) + " / " + SysMonitorService.formatSystemMemory(SysMonitorService.totalSwapKB) : "No swap configured"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
Item {
width: Theme.spacingL
height: 1
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 4
width: 200
Rectangle {
width: parent.width
height: 16
radius: 8
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
Rectangle {
width: SysMonitorService.totalSwapKB > 0 ? parent.width * (SysMonitorService.usedSwapKB / SysMonitorService.totalSwapKB) : 0
height: parent.height
radius: parent.radius
color: {
if (!SysMonitorService.totalSwapKB)
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3);
const usage = SysMonitorService.usedSwapKB / SysMonitorService.totalSwapKB;
if (usage > 0.9)
return Theme.error;
if (usage > 0.7)
return Theme.warning;
return Theme.info;
}
Behavior on width {
NumberAnimation {
duration: Theme.mediumDuration
}
}
}
}
StyledText {
text: SysMonitorService.totalSwapKB > 0 ? ((SysMonitorService.usedSwapKB / SysMonitorService.totalSwapKB) * 100).toFixed(1) + "% used" : "Not available"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
}
}
Row {
width: parent.width
height: 80
spacing: Theme.spacingM
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: 80
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.04)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06)
border.width: 1
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
StyledText {
text: "Network"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
Row {
spacing: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
Row {
spacing: 4
StyledText {
text: "↓"
font.pixelSize: Theme.fontSizeSmall
color: Theme.info
}
StyledText {
text: SysMonitorService.networkRxRate > 0 ? formatNetworkSpeed(SysMonitorService.networkRxRate) : "0 B/s"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
Row {
spacing: 4
StyledText {
text: "↑"
font.pixelSize: Theme.fontSizeSmall
color: Theme.error
}
StyledText {
text: SysMonitorService.networkTxRate > 0 ? formatNetworkSpeed(SysMonitorService.networkTxRate) : "0 B/s"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
}
}
}
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: 80
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.04)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06)
border.width: 1
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
StyledText {
text: "Disk"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
Row {
spacing: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
Row {
spacing: 4
StyledText {
text: "R"
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
}
StyledText {
text: formatDiskSpeed(SysMonitorService.diskReadRate)
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
Row {
spacing: 4
StyledText {
text: "W"
font.pixelSize: Theme.fontSizeSmall
color: Theme.warning
}
StyledText {
text: formatDiskSpeed(SysMonitorService.diskWriteRate)
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
}
}
}
}
}
+229
View File
@@ -0,0 +1,229 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Services
import qs.Widgets
Popup {
id: processContextMenu
property var processData: null
function show(x, y) {
if (!processContextMenu.parent && typeof Overlay !== "undefined" && Overlay.overlay)
processContextMenu.parent = Overlay.overlay;
const menuWidth = 180;
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2;
const screenWidth = Screen.width;
const screenHeight = Screen.height;
let finalX = x;
let finalY = y;
if (x + menuWidth > screenWidth - 20)
finalX = x - menuWidth;
if (y + menuHeight > screenHeight - 20)
finalY = y - menuHeight;
processContextMenu.x = Math.max(20, finalX);
processContextMenu.y = Math.max(20, finalY);
open();
}
width: 180
height: menuColumn.implicitHeight + Theme.spacingS * 2
padding: 0
modal: false
closePolicy: Popup.CloseOnEscape
onClosed: {
closePolicy = Popup.CloseOnEscape;
}
onOpened: {
outsideClickTimer.start();
}
Timer {
id: outsideClickTimer
interval: 100
onTriggered: {
processContextMenu.closePolicy = Popup.CloseOnEscape | Popup.CloseOnPressOutside;
}
}
background: Rectangle {
color: "transparent"
}
contentItem: Rectangle {
id: menuContent
color: Theme.popupBackground()
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
Column {
id: menuColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 1
Rectangle {
width: parent.width
height: 28
radius: Theme.cornerRadiusSmall
color: copyPidArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: "Copy PID"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
}
MouseArea {
id: copyPidArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (processContextMenu.processData)
Quickshell.execDetached(["wl-copy", processContextMenu.processData.pid.toString()]);
processContextMenu.close();
}
}
}
Rectangle {
width: parent.width
height: 28
radius: Theme.cornerRadiusSmall
color: copyNameArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: "Copy Process Name"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
}
MouseArea {
id: copyNameArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (processContextMenu.processData) {
let processName = processContextMenu.processData.displayName || processContextMenu.processData.command;
Quickshell.execDetached(["wl-copy", processName]);
}
processContextMenu.close();
}
}
}
Rectangle {
width: parent.width - Theme.spacingS * 2
height: 5
anchors.horizontalCenter: parent.horizontalCenter
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
}
Rectangle {
width: parent.width
height: 28
radius: Theme.cornerRadiusSmall
color: killArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
enabled: processContextMenu.processData
opacity: enabled ? 1 : 0.5
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: "Kill Process"
font.pixelSize: Theme.fontSizeSmall
color: parent.enabled ? (killArea.containsMouse ? Theme.error : Theme.surfaceText) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
font.weight: Font.Normal
}
MouseArea {
id: killArea
anchors.fill: parent
hoverEnabled: true
cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: parent.enabled
onClicked: {
if (processContextMenu.processData)
Quickshell.execDetached(["kill", processContextMenu.processData.pid.toString()]);
processContextMenu.close();
}
}
}
Rectangle {
width: parent.width
height: 28
radius: Theme.cornerRadiusSmall
color: forceKillArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
enabled: processContextMenu.processData && processContextMenu.processData.pid > 1000
opacity: enabled ? 1 : 0.5
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: "Force Kill Process"
font.pixelSize: Theme.fontSizeSmall
color: parent.enabled ? (forceKillArea.containsMouse ? Theme.error : Theme.surfaceText) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
font.weight: Font.Normal
}
MouseArea {
id: forceKillArea
anchors.fill: parent
hoverEnabled: true
cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: parent.enabled
onClicked: {
if (processContextMenu.processData)
Quickshell.execDetached(["kill", "-9", processContextMenu.processData.pid.toString()]);
processContextMenu.close();
}
}
}
}
}
}
+215
View File
@@ -0,0 +1,215 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: processItem
property var process: null
property var contextMenu: null
width: parent ? parent.width : 0
height: 40
radius: Theme.cornerRadiusLarge
color: processMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
border.color: processMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
border.width: 1
MouseArea {
id: processMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: (mouse) => {
if (mouse.button === Qt.RightButton) {
if (process && process.pid > 0 && contextMenu) {
contextMenu.processData = process;
let globalPos = processMouseArea.mapToGlobal(mouse.x, mouse.y);
let localPos = contextMenu.parent ? contextMenu.parent.mapFromGlobal(globalPos.x, globalPos.y) : globalPos;
contextMenu.show(localPos.x, localPos.y);
}
}
}
onPressAndHold: {
if (process && process.pid > 0 && contextMenu) {
contextMenu.processData = process;
let globalPos = processMouseArea.mapToGlobal(processMouseArea.width / 2, processMouseArea.height / 2);
contextMenu.show(globalPos.x, globalPos.y);
}
}
}
Item {
anchors.fill: parent
anchors.margins: 8
DankIcon {
id: processIcon
name: SysMonitorService.getProcessIcon(process ? process.command : "")
size: Theme.iconSize - 4
color: {
if (process && process.cpu > 80)
return Theme.error;
if (process && process.cpu > 50)
return Theme.warning;
return Theme.surfaceText;
}
opacity: 0.8
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: process ? process.displayName : ""
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
font.weight: Font.Medium
color: Theme.surfaceText
width: 250
elide: Text.ElideRight
anchors.left: processIcon.right
anchors.leftMargin: 8
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
id: cpuBadge
width: 80
height: 20
radius: Theme.cornerRadius
color: {
if (process && process.cpu > 80)
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12);
if (process && process.cpu > 50)
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12);
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08);
}
anchors.right: parent.right
anchors.rightMargin: 194
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: SysMonitorService.formatCpuUsage(process ? process.cpu : 0)
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
font.weight: Font.Bold
color: {
if (process && process.cpu > 80)
return Theme.error;
if (process && process.cpu > 50)
return Theme.warning;
return Theme.surfaceText;
}
anchors.centerIn: parent
}
}
Rectangle {
id: memoryBadge
width: 80
height: 20
radius: Theme.cornerRadius
color: {
if (process && process.memoryKB > 1024 * 1024)
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12);
if (process && process.memoryKB > 512 * 1024)
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12);
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08);
}
anchors.right: parent.right
anchors.rightMargin: 102
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: SysMonitorService.formatMemoryUsage(process ? process.memoryKB : 0)
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
font.weight: Font.Bold
color: {
if (process && process.memoryKB > 1024 * 1024)
return Theme.error;
if (process && process.memoryKB > 512 * 1024)
return Theme.warning;
return Theme.surfaceText;
}
anchors.centerIn: parent
}
}
StyledText {
text: process ? process.pid.toString() : ""
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
color: Theme.surfaceText
opacity: 0.7
width: 50
horizontalAlignment: Text.AlignRight
anchors.right: parent.right
anchors.rightMargin: 40
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
id: menuButton
width: 28
height: 28
radius: Theme.cornerRadius
color: menuButtonArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
DankIcon {
name: "more_vert"
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.6
anchors.centerIn: parent
}
MouseArea {
id: menuButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (process && process.pid > 0 && contextMenu) {
contextMenu.processData = process;
let globalPos = menuButtonArea.mapToGlobal(menuButtonArea.width / 2, menuButtonArea.height);
let localPos = contextMenu.parent ? contextMenu.parent.mapFromGlobal(globalPos.x, globalPos.y) : globalPos;
contextMenu.show(localPos.x, localPos.y);
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
}
}
+159
View File
@@ -0,0 +1,159 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Modules.ProcessList
import qs.Services
import qs.Widgets
PanelWindow {
id: processListPopout
property bool isVisible: false
property var parentWidget: null
function hide() {
isVisible = false;
if (processContextMenu.visible)
processContextMenu.close();
}
function show() {
isVisible = true;
}
function toggle() {
if (isVisible)
hide();
else
show();
}
visible: isVisible
implicitWidth: 600
implicitHeight: 600
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
Ref {
service: SysMonitorService
}
anchors {
top: true
left: true
right: true
bottom: true
}
MouseArea {
anchors.fill: parent
onClicked: function(mouse) {
var localPos = mapToItem(contentLoader, mouse.x, mouse.y);
if (localPos.x < 0 || localPos.x > contentLoader.width || localPos.y < 0 || localPos.y > contentLoader.height)
processListPopout.hide();
}
}
Loader {
id: contentLoader
readonly property real targetWidth: Math.min(600, Screen.width - Theme.spacingL * 2)
readonly property real targetHeight: Math.min(600, Screen.height - Theme.barHeight - Theme.spacingS * 2)
asynchronous: true
active: processListPopout.isVisible
width: targetWidth
height: targetHeight
y: Theme.barHeight + Theme.spacingXS
x: Math.max(Theme.spacingL, Screen.width - targetWidth - Theme.spacingL)
opacity: processListPopout.isVisible ? 1 : 0
scale: processListPopout.isVisible ? 1 : 0.9
Behavior on opacity {
NumberAnimation {
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
Behavior on scale {
NumberAnimation {
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
sourceComponent: Rectangle {
id: dropdownContent
radius: Theme.cornerRadiusLarge
color: Theme.popupBackground()
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
clip: true
antialiasing: true
smooth: true
ColumnLayout {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
Rectangle {
Layout.fillWidth: true
height: systemOverview.height + Theme.spacingM * 2
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.2)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
SystemOverview {
id: systemOverview
anchors.centerIn: parent
width: parent.width - Theme.spacingM * 2
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Theme.cornerRadiusLarge
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
ProcessListView {
anchors.fill: parent
anchors.margins: Theme.spacingS
contextMenu: processContextMenu
}
}
}
}
}
ProcessContextMenu {
id: processContextMenu
}
}
+339
View File
@@ -0,0 +1,339 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets
Column {
id: root
property var contextMenu: null
Component.onCompleted: {
SysMonitorService.addRef();
}
Component.onDestruction: {
SysMonitorService.removeRef();
}
Item {
id: columnHeaders
width: parent.width
anchors.leftMargin: 8
height: 24
Rectangle {
width: 60
height: 20
color: processHeaderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
radius: Theme.cornerRadius
anchors.left: parent.left
anchors.leftMargin: 0
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "Process"
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
font.weight: SysMonitorService.sortBy === "name" ? Font.Bold : Font.Medium
color: Theme.surfaceText
opacity: SysMonitorService.sortBy === "name" ? 1 : 0.7
anchors.centerIn: parent
}
MouseArea {
id: processHeaderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
processListView.captureAnchor();
SysMonitorService.setSortBy("name");
processListView.restoreAnchor();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
Rectangle {
width: 80
height: 20
color: cpuHeaderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
radius: Theme.cornerRadius
anchors.right: parent.right
anchors.rightMargin: 200
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "CPU"
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
font.weight: SysMonitorService.sortBy === "cpu" ? Font.Bold : Font.Medium
color: Theme.surfaceText
opacity: SysMonitorService.sortBy === "cpu" ? 1 : 0.7
anchors.centerIn: parent
}
MouseArea {
id: cpuHeaderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
processListView.captureAnchor();
SysMonitorService.setSortBy("cpu");
processListView.restoreAnchor();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
Rectangle {
width: 80
height: 20
color: memoryHeaderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
radius: Theme.cornerRadius
anchors.right: parent.right
anchors.rightMargin: 112
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "RAM"
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
font.weight: SysMonitorService.sortBy === "memory" ? Font.Bold : Font.Medium
color: Theme.surfaceText
opacity: SysMonitorService.sortBy === "memory" ? 1 : 0.7
anchors.centerIn: parent
}
MouseArea {
id: memoryHeaderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
processListView.captureAnchor();
SysMonitorService.setSortBy("memory");
processListView.restoreAnchor();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
Rectangle {
width: 50
height: 20
color: pidHeaderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
radius: Theme.cornerRadius
anchors.right: parent.right
anchors.rightMargin: 53
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "PID"
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
font.weight: SysMonitorService.sortBy === "pid" ? Font.Bold : Font.Medium
color: Theme.surfaceText
opacity: SysMonitorService.sortBy === "pid" ? 1 : 0.7
horizontalAlignment: Text.AlignHCenter
anchors.centerIn: parent
}
MouseArea {
id: pidHeaderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
processListView.captureAnchor();
SysMonitorService.setSortBy("pid");
processListView.restoreAnchor();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
Rectangle {
width: 28
height: 28
radius: Theme.cornerRadius
color: sortOrderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
anchors.right: parent.right
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: SysMonitorService.sortDescending ? "↓" : "↑"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.centerIn: parent
}
MouseArea {
id: sortOrderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
processListView.captureAnchor();
SysMonitorService.toggleSortOrder();
processListView.restoreAnchor();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
}
ListView {
id: processListView
property real stableY: 0
property bool isUserScrolling: false
property bool isScrollBarDragging: false
property real wheelMultiplier: 1.8
property int wheelBaseStep: 160
property string keyRoleName: "pid"
property var _anchorKey: undefined
property real _anchorOffset: 0
function captureAnchor() {
const y = contentY + 1;
const idx = indexAt(0, y);
if (idx < 0 || !model || idx >= model.length)
return ;
_anchorKey = model[idx][keyRoleName];
const it = itemAtIndex(idx);
_anchorOffset = it ? (y - it.y) : 0;
}
function restoreAnchor() {
Qt.callLater(function() {
if (_anchorKey === undefined || !model)
return ;
var i = -1;
for (var j = 0; j < model.length; ++j) {
if (model[j][keyRoleName] === _anchorKey) {
i = j;
break;
}
}
if (i < 0)
return ;
positionViewAtIndex(i, ListView.Beginning);
const maxY = Math.max(0, contentHeight - height);
contentY = Math.max(0, Math.min(maxY, contentY + _anchorOffset - 1));
});
}
width: parent.width
height: parent.height - columnHeaders.height
clip: true
spacing: 4
model: SysMonitorService.processes
boundsBehavior: Flickable.StopAtBounds
flickDeceleration: 1500
maximumFlickVelocity: 2000
onMovementStarted: isUserScrolling = true
onMovementEnded: {
isUserScrolling = false;
if (contentY > 40)
stableY = contentY;
}
onContentYChanged: {
if (!isUserScrolling && !isScrollBarDragging && visible && stableY > 40 && Math.abs(contentY - stableY) > 10)
contentY = stableY;
}
onModelChanged: {
if (model && model.length > 0 && !isUserScrolling && stableY > 40)
Qt.callLater(function() {
contentY = stableY;
});
}
WheelHandler {
target: null
onWheel: (ev) => {
let dy = ev.pixelDelta.y !== 0 ? ev.pixelDelta.y : (ev.angleDelta.y / 120) * processListView.wheelBaseStep;
if (ev.inverted)
dy = -dy;
const maxY = Math.max(0, processListView.contentHeight - processListView.height);
processListView.contentY = Math.max(0, Math.min(maxY, processListView.contentY - dy * processListView.wheelMultiplier));
ev.accepted = true;
}
}
delegate: ProcessListItem {
process: modelData
contextMenu: root.contextMenu
}
ScrollBar.vertical: ScrollBar {
id: verticalScrollBar
policy: ScrollBar.AsNeeded
onPressedChanged: {
processListView.isScrollBarDragging = pressed;
if (!pressed && processListView.contentY > 40)
processListView.stableY = processListView.contentY;
}
}
ScrollBar.horizontal: ScrollBar {
policy: ScrollBar.AlwaysOff
}
}
}
+29
View File
@@ -0,0 +1,29 @@
import QtQuick
import QtQuick.Layouts
import qs.Common
import qs.Modules.ProcessList
import qs.Services
ColumnLayout {
id: processesTab
property var contextMenu: null
anchors.fill: parent
spacing: Theme.spacingM
SystemOverview {
Layout.fillWidth: true
}
ProcessListView {
Layout.fillWidth: true
Layout.fillHeight: true
contextMenu: processesTab.contextMenu || localContextMenu
}
ProcessContextMenu {
id: localContextMenu
}
}
+202
View File
@@ -0,0 +1,202 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Row {
width: parent.width
spacing: Theme.spacingM
Component.onCompleted: {
SysMonitorService.addRef();
}
Component.onDestruction: {
SysMonitorService.removeRef();
}
Rectangle {
width: (parent.width - Theme.spacingM * 2) / 3
height: 80
radius: Theme.cornerRadiusLarge
color: {
if (SysMonitorService.sortBy === "cpu")
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16);
else if (cpuCardMouseArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
else
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
}
border.color: SysMonitorService.sortBy === "cpu" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
border.width: SysMonitorService.sortBy === "cpu" ? 2 : 1
MouseArea {
id: cpuCardMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: SysMonitorService.setSortBy("cpu")
}
Column {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: "CPU"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: SysMonitorService.sortBy === "cpu" ? Theme.primary : Theme.secondary
opacity: SysMonitorService.sortBy === "cpu" ? 1 : 0.8
}
StyledText {
text: SysMonitorService.totalCpuUsage.toFixed(1) + "%"
font.pixelSize: Theme.fontSizeLarge
font.family: Prefs.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
}
StyledText {
text: SysMonitorService.cpuCount + " cores"
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
color: Theme.surfaceText
opacity: 0.7
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
Rectangle {
width: (parent.width - Theme.spacingM * 2) / 3
height: 80
radius: Theme.cornerRadiusLarge
color: {
if (SysMonitorService.sortBy === "memory")
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16);
else if (memoryCardMouseArea.containsMouse)
return Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.12);
else
return Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08);
}
border.color: SysMonitorService.sortBy === "memory" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4) : Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.2)
border.width: SysMonitorService.sortBy === "memory" ? 2 : 1
MouseArea {
id: memoryCardMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: SysMonitorService.setSortBy("memory")
}
Column {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: "Memory"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: SysMonitorService.sortBy === "memory" ? Theme.primary : Theme.secondary
opacity: SysMonitorService.sortBy === "memory" ? 1 : 0.8
}
StyledText {
text: SysMonitorService.formatSystemMemory(SysMonitorService.usedMemoryKB)
font.pixelSize: Theme.fontSizeLarge
font.family: Prefs.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
}
StyledText {
text: "of " + SysMonitorService.formatSystemMemory(SysMonitorService.totalMemoryKB)
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
color: Theme.surfaceText
opacity: 0.7
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
Rectangle {
width: (parent.width - Theme.spacingM * 2) / 3
height: 80
radius: Theme.cornerRadiusLarge
color: SysMonitorService.totalSwapKB > 0 ? Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.08) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.04)
border.color: SysMonitorService.totalSwapKB > 0 ? Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.2) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.12)
border.width: 1
Column {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: "Swap"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: SysMonitorService.totalSwapKB > 0 ? Theme.warning : Theme.surfaceText
opacity: 0.8
}
StyledText {
text: SysMonitorService.totalSwapKB > 0 ? SysMonitorService.formatSystemMemory(SysMonitorService.usedSwapKB) : "None"
font.pixelSize: Theme.fontSizeLarge
font.family: Prefs.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
}
StyledText {
text: SysMonitorService.totalSwapKB > 0 ? "of " + SysMonitorService.formatSystemMemory(SysMonitorService.totalSwapKB) : "No swap configured"
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
color: Theme.surfaceText
opacity: 0.7
}
}
}
}
+479
View File
@@ -0,0 +1,479 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets
ScrollView {
anchors.fill: parent
clip: true
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
Component.onCompleted: {
SysMonitorService.addRef();
}
Component.onDestruction: {
SysMonitorService.removeRef();
}
Column {
width: parent.width
spacing: Theme.spacingM
Rectangle {
width: parent.width
height: systemInfoColumn.implicitHeight + 2 * Theme.spacingL
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.6)
border.width: 0
Column {
id: systemInfoColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
Row {
width: parent.width
spacing: Theme.spacingL
SystemLogo {
width: 80
height: 80
}
Column {
width: parent.width - 80 - Theme.spacingL
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
StyledText {
text: SysMonitorService.hostname
font.pixelSize: Theme.fontSizeXLarge
font.family: Prefs.monoFontFamily
font.weight: Font.Light
color: Theme.surfaceText
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: SysMonitorService.distribution + " • " + SysMonitorService.architecture + " • " + SysMonitorService.kernelVersion
font.pixelSize: Theme.fontSizeMedium
font.family: Prefs.monoFontFamily
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: "Up " + UserInfoService.uptime + " • Boot: " + SysMonitorService.bootTime
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: "Load: " + SysMonitorService.loadAverage + " • " + SysMonitorService.processCount + " processes, " + SysMonitorService.threadCount + " threads"
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
verticalAlignment: Text.AlignVCenter
}
}
}
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
Row {
width: parent.width
spacing: Theme.spacingXL
Rectangle {
width: (parent.width - Theme.spacingXL) / 2
height: Math.max(hardwareColumn.implicitHeight, memoryColumn.implicitHeight) + Theme.spacingM
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainerHigh.r, Theme.surfaceContainerHigh.g, Theme.surfaceContainerHigh.b, 0.4)
border.width: 1
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
Column {
id: hardwareColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Theme.spacingM
spacing: Theme.spacingXS
Row {
width: parent.width
spacing: Theme.spacingS
DankIcon {
name: "memory"
size: Theme.iconSizeSmall
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Hardware"
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
font.weight: Font.Bold
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: SysMonitorService.cpuModel
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
font.weight: Font.Medium
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
wrapMode: Text.NoWrap
maximumLineCount: 1
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: SysMonitorService.motherboard
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
width: parent.width
elide: Text.ElideRight
wrapMode: Text.NoWrap
maximumLineCount: 1
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: "BIOS " + SysMonitorService.biosVersion
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
width: parent.width
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
}
}
Rectangle {
width: (parent.width - Theme.spacingXL) / 2
height: Math.max(hardwareColumn.implicitHeight, memoryColumn.implicitHeight) + Theme.spacingM
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainerHigh.r, Theme.surfaceContainerHigh.g, Theme.surfaceContainerHigh.b, 0.4)
border.width: 1
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
Column {
id: memoryColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Theme.spacingM
spacing: Theme.spacingXS
Row {
width: parent.width
spacing: Theme.spacingS
DankIcon {
name: "developer_board"
size: Theme.iconSizeSmall
color: Theme.secondary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Memory"
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
font.weight: Font.Bold
color: Theme.secondary
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: SysMonitorService.formatSystemMemory(SysMonitorService.totalMemoryKB) + " Total"
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
font.weight: Font.Medium
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: SysMonitorService.formatSystemMemory(SysMonitorService.usedMemoryKB) + " Used • " + SysMonitorService.formatSystemMemory(SysMonitorService.totalMemoryKB - SysMonitorService.usedMemoryKB) + " Available"
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
width: parent.width
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
Item {
width: parent.width
height: Theme.fontSizeSmall + Theme.spacingXS
}
}
}
}
}
}
Rectangle {
width: parent.width
height: storageColumn.implicitHeight + 2 * Theme.spacingL
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.6)
border.width: 0
Column {
id: storageColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Theme.spacingL
spacing: Theme.spacingS
Row {
width: parent.width
spacing: Theme.spacingS
DankIcon {
name: "storage"
size: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Storage & Disks"
font.pixelSize: Theme.fontSizeLarge
font.family: Prefs.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Column {
width: parent.width
spacing: 2
Row {
width: parent.width
height: 24
spacing: Theme.spacingS
StyledText {
text: "Device"
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.25
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: "Mount"
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.2
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: "Size"
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: "Used"
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: "Available"
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: "Use%"
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.1
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
}
Repeater {
id: diskMountRepeater
model: SysMonitorService.diskMounts
Rectangle {
width: parent.width
height: 24
radius: Theme.cornerRadiusSmall
color: diskMouseArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.04) : "transparent"
MouseArea {
id: diskMouseArea
anchors.fill: parent
hoverEnabled: true
}
Row {
anchors.fill: parent
spacing: Theme.spacingS
StyledText {
text: modelData.device
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
color: Theme.surfaceText
width: parent.width * 0.25
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: modelData.mount
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
color: Theme.surfaceText
width: parent.width * 0.2
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: modelData.size
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: modelData.used
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: modelData.avail
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: modelData.percent
font.pixelSize: Theme.fontSizeSmall
font.family: Prefs.monoFontFamily
color: {
const percent = parseInt(modelData.percent);
if (percent > 90)
return Theme.error;
if (percent > 75)
return Theme.warning;
return Theme.surfaceText;
}
width: parent.width * 0.1
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
}
}
}
}
}
}
}
}
+845
View File
@@ -0,0 +1,845 @@
import QtQuick
import QtQuick.Controls
import Quickshell.Io
import qs.Common
import qs.Services
import qs.Widgets
ScrollView {
id: appearanceTab
contentWidth: availableWidth
contentHeight: column.implicitHeight + Theme.spacingXL
clip: true
Column {
id: column
width: parent.width
spacing: Theme.spacingXL
topPadding: Theme.spacingL
bottomPadding: Theme.spacingXL
StyledRect {
width: parent.width
height: displaySection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadiusLarge
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
Column {
id: displaySection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "monitor"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Display Settings"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
width: parent.width
text: "Night Mode"
description: "Apply warm color temperature to reduce eye strain"
checked: Prefs.nightModeEnabled
onToggled: (checked) => {
Prefs.setNightModeEnabled(checked);
if (checked)
nightModeEnableProcess.running = true;
else
nightModeDisableProcess.running = true;
}
}
DankToggle {
width: parent.width
text: "Light Mode"
description: "Use light theme instead of dark theme"
checked: Prefs.isLightMode
onToggled: (checked) => {
Prefs.setLightMode(checked);
Theme.isLightMode = checked;
}
}
DankDropdown {
width: parent.width
text: "Icon Theme"
description: "Select icon theme"
currentValue: Prefs.iconTheme
enableFuzzySearch: true
popupWidthOffset: 100
maxPopupHeight: 400
options: {
Prefs.detectAvailableIconThemes();
return Prefs.availableIconThemes;
}
onValueChanged: (value) => {
Prefs.setIconTheme(value);
if (value !== "System Default" && !Prefs.qt5ctAvailable && !Prefs.qt6ctAvailable)
ToastService.showWarning("qt5ct or qt6ct not found - Qt app themes may not update without these tools");
}
}
DankDropdown {
width: parent.width
text: "Font Family"
description: "Select system font family"
currentValue: {
if (Prefs.fontFamily === Prefs.defaultFontFamily)
return "Default (" + Prefs.defaultFontFamily + ")";
else
return Prefs.fontFamily || "Default (" + Prefs.defaultFontFamily + ")";
}
enableFuzzySearch: true
popupWidthOffset: 100
maxPopupHeight: 400
options: {
var fonts = ["Default (" + Prefs.defaultFontFamily + ")"];
var availableFonts = Qt.fontFamilies();
var rootFamilies = [];
var seenFamilies = new Set();
for (var i = 0; i < availableFonts.length; i++) {
var fontName = availableFonts[i];
if (fontName.startsWith("."))
continue;
if (fontName === Prefs.defaultFontFamily)
continue;
var rootName = fontName.replace(/ (Thin|Extra Light|Light|Regular|Medium|Semi Bold|Demi Bold|Bold|Extra Bold|Black|Heavy)$/i, "").replace(/ (Italic|Oblique|Condensed|Extended|Narrow|Wide)$/i, "").replace(/ (UI|Display|Text|Mono|Sans|Serif)$/i, function(match, suffix) {
return match;
}).trim();
if (!seenFamilies.has(rootName) && rootName !== "") {
seenFamilies.add(rootName);
rootFamilies.push(rootName);
}
}
return fonts.concat(rootFamilies.sort());
}
onValueChanged: (value) => {
if (value.startsWith("Default ("))
Prefs.setFontFamily(Prefs.defaultFontFamily);
else
Prefs.setFontFamily(value);
}
}
DankDropdown {
width: parent.width
text: "Font Weight"
description: "Select font weight"
currentValue: {
switch (Prefs.fontWeight) {
case Font.Thin:
return "Thin";
case Font.ExtraLight:
return "Extra Light";
case Font.Light:
return "Light";
case Font.Normal:
return "Regular";
case Font.Medium:
return "Medium";
case Font.DemiBold:
return "Demi Bold";
case Font.Bold:
return "Bold";
case Font.ExtraBold:
return "Extra Bold";
case Font.Black:
return "Black";
default:
return "Regular";
}
}
options: ["Thin", "Extra Light", "Light", "Regular", "Medium", "Demi Bold", "Bold", "Extra Bold", "Black"]
onValueChanged: (value) => {
var weight;
switch (value) {
case "Thin":
weight = Font.Thin;
break;
case "Extra Light":
weight = Font.ExtraLight;
break;
case "Light":
weight = Font.Light;
break;
case "Regular":
weight = Font.Normal;
break;
case "Medium":
weight = Font.Medium;
break;
case "Demi Bold":
weight = Font.DemiBold;
break;
case "Bold":
weight = Font.Bold;
break;
case "Extra Bold":
weight = Font.ExtraBold;
break;
case "Black":
weight = Font.Black;
break;
default:
weight = Font.Normal;
break;
}
Prefs.setFontWeight(weight);
}
}
DankDropdown {
width: parent.width
text: "Monospace Font"
description: "Select monospace font for process list and technical displays"
currentValue: {
if (Prefs.monoFontFamily === Prefs.defaultMonoFontFamily)
return "Default";
return Prefs.monoFontFamily || "Default";
}
enableFuzzySearch: true
popupWidthOffset: 100
maxPopupHeight: 400
options: {
var fonts = ["Default"];
var availableFonts = Qt.fontFamilies();
var monoFamilies = [];
var seenFamilies = new Set();
for (var i = 0; i < availableFonts.length; i++) {
var fontName = availableFonts[i];
if (fontName.startsWith("."))
continue;
if (fontName === Prefs.defaultMonoFontFamily)
continue;
var lowerName = fontName.toLowerCase();
if (lowerName.includes("mono") || lowerName.includes("code") || lowerName.includes("console") || lowerName.includes("terminal") || lowerName.includes("courier") || lowerName.includes("dejavu sans mono") || lowerName.includes("jetbrains") || lowerName.includes("fira") || lowerName.includes("hack") || lowerName.includes("source code") || lowerName.includes("ubuntu mono") || lowerName.includes("cascadia")) {
var rootName = fontName.replace(/ (Thin|Extra Light|Light|Regular|Medium|Semi Bold|Demi Bold|Bold|Extra Bold|Black|Heavy)$/i, "").replace(/ (Italic|Oblique|Condensed|Extended|Narrow|Wide)$/i, "").trim();
if (!seenFamilies.has(rootName) && rootName !== "") {
seenFamilies.add(rootName);
monoFamilies.push(rootName);
}
}
}
return fonts.concat(monoFamilies.sort());
}
onValueChanged: (value) => {
if (value === "Default")
Prefs.setMonoFontFamily(Prefs.defaultMonoFontFamily);
else
Prefs.setMonoFontFamily(value);
}
}
}
}
StyledRect {
width: parent.width
height: transparencySection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadiusLarge
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
Column {
id: transparencySection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "opacity"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Transparency Settings"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Column {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: "Top Bar Transparency"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankSlider {
width: parent.width
height: 24
value: Math.round(Prefs.topBarTransparency * 100)
minimum: 0
maximum: 100
unit: ""
showValue: true
onSliderValueChanged: (newValue) => {
Prefs.setTopBarTransparency(newValue / 100);
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: "Top Bar Widget Transparency"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankSlider {
width: parent.width
height: 24
value: Math.round(Prefs.topBarWidgetTransparency * 100)
minimum: 0
maximum: 100
unit: ""
showValue: true
onSliderValueChanged: (newValue) => {
Prefs.setTopBarWidgetTransparency(newValue / 100);
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: "Popup Transparency"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankSlider {
width: parent.width
height: 24
value: Math.round(Prefs.popupTransparency * 100)
minimum: 0
maximum: 100
unit: ""
showValue: true
onSliderValueChanged: (newValue) => {
Prefs.setPopupTransparency(newValue / 100);
}
}
}
}
}
StyledRect {
width: parent.width
height: themeSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadiusLarge
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
Column {
id: themeSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "palette"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Theme Color"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Column {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: "Current Theme: " + (Theme.isDynamicTheme ? "Auto" : (Theme.currentThemeIndex < Theme.themes.length ? Theme.themes[Theme.currentThemeIndex].name : "Blue"))
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: {
if (Theme.isDynamicTheme)
return "Wallpaper-based dynamic colors";
var descriptions = ["Material blue inspired by modern interfaces", "Deep blue inspired by material 3", "Rich purple tones for BB elegance", "Natural green for productivity", "Energetic orange for creativity", "Bold red for impact", "Cool cyan for tranquility", "Vibrant pink for expression", "Warm amber for comfort", "Soft coral for gentle warmth"];
return descriptions[Theme.currentThemeIndex] || "Select a theme";
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
wrapMode: Text.WordWrap
width: Math.min(parent.width, 400)
horizontalAlignment: Text.AlignHCenter
}
}
Column {
spacing: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
Row {
spacing: Theme.spacingM
anchors.horizontalCenter: parent.horizontalCenter
Repeater {
model: 5
Rectangle {
width: 32
height: 32
radius: 16
color: Theme.themes[index].primary
border.color: Theme.outline
border.width: (Theme.currentThemeIndex === index && !Theme.isDynamicTheme) ? 2 : 1
scale: (Theme.currentThemeIndex === index && !Theme.isDynamicTheme) ? 1.1 : 1
Rectangle {
width: nameText.contentWidth + Theme.spacingS * 2
height: nameText.contentHeight + Theme.spacingXS * 2
color: Theme.surfaceContainer
border.color: Theme.outline
border.width: 1
radius: Theme.cornerRadiusSmall
anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingXS
anchors.horizontalCenter: parent.horizontalCenter
visible: mouseArea.containsMouse
StyledText {
id: nameText
text: Theme.themes[index].name
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.centerIn: parent
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
Theme.switchTheme(index, false);
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on border.width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
}
Row {
spacing: Theme.spacingM
anchors.horizontalCenter: parent.horizontalCenter
Repeater {
model: 5
Rectangle {
property int themeIndex: index + 5
width: 32
height: 32
radius: 16
color: themeIndex < Theme.themes.length ? Theme.themes[themeIndex].primary : "transparent"
border.color: Theme.outline
border.width: Theme.currentThemeIndex === themeIndex ? 2 : 1
visible: themeIndex < Theme.themes.length
scale: Theme.currentThemeIndex === themeIndex ? 1.1 : 1
Rectangle {
width: nameText2.contentWidth + Theme.spacingS * 2
height: nameText2.contentHeight + Theme.spacingXS * 2
color: Theme.surfaceContainer
border.color: Theme.outline
border.width: 1
radius: Theme.cornerRadiusSmall
anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingXS
anchors.horizontalCenter: parent.horizontalCenter
visible: mouseArea2.containsMouse && themeIndex < Theme.themes.length
StyledText {
id: nameText2
text: themeIndex < Theme.themes.length ? Theme.themes[themeIndex].name : ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.centerIn: parent
}
}
MouseArea {
id: mouseArea2
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (themeIndex < Theme.themes.length)
Theme.switchTheme(themeIndex);
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on border.width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
}
Item {
width: 1
height: Theme.spacingM
}
Rectangle {
width: 120
height: 40
radius: 20
anchors.horizontalCenter: parent.horizontalCenter
color: {
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing")
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12);
else
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3);
}
border.color: {
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing")
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.5);
else if (Theme.isDynamicTheme)
return Theme.primary;
else
return Theme.outline;
}
border.width: Theme.isDynamicTheme ? 2 : 1
scale: Theme.isDynamicTheme ? 1.1 : (autoMouseArea.containsMouse ? 1.02 : 1)
Row {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: {
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing")
return "error";
else
return "palette";
}
size: 16
color: {
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing")
return Theme.error;
else
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: {
if (ToastService.wallpaperErrorStatus === "error")
return "Error";
else if (ToastService.wallpaperErrorStatus === "matugen_missing")
return "No matugen";
else
return "Auto";
}
font.pixelSize: Theme.fontSizeMedium
color: {
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing")
return Theme.error;
else
return Theme.surfaceText;
}
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: autoMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (ToastService.wallpaperErrorStatus === "matugen_missing")
ToastService.showError("matugen not found - install matugen package for dynamic theming");
else if (ToastService.wallpaperErrorStatus === "error")
ToastService.showError("Wallpaper processing failed - check wallpaper path");
else
Theme.switchTheme(10, true);
}
}
Rectangle {
width: autoTooltipText.contentWidth + Theme.spacingM * 2
height: autoTooltipText.contentHeight + Theme.spacingS * 2
color: Theme.surfaceContainer
border.color: Theme.outline
border.width: 1
radius: Theme.cornerRadiusSmall
anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
visible: autoMouseArea.containsMouse && (!Theme.isDynamicTheme || ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing")
StyledText {
id: autoTooltipText
text: {
if (ToastService.wallpaperErrorStatus === "matugen_missing")
return "Install matugen package for dynamic themes";
else
return "Dynamic wallpaper-based colors";
}
font.pixelSize: Theme.fontSizeSmall
color: (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") ? Theme.error : Theme.surfaceText
anchors.centerIn: parent
wrapMode: Text.WordWrap
width: Math.min(implicitWidth, 250)
horizontalAlignment: Text.AlignHCenter
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on color {
ColorAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
StyledRect {
width: parent.width
height: systemThemingSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadiusLarge
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
visible: Theme.isDynamicTheme && Colors.matugenAvailable
Column {
id: systemThemingSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "extension"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "System App Theming"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
width: parent.width
text: "Theme GTK Applications"
description: Colors.gtkThemingEnabled ? "File managers, text editors, and system dialogs will match your theme" : "GTK theming not available (install gsettings)"
enabled: Colors.gtkThemingEnabled
checked: Colors.gtkThemingEnabled && Prefs.gtkThemingEnabled
onToggled: function(checked) {
Prefs.setGtkThemingEnabled(checked);
if (checked && Theme.isDynamicTheme)
Colors.generateGtkThemes();
}
}
DankToggle {
width: parent.width
text: "Theme Qt Applications"
description: Colors.qtThemingEnabled ? "Qt applications will match your theme colors" : "Qt theming not available (install qt5ct or qt6ct)"
enabled: Colors.qtThemingEnabled
checked: Colors.qtThemingEnabled && Prefs.qtThemingEnabled
onToggled: function(checked) {
Prefs.setQtThemingEnabled(checked);
if (checked && Theme.isDynamicTheme)
Colors.generateQtThemes();
}
}
}
}
}
Process {
id: nightModeEnableProcess
command: ["bash", "-c", "if command -v wlsunset > /dev/null; then pkill wlsunset; wlsunset -t 3000 & elif command -v redshift > /dev/null; then pkill redshift; redshift -P -O 3000 & else echo 'No night mode tool available'; fi"]
running: false
onExited: (exitCode) => {
if (exitCode !== 0)
Prefs.setNightModeEnabled(true);
}
}
Process {
id: nightModeDisableProcess
command: ["bash", "-c", "pkill wlsunset; pkill redshift; if command -v wlsunset > /dev/null; then wlsunset -t 6500 -T 6500 & sleep 1; pkill wlsunset; elif command -v redshift > /dev/null; then redshift -P -O 6500; redshift -x; fi"]
running: false
onExited: (exitCode) => {
if (exitCode !== 0)
Prefs.setNightModeEnabled(false);
}
}
}
+469
View File
@@ -0,0 +1,469 @@
import QtQuick
import QtQuick.Controls
import Quickshell.Widgets
import qs.Common
import qs.Widgets
ScrollView {
id: launcherTab
contentHeight: column.implicitHeight + Theme.spacingXL
clip: true
Column {
id: column
width: parent.width
spacing: Theme.spacingXL
topPadding: Theme.spacingL
bottomPadding: Theme.spacingXL
StyledRect {
width: parent.width
height: appLauncherSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadiusLarge
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
Column {
id: appLauncherSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
DankToggle {
width: parent.width
text: "Use OS Logo"
description: "Display operating system logo instead of apps icon"
checked: Prefs.useOSLogo
onToggled: (checked) => {
return Prefs.setUseOSLogo(checked);
}
}
Row {
width: parent.width - Theme.spacingL
spacing: Theme.spacingL
visible: Prefs.useOSLogo
opacity: visible ? 1 : 0
anchors.left: parent.left
anchors.leftMargin: Theme.spacingL
Column {
width: 120
spacing: Theme.spacingS
StyledText {
text: "Color Override"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankTextField {
width: 100
height: 28
placeholderText: "#ffffff"
text: Prefs.osLogoColorOverride
maximumLength: 7
font.pixelSize: Theme.fontSizeSmall
topPadding: Theme.spacingXS
bottomPadding: Theme.spacingXS
onEditingFinished: {
var color = text.trim();
if (color === "" || /^#[0-9A-Fa-f]{6}$/.test(color))
Prefs.setOSLogoColorOverride(color);
else
text = Prefs.osLogoColorOverride;
}
}
}
Column {
width: 120
spacing: Theme.spacingS
StyledText {
text: "Brightness"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankSlider {
width: 100
height: 20
minimum: 0
maximum: 100
value: Math.round(Prefs.osLogoBrightness * 100)
unit: "%"
showValue: true
onSliderValueChanged: (newValue) => {
Prefs.setOSLogoBrightness(newValue / 100);
}
}
}
Column {
width: 120
spacing: Theme.spacingS
StyledText {
text: "Contrast"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankSlider {
width: 100
height: 20
minimum: 0
maximum: 200
value: Math.round(Prefs.osLogoContrast * 100)
unit: "%"
showValue: true
onSliderValueChanged: (newValue) => {
Prefs.setOSLogoContrast(newValue / 100);
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
}
StyledRect {
width: parent.width
height: dockSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadiusLarge
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
Column {
id: dockSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "dock_to_bottom"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Dock"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
width: parent.width
text: "Show Dock"
description: "Display a dock at the bottom of the screen with pinned and running applications"
checked: Prefs.showDock
onToggled: (checked) => {
Prefs.setShowDock(checked)
}
}
DankToggle {
width: parent.width
text: "Auto-hide Dock"
description: "Hide the dock when not in use and reveal it when hovering near the bottom of the screen"
checked: Prefs.dockAutoHide
visible: Prefs.showDock
opacity: visible ? 1 : 0
onToggled: (checked) => {
Prefs.setDockAutoHide(checked)
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
visible: Prefs.showDock
opacity: visible ? 1 : 0
StyledText {
text: "Dock Transparency"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankSlider {
width: parent.width
height: 24
value: Math.round(Prefs.dockTransparency * 100)
minimum: 0
maximum: 100
unit: ""
showValue: true
onSliderValueChanged: (newValue) => {
Prefs.setDockTransparency(newValue / 100);
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
}
StyledRect {
width: parent.width
height: recentlyUsedSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadiusLarge
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
Column {
id: recentlyUsedSection
property var rankedAppsModel: {
var apps = [];
for (var appId in Prefs.appUsageRanking) {
var appData = Prefs.appUsageRanking[appId];
apps.push({
"id": appId,
"name": appData.name,
"exec": appData.exec,
"icon": appData.icon,
"comment": appData.comment,
"usageCount": appData.usageCount,
"lastUsed": appData.lastUsed
});
}
apps.sort(function(a, b) {
if (a.usageCount !== b.usageCount)
return b.usageCount - a.usageCount;
return a.name.localeCompare(b.name);
});
return apps.slice(0, 20);
}
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "history"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Recently Used Apps"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - parent.children[0].width - parent.children[1].width - clearAllButton.width - Theme.spacingM * 3
height: 1
}
DankActionButton {
id: clearAllButton
iconName: "delete_sweep"
iconSize: Theme.iconSize - 2
iconColor: Theme.error
hoverColor: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
anchors.verticalCenter: parent.verticalCenter
onClicked: {
Prefs.appUsageRanking = {
};
Prefs.saveSettings();
}
}
}
StyledText {
width: parent.width
text: "Apps are ordered by usage frequency, then last used, then alphabetically."
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
Column {
id: rankedAppsList
width: parent.width
spacing: Theme.spacingS
Repeater {
model: recentlyUsedSection.rankedAppsModel
delegate: Rectangle {
width: rankedAppsList.width
height: 48
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
border.width: 1
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
StyledText {
text: (index + 1).toString()
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.primary
width: 20
anchors.verticalCenter: parent.verticalCenter
}
Image {
width: 24
height: 24
source: modelData.icon ? "image://icon/" + modelData.icon : "image://icon/application-x-executable"
sourceSize.width: 24
sourceSize.height: 24
fillMode: Image.PreserveAspectFit
anchors.verticalCenter: parent.verticalCenter
onStatusChanged: {
if (status === Image.Error)
source = "image://icon/application-x-executable";
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: modelData.name || "Unknown App"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: {
if (!modelData.lastUsed)
return "Never used";
var date = new Date(modelData.lastUsed);
var now = new Date();
var diffMs = now - date;
var diffMins = Math.floor(diffMs / (1000 * 60));
var diffHours = Math.floor(diffMs / (1000 * 60 * 60));
var diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
if (diffMins < 1)
return "Last launched just now";
if (diffMins < 60)
return "Last launched " + diffMins + " minute" + (diffMins === 1 ? "" : "s") + " ago";
if (diffHours < 24)
return "Last launched " + diffHours + " hour" + (diffHours === 1 ? "" : "s") + " ago";
if (diffDays < 7)
return "Last launched " + diffDays + " day" + (diffDays === 1 ? "" : "s") + " ago";
return "Last launched " + date.toLocaleDateString();
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
DankActionButton {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
circular: true
iconName: "close"
iconSize: 16
iconColor: Theme.error
hoverColor: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
onClicked: {
var currentRanking = Object.assign({
}, Prefs.appUsageRanking);
delete currentRanking[modelData.id];
Prefs.appUsageRanking = currentRanking;
Prefs.saveSettings();
}
}
}
}
StyledText {
width: parent.width
text: recentlyUsedSection.rankedAppsModel.length === 0 ? "No apps have been launched yet." : ""
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignHCenter
visible: recentlyUsedSection.rankedAppsModel.length === 0
}
}
}
}
}
}
+581
View File
@@ -0,0 +1,581 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import qs.Common
import qs.Modals
import qs.Services
import qs.Widgets
ScrollView {
id: personalizationTab
property alias profileBrowser: profileBrowserLoader.item
property alias wallpaperBrowser: wallpaperBrowserLoader.item
contentHeight: column.implicitHeight
clip: true
Column {
id: column
width: parent.width
spacing: Theme.spacingXL
StyledRect {
width: parent.width
height: profileSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadiusLarge
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
Column {
id: profileSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "person"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Profile Image"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
width: parent.width
spacing: Theme.spacingL
Item {
id: avatarContainer
property bool hasImage: avatarImageSource.status === Image.Ready
width: 80
height: 80
Rectangle {
anchors.fill: parent
radius: width / 2
color: "transparent"
border.color: Theme.primary
border.width: 1
visible: parent.hasImage
}
Image {
id: avatarImageSource
source: {
if (Prefs.profileImage === "")
return "";
if (Prefs.profileImage.startsWith("/"))
return "file://" + Prefs.profileImage;
return Prefs.profileImage;
}
smooth: true
asynchronous: true
mipmap: true
cache: true
visible: false
}
MultiEffect {
anchors.fill: parent
anchors.margins: 5
source: avatarImageSource
maskEnabled: true
maskSource: settingsCircularMask
visible: avatarContainer.hasImage
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
Item {
id: settingsCircularMask
width: 70
height: 70
layer.enabled: true
layer.smooth: true
visible: false
Rectangle {
anchors.fill: parent
radius: width / 2
color: "black"
antialiasing: true
}
}
Rectangle {
anchors.fill: parent
radius: width / 2
color: Theme.primary
visible: !parent.hasImage
DankIcon {
anchors.centerIn: parent
name: "person"
size: Theme.iconSizeLarge + 8
color: Theme.primaryText
}
}
DankIcon {
anchors.centerIn: parent
name: "warning"
size: Theme.iconSizeLarge
color: Theme.error
visible: Prefs.profileImage !== "" && avatarImageSource.status === Image.Error
}
}
Column {
width: parent.width - 80 - Theme.spacingL
spacing: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: Prefs.profileImage ? Prefs.profileImage.split('/').pop() : "No profile image selected"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
elide: Text.ElideMiddle
width: parent.width
}
StyledText {
text: Prefs.profileImage ? Prefs.profileImage : ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideMiddle
width: parent.width
visible: Prefs.profileImage !== ""
}
Row {
spacing: Theme.spacingS
StyledRect {
width: 100
height: 32
radius: Theme.cornerRadius
color: Theme.primary
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "folder_open"
size: Theme.iconSizeSmall
color: Theme.primaryText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Browse"
color: Theme.primaryText
font.pixelSize: Theme.fontSizeSmall
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
profileBrowserLoader.active = true;
profileBrowser.visible = true;
}
}
}
StyledRect {
width: 80
height: 32
radius: Theme.cornerRadius
color: Theme.surfaceVariant
opacity: Prefs.profileImage !== "" ? 1 : 0.5
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "clear"
size: Theme.iconSizeSmall
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Clear"
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
anchors.fill: parent
enabled: Prefs.profileImage !== ""
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
Prefs.setProfileImage("");
}
}
}
}
}
}
}
}
StyledRect {
width: parent.width
height: wallpaperSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadiusLarge
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
Column {
id: wallpaperSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "wallpaper"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Wallpaper"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
width: parent.width
spacing: Theme.spacingL
StyledRect {
width: 160
height: 90
radius: Theme.cornerRadius
color: Theme.surfaceVariant
border.color: Theme.outline
border.width: 1
CachingImage {
anchors.fill: parent
anchors.margins: 1
imagePath: Prefs.wallpaperPath || ""
fillMode: Image.PreserveAspectCrop
visible: Prefs.wallpaperPath !== ""
maxCacheSize: 160
layer.enabled: true
layer.effect: MultiEffect {
maskEnabled: true
maskSource: wallpaperMask
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
}
Rectangle {
id: wallpaperMask
anchors.fill: parent
anchors.margins: 1
radius: Theme.cornerRadius - 1
color: "black"
visible: false
layer.enabled: true
}
DankIcon {
anchors.centerIn: parent
name: "image"
size: Theme.iconSizeLarge + 8
color: Theme.surfaceVariantText
visible: Prefs.wallpaperPath === ""
}
}
Column {
width: parent.width - 160 - Theme.spacingL
spacing: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: Prefs.wallpaperPath ? Prefs.wallpaperPath.split('/').pop() : "No wallpaper selected"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
elide: Text.ElideMiddle
width: parent.width
}
StyledText {
text: Prefs.wallpaperPath ? Prefs.wallpaperPath : ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideMiddle
width: parent.width
visible: Prefs.wallpaperPath !== ""
}
Row {
spacing: Theme.spacingS
StyledRect {
width: 100
height: 32
radius: Theme.cornerRadius
color: Theme.primary
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "folder_open"
size: Theme.iconSizeSmall
color: Theme.primaryText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Browse"
color: Theme.primaryText
font.pixelSize: Theme.fontSizeSmall
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
wallpaperBrowserLoader.active = true;
wallpaperBrowser.visible = true;
}
}
}
StyledRect {
width: 80
height: 32
radius: Theme.cornerRadius
color: Theme.surfaceVariant
opacity: Prefs.wallpaperPath !== "" ? 1 : 0.5
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "clear"
size: Theme.iconSizeSmall
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Clear"
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
anchors.fill: parent
enabled: Prefs.wallpaperPath !== ""
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
Prefs.setWallpaperPath("");
}
}
}
}
}
}
}
}
StyledRect {
width: parent.width
height: dynamicThemeSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadiusLarge
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
Column {
id: dynamicThemeSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "palette"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM - toggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "Dynamic Theming"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: "Automatically extract colors from wallpaper"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
DankToggle {
id: toggle
anchors.verticalCenter: parent.verticalCenter
checked: Theme.isDynamicTheme
enabled: ToastService.wallpaperErrorStatus !== "matugen_missing"
onToggled: (toggled) => {
if (toggled)
Theme.switchTheme(10, true);
else
Theme.switchTheme(0);
}
}
}
StyledText {
text: "matugen not detected - dynamic theming unavailable"
font.pixelSize: Theme.fontSizeSmall
color: Theme.error
visible: ToastService.wallpaperErrorStatus === "matugen_missing"
width: parent.width
leftPadding: Theme.iconSize + Theme.spacingM
}
}
}
}
LazyLoader {
id: profileBrowserLoader
active: false
FileBrowserModal {
id: profileBrowser
browserTitle: "Select Profile Image"
browserIcon: "person"
browserType: "profile"
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
onFileSelected: (path) => {
Prefs.setProfileImage(path);
visible = false;
}
}
}
LazyLoader {
id: wallpaperBrowserLoader
active: false
FileBrowserModal {
id: wallpaperBrowser
browserTitle: "Select Wallpaper"
browserIcon: "wallpaper"
browserType: "wallpaper"
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
onFileSelected: (path) => {
Prefs.setWallpaperPath(path);
visible = false;
}
}
}
}
+114
View File
@@ -0,0 +1,114 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Widgets
Column {
id: root
property string title: ""
property string iconName: ""
property alias content: contentLoader.sourceComponent
property bool expanded: false
property bool collapsible: true
property bool lazyLoad: true
width: parent.width
spacing: expanded ? Theme.spacingM : 0
Component.onCompleted: {
if (!collapsible)
expanded = true;
}
MouseArea {
width: parent.width
height: headerRow.height
enabled: collapsible
hoverEnabled: collapsible
onClicked: {
if (collapsible)
expanded = !expanded;
}
Rectangle {
anchors.fill: parent
color: parent.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: Theme.radiusS
}
Row {
id: headerRow
width: parent.width
spacing: Theme.spacingS
topPadding: Theme.spacingS
bottomPadding: Theme.spacingS
DankIcon {
name: root.collapsible ? (root.expanded ? "expand_less" : "expand_more") : root.iconName
size: Theme.iconSize - 2
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
Behavior on rotation {
NumberAnimation {
duration: Appearance.anim.durations.fast
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
DankIcon {
name: root.iconName
size: Theme.iconSize - 4
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
visible: root.collapsible
}
StyledText {
text: root.title
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
}
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
visible: expanded || !collapsible
}
Loader {
id: contentLoader
width: parent.width
active: lazyLoad ? expanded || !collapsible : true
visible: expanded || !collapsible
asynchronous: true
opacity: visible ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
}
+142
View File
@@ -0,0 +1,142 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Widgets
ScrollView {
id: timeWeatherTab
contentHeight: column.implicitHeight
clip: true
Column {
id: column
width: parent.width
spacing: Theme.spacingXL
StyledRect {
width: parent.width
height: timeSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadiusLarge
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
Column {
id: timeSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "schedule"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Time Format"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
width: parent.width
text: "24-Hour Format"
description: "Use 24-hour time format instead of 12-hour AM/PM"
checked: Prefs.use24HourClock
onToggled: (checked) => {
return Prefs.setClockFormat(checked);
}
}
}
}
StyledRect {
width: parent.width
height: weatherSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadiusLarge
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
Column {
id: weatherSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "cloud"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Weather"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
width: parent.width
text: "Fahrenheit"
description: "Use Fahrenheit instead of Celsius for temperature"
checked: Prefs.useFahrenheit
onToggled: (checked) => {
return Prefs.setTemperatureUnit(checked);
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: "Location"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
DankLocationSearch {
width: parent.width
currentLocation: Prefs.weatherLocation
placeholderText: "New York, NY"
onLocationSelected: (displayName, coordinates) => {
Prefs.setWeatherLocation(displayName, coordinates);
}
}
}
}
}
}
}
+633
View File
@@ -0,0 +1,633 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Widgets
ScrollView {
id: widgetsTab
property var baseWidgetDefinitions: [{
"id": "launcherButton",
"text": "App Launcher",
"description": "Quick access to application launcher",
"icon": "apps",
"enabled": true
}, {
"id": "workspaceSwitcher",
"text": "Workspace Switcher",
"description": "Shows current workspace and allows switching",
"icon": "view_module",
"enabled": true
}, {
"id": "focusedWindow",
"text": "Focused Window",
"description": "Display currently focused application title",
"icon": "window",
"enabled": true
}, {
"id": "clock",
"text": "Clock",
"description": "Current time and date display",
"icon": "schedule",
"enabled": true
}, {
"id": "weather",
"text": "Weather Widget",
"description": "Current weather conditions and temperature",
"icon": "wb_sunny",
"enabled": true
}, {
"id": "music",
"text": "Media Controls",
"description": "Control currently playing media",
"icon": "music_note",
"enabled": true
}, {
"id": "clipboard",
"text": "Clipboard Manager",
"description": "Access clipboard history",
"icon": "content_paste",
"enabled": true
}, {
"id": "systemResources",
"text": "System Resources",
"description": "CPU and memory usage indicators",
"icon": "memory",
"enabled": true
}, {
"id": "systemTray",
"text": "System Tray",
"description": "System notification area icons",
"icon": "notifications",
"enabled": true
}, {
"id": "controlCenterButton",
"text": "Control Center",
"description": "Access to system controls and settings",
"icon": "settings",
"enabled": true
}, {
"id": "notificationButton",
"text": "Notification Center",
"description": "Access to notifications and do not disturb",
"icon": "notifications",
"enabled": true
}, {
"id": "battery",
"text": "Battery",
"description": "Battery level and power management",
"icon": "battery_std",
"enabled": true
}, {
"id": "spacer",
"text": "Spacer",
"description": "Customizable empty space",
"icon": "more_horiz",
"enabled": true
}, {
"id": "separator",
"text": "Separator",
"description": "Visual divider between widgets",
"icon": "remove",
"enabled": true
}]
property var defaultLeftWidgets: [{
"id": "launcherButton",
"enabled": true
}, {
"id": "workspaceSwitcher",
"enabled": true
}, {
"id": "focusedWindow",
"enabled": true
}]
property var defaultCenterWidgets: [{
"id": "music",
"enabled": true
}, {
"id": "clock",
"enabled": true
}, {
"id": "weather",
"enabled": true
}]
property var defaultRightWidgets: [{
"id": "systemTray",
"enabled": true
}, {
"id": "clipboard",
"enabled": true
}, {
"id": "systemResources",
"enabled": true
}, {
"id": "notificationButton",
"enabled": true
}, {
"id": "battery",
"enabled": true
}, {
"id": "controlCenterButton",
"enabled": true
}]
function addWidgetToSection(widgetId, targetSection) {
var widgetObj = {
"id": widgetId,
"enabled": true
};
if (widgetId === "spacer")
widgetObj.size = 20;
var widgets = [];
if (targetSection === "left") {
widgets = Prefs.topBarLeftWidgets.slice();
widgets.push(widgetObj);
Prefs.setTopBarLeftWidgets(widgets);
} else if (targetSection === "center") {
widgets = Prefs.topBarCenterWidgets.slice();
widgets.push(widgetObj);
Prefs.setTopBarCenterWidgets(widgets);
} else if (targetSection === "right") {
widgets = Prefs.topBarRightWidgets.slice();
widgets.push(widgetObj);
Prefs.setTopBarRightWidgets(widgets);
}
}
function removeWidgetFromSection(sectionId, itemId) {
var widgets = [];
if (sectionId === "left") {
widgets = Prefs.topBarLeftWidgets.slice();
widgets = widgets.filter((widget) => {
var widgetId = typeof widget === "string" ? widget : widget.id;
return widgetId !== itemId;
});
Prefs.setTopBarLeftWidgets(widgets);
} else if (sectionId === "center") {
widgets = Prefs.topBarCenterWidgets.slice();
widgets = widgets.filter((widget) => {
var widgetId = typeof widget === "string" ? widget : widget.id;
return widgetId !== itemId;
});
Prefs.setTopBarCenterWidgets(widgets);
} else if (sectionId === "right") {
widgets = Prefs.topBarRightWidgets.slice();
widgets = widgets.filter((widget) => {
var widgetId = typeof widget === "string" ? widget : widget.id;
return widgetId !== itemId;
});
Prefs.setTopBarRightWidgets(widgets);
}
}
function handleItemEnabledChanged(sectionId, itemId, enabled) {
var widgets = [];
if (sectionId === "left")
widgets = Prefs.topBarLeftWidgets.slice();
else if (sectionId === "center")
widgets = Prefs.topBarCenterWidgets.slice();
else if (sectionId === "right")
widgets = Prefs.topBarRightWidgets.slice();
for (var i = 0; i < widgets.length; i++) {
var widget = widgets[i];
var widgetId = typeof widget === "string" ? widget : widget.id;
if (widgetId === itemId) {
widgets[i] = typeof widget === "string" ? {
"id": widget,
"enabled": enabled
} : {
"id": widget.id,
"enabled": enabled,
"size": widget.size
};
break;
}
}
if (sectionId === "left")
Prefs.setTopBarLeftWidgets(widgets);
else if (sectionId === "center")
Prefs.setTopBarCenterWidgets(widgets);
else if (sectionId === "right")
Prefs.setTopBarRightWidgets(widgets);
}
function handleItemOrderChanged(sectionId, newOrder) {
if (sectionId === "left")
Prefs.setTopBarLeftWidgets(newOrder);
else if (sectionId === "center")
Prefs.setTopBarCenterWidgets(newOrder);
else if (sectionId === "right")
Prefs.setTopBarRightWidgets(newOrder);
}
function handleSpacerSizeChanged(sectionId, itemId, newSize) {
var widgets = [];
if (sectionId === "left")
widgets = Prefs.topBarLeftWidgets.slice();
else if (sectionId === "center")
widgets = Prefs.topBarCenterWidgets.slice();
else if (sectionId === "right")
widgets = Prefs.topBarRightWidgets.slice();
for (var i = 0; i < widgets.length; i++) {
var widget = widgets[i];
var widgetId = typeof widget === "string" ? widget : widget.id;
if (widgetId === itemId && widgetId === "spacer") {
widgets[i] = typeof widget === "string" ? {
"id": widget,
"enabled": true,
"size": newSize
} : {
"id": widget.id,
"enabled": widget.enabled,
"size": newSize
};
break;
}
}
if (sectionId === "left")
Prefs.setTopBarLeftWidgets(widgets);
else if (sectionId === "center")
Prefs.setTopBarCenterWidgets(widgets);
else if (sectionId === "right")
Prefs.setTopBarRightWidgets(widgets);
}
function getItemsForSection(sectionId) {
var widgets = [];
var widgetData = [];
if (sectionId === "left")
widgetData = Prefs.topBarLeftWidgets || [];
else if (sectionId === "center")
widgetData = Prefs.topBarCenterWidgets || [];
else if (sectionId === "right")
widgetData = Prefs.topBarRightWidgets || [];
widgetData.forEach((widget) => {
var widgetId = typeof widget === "string" ? widget : widget.id;
var widgetEnabled = typeof widget === "string" ? true : widget.enabled;
var widgetSize = typeof widget === "string" ? undefined : widget.size;
var widgetDef = baseWidgetDefinitions.find((w) => {
return w.id === widgetId;
});
if (widgetDef) {
var item = Object.assign({
}, widgetDef);
item.enabled = widgetEnabled;
if (widgetSize !== undefined)
item.size = widgetSize;
widgets.push(item);
}
});
return widgets;
}
contentHeight: column.implicitHeight + Theme.spacingXL
clip: true
Component.onCompleted: {
if (!Prefs.topBarLeftWidgets || Prefs.topBarLeftWidgets.length === 0)
Prefs.setTopBarLeftWidgets(defaultLeftWidgets);
if (!Prefs.topBarCenterWidgets || Prefs.topBarCenterWidgets.length === 0)
Prefs.setTopBarCenterWidgets(defaultCenterWidgets);
if (!Prefs.topBarRightWidgets || Prefs.topBarRightWidgets.length === 0)
Prefs.setTopBarRightWidgets(defaultRightWidgets);
["left", "center", "right"].forEach((sectionId) => {
var widgets = [];
if (sectionId === "left")
widgets = Prefs.topBarLeftWidgets.slice();
else if (sectionId === "center")
widgets = Prefs.topBarCenterWidgets.slice();
else if (sectionId === "right")
widgets = Prefs.topBarRightWidgets.slice();
var updated = false;
for (var i = 0; i < widgets.length; i++) {
var widget = widgets[i];
if (typeof widget === "object" && widget.id === "spacer" && !widget.size) {
widgets[i] = Object.assign({
}, widget, {
"size": 20
});
updated = true;
}
}
if (updated) {
if (sectionId === "left")
Prefs.setTopBarLeftWidgets(widgets);
else if (sectionId === "center")
Prefs.setTopBarCenterWidgets(widgets);
else if (sectionId === "right")
Prefs.setTopBarRightWidgets(widgets);
}
});
}
Column {
id: column
width: parent.width
spacing: Theme.spacingXL
topPadding: Theme.spacingL
bottomPadding: Theme.spacingXL
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "widgets"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Top Bar Widget Management"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - 400
height: 1
}
Rectangle {
width: 80
height: 28
radius: Theme.cornerRadius
color: resetArea.containsMouse ? Theme.surfacePressed : Theme.surfaceVariant
anchors.verticalCenter: parent.verticalCenter
border.width: 1
border.color: resetArea.containsMouse ? Theme.outline : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.5)
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "refresh"
size: 14
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Reset"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: resetArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
Prefs.setTopBarLeftWidgets(defaultLeftWidgets);
Prefs.setTopBarCenterWidgets(defaultCenterWidgets);
Prefs.setTopBarRightWidgets(defaultRightWidgets);
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
Rectangle {
width: parent.width
height: messageText.contentHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 1
visible: true
opacity: 1
z: 1
StyledText {
id: messageText
anchors.centerIn: parent
text: "Drag widgets to reorder within sections. Use the eye icon to hide/show widgets (maintains spacing), or X to remove them completely."
font.pixelSize: Theme.fontSizeSmall
color: Theme.outline
width: parent.width - Theme.spacingM * 2
wrapMode: Text.WordWrap
}
}
Column {
width: parent.width
spacing: Theme.spacingL
DankSections {
width: parent.width
title: "Left Section"
titleIcon: "format_align_left"
sectionId: "left"
allWidgets: widgetsTab.baseWidgetDefinitions
items: widgetsTab.getItemsForSection("left")
onItemEnabledChanged: (sectionId, itemId, enabled) => {
widgetsTab.handleItemEnabledChanged(sectionId, itemId, enabled);
}
onItemOrderChanged: (newOrder) => {
widgetsTab.handleItemOrderChanged("left", newOrder);
}
onAddWidget: (sectionId) => {
widgetSelectionPopup.allWidgets = widgetsTab.baseWidgetDefinitions;
widgetSelectionPopup.targetSection = sectionId;
widgetSelectionPopup.safeOpen();
}
onRemoveWidget: (sectionId, itemId) => {
widgetsTab.removeWidgetFromSection(sectionId, itemId);
}
onSpacerSizeChanged: (sectionId, itemId, newSize) => {
widgetsTab.handleSpacerSizeChanged(sectionId, itemId, newSize);
}
}
DankSections {
width: parent.width
title: "Center Section"
titleIcon: "format_align_center"
sectionId: "center"
allWidgets: widgetsTab.baseWidgetDefinitions
items: widgetsTab.getItemsForSection("center")
onItemEnabledChanged: (sectionId, itemId, enabled) => {
widgetsTab.handleItemEnabledChanged(sectionId, itemId, enabled);
}
onItemOrderChanged: (newOrder) => {
widgetsTab.handleItemOrderChanged("center", newOrder);
}
onAddWidget: (sectionId) => {
widgetSelectionPopup.allWidgets = widgetsTab.baseWidgetDefinitions;
widgetSelectionPopup.targetSection = sectionId;
widgetSelectionPopup.safeOpen();
}
onRemoveWidget: (sectionId, itemId) => {
widgetsTab.removeWidgetFromSection(sectionId, itemId);
}
onSpacerSizeChanged: (sectionId, itemId, newSize) => {
widgetsTab.handleSpacerSizeChanged(sectionId, itemId, newSize);
}
}
DankSections {
width: parent.width
title: "Right Section"
titleIcon: "format_align_right"
sectionId: "right"
allWidgets: widgetsTab.baseWidgetDefinitions
items: widgetsTab.getItemsForSection("right")
onItemEnabledChanged: (sectionId, itemId, enabled) => {
widgetsTab.handleItemEnabledChanged(sectionId, itemId, enabled);
}
onItemOrderChanged: (newOrder) => {
widgetsTab.handleItemOrderChanged("right", newOrder);
}
onAddWidget: (sectionId) => {
widgetSelectionPopup.allWidgets = widgetsTab.baseWidgetDefinitions;
widgetSelectionPopup.targetSection = sectionId;
widgetSelectionPopup.safeOpen();
}
onRemoveWidget: (sectionId, itemId) => {
widgetsTab.removeWidgetFromSection(sectionId, itemId);
}
onSpacerSizeChanged: (sectionId, itemId, newSize) => {
widgetsTab.handleSpacerSizeChanged(sectionId, itemId, newSize);
}
}
}
StyledRect {
width: parent.width
height: workspaceSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadiusLarge
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
Column {
id: workspaceSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "view_module"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Workspace Settings"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
width: parent.width
text: "Workspace Index Numbers"
description: "Show workspace index numbers in the top bar workspace switcher"
checked: Prefs.showWorkspaceIndex
onToggled: (checked) => {
return Prefs.setShowWorkspaceIndex(checked);
}
}
DankToggle {
width: parent.width
text: "Workspace Padding"
description: "Always show a minimum of 3 workspaces, even if fewer are available"
checked: Prefs.showWorkspacePadding
onToggled: (checked) => {
return Prefs.setShowWorkspacePadding(checked);
}
}
}
}
}
Rectangle {
width: tooltipText.contentWidth + Theme.spacingM * 2
height: tooltipText.contentHeight + Theme.spacingS * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainer
border.color: Theme.outline
border.width: 1
visible: resetArea.containsMouse
opacity: resetArea.containsMouse ? 1 : 0
y: column.y + 48 // Position above the reset button in the header
x: parent.width - width - Theme.spacingM
z: 100
StyledText {
id: tooltipText
anchors.centerIn: parent
text: "Reset widget layout to defaults"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
DankWidgetSelectionPopup {
id: widgetSelectionPopup
anchors.centerIn: parent
onWidgetSelected: (widgetId, targetSection) => {
widgetsTab.addWidgetToSection(widgetId, targetSection);
}
}
}
+137
View File
@@ -0,0 +1,137 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
PanelWindow {
id: root
property var modelData
screen: modelData
visible: ToastService.toastVisible
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
Rectangle {
id: toast
width: Math.min(400, Screen.width - Theme.spacingL * 2)
height: toastContent.height + Theme.spacingL * 2
anchors.horizontalCenter: parent.horizontalCenter
y: Theme.barHeight + Theme.spacingL
color: {
switch (ToastService.currentLevel) {
case ToastService.levelError:
return Theme.error;
case ToastService.levelWarn:
return Theme.warning;
case ToastService.levelInfo:
return Theme.primary;
default:
return Theme.primary;
}
}
radius: Theme.cornerRadiusLarge
layer.enabled: true
opacity: ToastService.toastVisible ? 0.9 : 0
scale: ToastService.toastVisible ? 1 : 0.9
Row {
id: toastContent
anchors.centerIn: parent
spacing: Theme.spacingM
DankIcon {
name: {
switch (ToastService.currentLevel) {
case ToastService.levelError:
return "error";
case ToastService.levelWarn:
return "warning";
case ToastService.levelInfo:
return "info";
default:
return "info";
}
}
size: Theme.iconSize
color: Theme.background
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: ToastService.currentMessage
font.pixelSize: Theme.fontSizeMedium
color: Theme.background
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
width: 300
wrapMode: Text.WordWrap
}
}
MouseArea {
anchors.fill: parent
onClicked: ToastService.hideToast()
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 4
shadowBlur: 0.8
shadowColor: Qt.rgba(0, 0, 0, 0.3)
shadowOpacity: 0.3
}
transform: Translate {
y: ToastService.toastVisible ? 0 : -20
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
mask: Region {
item: toast
}
}
+69
View File
@@ -0,0 +1,69 @@
import QtQuick
import Quickshell.Services.Mpris
import qs.Common
import qs.Services
Item {
id: root
readonly property MprisPlayer activePlayer: MprisController.activePlayer
readonly property bool hasActiveMedia: activePlayer !== null
readonly property bool isPlaying: hasActiveMedia && activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing
width: 20
height: Theme.iconSize
Ref {
service: CavaService
}
Timer {
id: fallbackTimer
running: !CavaService.cavaAvailable && isPlaying
interval: 256
repeat: true
onTriggered: {
CavaService.values = [Math.random() * 40 + 10, Math.random() * 60 + 20, Math.random() * 50 + 15, Math.random() * 35 + 20, Math.random() * 45 + 15, Math.random() * 55 + 25];
}
}
Row {
anchors.centerIn: parent
spacing: 1.5
Repeater {
model: 6
Rectangle {
width: 2
height: {
if (root.isPlaying && CavaService.values.length > index) {
const rawLevel = CavaService.values[index] || 0;
const scaledLevel = Math.sqrt(Math.min(Math.max(rawLevel, 0), 100) / 100) * 100;
const maxHeight = Theme.iconSize - 2;
const minHeight = 3;
return minHeight + (scaledLevel / 100) * (maxHeight - minHeight);
}
return 3;
}
radius: 1.5
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
Behavior on height {
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standardDecel
}
}
}
}
}
}
+168
View File
@@ -0,0 +1,168 @@
import QtQuick
import Quickshell.Services.UPower
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: battery
property bool batteryPopupVisible: false
signal toggleBatteryPopup()
width: BatteryService.batteryAvailable ? 70 : 40
height: 30
radius: Theme.cornerRadius
color: {
const baseColor = batteryArea.containsMouse || batteryPopupVisible ? Theme.primaryPressed : Theme.secondaryHover;
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
}
visible: true
Row {
anchors.centerIn: parent
spacing: 4
DankIcon {
name: Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable)
size: Theme.iconSize - 6
color: {
if (!BatteryService.batteryAvailable)
return Theme.surfaceText;
if (BatteryService.isLowBattery && !BatteryService.isCharging)
return Theme.error;
if (BatteryService.isCharging)
return Theme.primary;
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
SequentialAnimation on opacity {
running: BatteryService.isCharging
loops: Animation.Infinite
NumberAnimation {
to: 0.6
duration: Anims.durLong
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
NumberAnimation {
to: 1
duration: Anims.durLong
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
}
}
StyledText {
text: BatteryService.batteryLevel + "%"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: {
if (!BatteryService.batteryAvailable)
return Theme.surfaceText;
if (BatteryService.isLowBattery && !BatteryService.isCharging)
return Theme.error;
if (BatteryService.isCharging)
return Theme.primary;
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
visible: BatteryService.batteryAvailable
}
}
MouseArea {
id: batteryArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
toggleBatteryPopup();
}
}
Rectangle {
id: batteryTooltip
width: Math.max(120, tooltipText.contentWidth + Theme.spacingM * 2)
height: tooltipText.contentHeight + Theme.spacingS * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainer
border.color: Theme.surfaceVariantAlpha
border.width: 1
visible: batteryArea.containsMouse && !batteryPopupVisible
anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
opacity: batteryArea.containsMouse ? 1 : 0
Column {
anchors.centerIn: parent
spacing: 2
StyledText {
id: tooltipText
text: {
if (!BatteryService.batteryAvailable) {
if (typeof PowerProfiles === "undefined")
return "Power Management";
switch (PowerProfiles.profile) {
case PowerProfile.PowerSaver:
return "Power Profile: Power Saver";
case PowerProfile.Performance:
return "Power Profile: Performance";
default:
return "Power Profile: Balanced";
}
}
let status = BatteryService.batteryStatus;
let level = BatteryService.batteryLevel + "%";
let time = BatteryService.formatTimeRemaining();
if (time !== "Unknown")
return status + " • " + level + " • " + time;
else
return status + " • " + level;
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
horizontalAlignment: Text.AlignHCenter
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
+511
View File
@@ -0,0 +1,511 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Services.UPower
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
PanelWindow {
id: root
property bool batteryPopupVisible: false
function isActiveProfile(profile) {
if (typeof PowerProfiles === "undefined")
return false;
return PowerProfiles.profile === profile;
}
function setProfile(profile) {
if (typeof PowerProfiles === "undefined") {
ToastService.showError("power-profiles-daemon not available");
return ;
}
PowerProfiles.profile = profile;
if (PowerProfiles.profile !== profile)
ToastService.showError("Failed to set power profile");
}
visible: batteryPopupVisible
implicitWidth: 400
implicitHeight: 300
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
MouseArea {
anchors.fill: parent
onClicked: function(mouse) {
var localPos = mapToItem(contentLoader, mouse.x, mouse.y);
if (localPos.x < 0 || localPos.x > contentLoader.width || localPos.y < 0 || localPos.y > contentLoader.height)
batteryPopupVisible = false;
}
}
Loader {
id: contentLoader
readonly property real targetWidth: Math.min(380, Screen.width - Theme.spacingL * 2)
readonly property real targetHeight: Math.min(450, Screen.height - Theme.barHeight - Theme.spacingS * 2)
asynchronous: true
active: batteryPopupVisible
width: targetWidth
height: targetHeight
x: Math.max(Theme.spacingL, Screen.width - targetWidth - Theme.spacingL)
y: Theme.barHeight + Theme.spacingS
opacity: batteryPopupVisible ? 1 : 0
scale: batteryPopupVisible ? 1 : 0.9
Behavior on opacity {
NumberAnimation {
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
Behavior on scale {
NumberAnimation {
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
sourceComponent: Rectangle {
color: Theme.popupBackground()
radius: Theme.cornerRadiusLarge
border.color: Theme.outlineMedium
border.width: 1
antialiasing: true
smooth: true
Rectangle {
anchors.fill: parent
anchors.margins: -3
color: "transparent"
radius: parent.radius + 3
border.color: Qt.rgba(0, 0, 0, 0.05)
border.width: 1
z: -3
}
Rectangle {
anchors.fill: parent
anchors.margins: -2
color: "transparent"
radius: parent.radius + 2
border.color: Theme.shadowMedium
border.width: 1
z: -2
}
Rectangle {
anchors.fill: parent
color: "transparent"
border.color: Theme.outlineStrong
border.width: 1
radius: parent.radius
z: -1
}
ScrollView {
anchors.fill: parent
anchors.margins: Theme.spacingL
clip: true
Column {
width: parent.width
spacing: Theme.spacingL
Row {
width: parent.width
StyledText {
text: BatteryService.batteryAvailable ? "Battery Information" : "Power Management"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - 200
height: 1
}
Rectangle {
width: 32
height: 32
radius: 16
color: closeBatteryArea.containsMouse ? Theme.errorHover : "transparent"
DankIcon {
anchors.centerIn: parent
name: "close"
size: Theme.iconSize - 4
color: closeBatteryArea.containsMouse ? Theme.error : Theme.surfaceText
}
MouseArea {
id: closeBatteryArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
batteryPopupVisible = false;
}
}
}
}
Rectangle {
width: parent.width
height: 80
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.4)
border.color: BatteryService.isCharging ? Theme.primary : (BatteryService.isLowBattery ? Theme.error : Theme.outlineMedium)
border.width: BatteryService.isCharging || BatteryService.isLowBattery ? 2 : 1
visible: BatteryService.batteryAvailable
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingL
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingL
DankIcon {
name: Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable)
size: Theme.iconSizeLarge
color: {
if (BatteryService.isLowBattery && !BatteryService.isCharging)
return Theme.error;
if (BatteryService.isCharging)
return Theme.primary;
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
Row {
spacing: Theme.spacingM
StyledText {
text: BatteryService.batteryLevel + "%"
font.pixelSize: Theme.fontSizeLarge
color: {
if (BatteryService.isLowBattery && !BatteryService.isCharging)
return Theme.error;
if (BatteryService.isCharging)
return Theme.primary;
return Theme.surfaceText;
}
font.weight: Font.Bold
}
StyledText {
text: BatteryService.batteryStatus
font.pixelSize: Theme.fontSizeMedium
color: {
if (BatteryService.isLowBattery && !BatteryService.isCharging)
return Theme.error;
if (BatteryService.isCharging)
return Theme.primary;
return Theme.surfaceText;
}
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: {
let time = BatteryService.formatTimeRemaining();
if (time !== "Unknown")
return BatteryService.isCharging ? "Time until full: " + time : "Time remaining: " + time;
return "";
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
visible: text.length > 0
}
}
}
}
Rectangle {
width: parent.width
height: 80
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.4)
border.color: Theme.outlineMedium
border.width: 1
visible: !BatteryService.batteryAvailable
Row {
anchors.centerIn: parent
spacing: Theme.spacingL
DankIcon {
name: Theme.getBatteryIcon(0, false, false)
size: 36
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "No Battery Detected"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: "Power profile management is available"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium
}
}
}
}
Column {
width: parent.width
spacing: Theme.spacingM
visible: BatteryService.batteryAvailable
StyledText {
text: "Battery Details"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
Row {
width: parent.width
spacing: Theme.spacingXL
Column {
spacing: 2
width: (parent.width - Theme.spacingXL) / 2
StyledText {
text: "Health"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
font.weight: Font.Medium
}
StyledText {
text: BatteryService.batteryHealth
font.pixelSize: Theme.fontSizeMedium
color: {
if (BatteryService.batteryHealth === "N/A")
return Theme.surfaceText;
var healthNum = parseInt(BatteryService.batteryHealth);
return healthNum < 80 ? Theme.error : Theme.surfaceText;
}
}
}
Column {
spacing: 2
width: (parent.width - Theme.spacingXL) / 2
StyledText {
text: "Capacity"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
font.weight: Font.Medium
}
StyledText {
text: BatteryService.batteryCapacity > 0 ? BatteryService.batteryCapacity.toFixed(1) + " Wh" : "Unknown"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
}
}
}
}
Column {
width: parent.width
spacing: Theme.spacingM
visible: true
StyledText {
text: "Power Profile"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
Column {
width: parent.width
spacing: Theme.spacingS
Repeater {
model: (typeof PowerProfiles !== "undefined") ? [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) : [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance]
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadiusLarge
color: profileArea.containsMouse ? Theme.primaryHoverLight : (root.isActiveProfile(modelData) ? Theme.primaryPressed : Theme.surfaceLight)
border.color: root.isActiveProfile(modelData) ? Theme.primary : Theme.outlineLight
border.width: root.isActiveProfile(modelData) ? 2 : 1
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingL
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: Theme.getPowerProfileIcon(modelData)
size: Theme.iconSize
color: root.isActiveProfile(modelData) ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: Theme.getPowerProfileLabel(modelData)
font.pixelSize: Theme.fontSizeMedium
color: root.isActiveProfile(modelData) ? Theme.primary : Theme.surfaceText
font.weight: root.isActiveProfile(modelData) ? Font.Medium : Font.Normal
}
StyledText {
text: Theme.getPowerProfileDescription(modelData)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
}
}
}
MouseArea {
id: profileArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.setProfile(modelData);
}
}
}
}
}
}
Rectangle {
width: parent.width
height: 60
radius: Theme.cornerRadiusLarge
color: Theme.errorHover
border.color: Theme.primarySelected
border.width: 1
visible: (typeof PowerProfiles !== "undefined") && PowerProfiles.degradationReason !== PerformanceDegradationReason.None
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingL
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "warning"
size: Theme.iconSize
color: Theme.error
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "Power Profile Degradation"
font.pixelSize: Theme.fontSizeMedium
color: Theme.error
font.weight: Font.Medium
}
StyledText {
text: (typeof PowerProfiles !== "undefined") ? PerformanceDegradationReason.toString(PowerProfiles.degradationReason) : ""
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.8)
}
}
}
}
}
}
}
}
}
+82
View File
@@ -0,0 +1,82 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Widgets
Rectangle {
id: root
property date currentDate: new Date()
property bool compactMode: false
signal clockClicked()
width: clockRow.implicitWidth + Theme.spacingS * 2
height: 30
radius: Theme.cornerRadius
color: {
const baseColor = clockMouseArea.containsMouse ? Theme.primaryHover : Theme.surfaceTextHover;
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
}
Component.onCompleted: {
root.currentDate = systemClock.date;
}
Row {
id: clockRow
anchors.centerIn: parent
spacing: Theme.spacingS
StyledText {
text: Prefs.use24HourClock ? Qt.formatTime(root.currentDate, "H:mm") : Qt.formatTime(root.currentDate, "h:mm AP")
font.pixelSize: Theme.fontSizeMedium - 1
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "•"
font.pixelSize: Theme.fontSizeSmall
color: Theme.outlineButton
anchors.verticalCenter: parent.verticalCenter
visible: !compactMode
}
StyledText {
text: Qt.formatDate(root.currentDate, "ddd d")
font.pixelSize: Theme.fontSizeMedium - 1
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
visible: !compactMode
}
}
SystemClock {
id: systemClock
precision: SystemClock.Seconds
onDateChanged: root.currentDate = systemClock.date
}
MouseArea {
id: clockMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.clockClicked();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
+133
View File
@@ -0,0 +1,133 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
property bool isActive: false
signal clicked()
function getWiFiSignalIcon(signalStrength) {
switch (signalStrength) {
case "excellent":
return "wifi";
case "good":
return "wifi_2_bar";
case "fair":
return "wifi_1_bar";
case "poor":
return "signal_wifi_0_bar";
default:
return "wifi";
}
}
width: Math.max(80, controlIndicators.implicitWidth + Theme.spacingS * 2)
height: 30
radius: Theme.cornerRadius
color: {
const baseColor = controlCenterArea.containsMouse || root.isActive ? Theme.primaryPressed : Theme.secondaryHover;
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
}
Row {
id: controlIndicators
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: {
if (NetworkService.networkStatus === "ethernet")
return "lan";
else if (NetworkService.networkStatus === "wifi")
return getWiFiSignalIcon(NetworkService.wifiSignalStrength);
else
return "wifi_off";
}
size: Theme.iconSize - 8
color: NetworkService.networkStatus !== "disconnected" ? Theme.primary : Theme.outlineButton
anchors.verticalCenter: parent.verticalCenter
visible: true
}
DankIcon {
name: "bluetooth"
size: Theme.iconSize - 8
color: BluetoothService.enabled ? Theme.primary : Theme.outlineButton
anchors.verticalCenter: parent.verticalCenter
visible: BluetoothService.available && BluetoothService.enabled
}
Rectangle {
width: audioIcon.implicitWidth + 4
height: audioIcon.implicitHeight + 4
color: "transparent"
anchors.verticalCenter: parent.verticalCenter
DankIcon {
id: audioIcon
name: (AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.muted) ? "volume_off" : (AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.volume * 100) < 33 ? "volume_down" : "volume_up"
size: Theme.iconSize - 8
color: audioWheelArea.containsMouse || controlCenterArea.containsMouse || root.isActive ? Theme.primary : 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;
}
wheelEvent.accepted = true;
}
}
}
DankIcon {
name: "mic"
size: Theme.iconSize - 8
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
visible: false // TODO: Add mic detection
}
}
MouseArea {
id: controlCenterArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.clicked();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
+71
View File
@@ -0,0 +1,71 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
property bool showPercentage: true
property bool showIcon: true
property var toggleProcessList
width: 55
height: 30
radius: Theme.cornerRadius
color: {
const baseColor = cpuArea.containsMouse ? Theme.primaryPressed : Theme.secondaryHover;
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
}
Component.onCompleted: {
SysMonitorService.addRef();
}
Component.onDestruction: {
SysMonitorService.removeRef();
}
MouseArea {
id: cpuArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
SysMonitorService.setSortBy("cpu");
if (root.toggleProcessList)
root.toggleProcessList();
}
}
Row {
anchors.centerIn: parent
spacing: 3
DankIcon {
name: "memory"
size: Theme.iconSize - 8
color: {
if (SysMonitorService.cpuUsage > 80)
return Theme.error;
if (SysMonitorService.cpuUsage > 60)
return Theme.warning;
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: (SysMonitorService.cpuUsage || 0).toFixed(0) + "%"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
}
+94
View File
@@ -0,0 +1,94 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
property bool compactMode: false
property int availableWidth: 400
readonly property int baseWidth: contentRow.implicitWidth + Theme.spacingS * 2
readonly property int maxNormalWidth: 456
readonly property int maxCompactWidth: 288
width: compactMode ? Math.min(baseWidth, maxCompactWidth) : Math.min(baseWidth, maxNormalWidth)
height: 30
radius: Theme.cornerRadius
color: {
if (!FocusedWindowService.focusedAppName && !FocusedWindowService.focusedWindowTitle)
return "transparent";
const baseColor = mouseArea.containsMouse ? Theme.primaryHover : Theme.surfaceTextHover;
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
}
clip: true
visible: FocusedWindowService.niriAvailable && (FocusedWindowService.focusedAppName || FocusedWindowService.focusedWindowTitle)
Row {
id: contentRow
anchors.centerIn: parent
spacing: Theme.spacingS
StyledText {
id: appText
text: FocusedWindowService.focusedAppName || ""
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
maximumLineCount: 1
width: Math.min(implicitWidth, compactMode ? 80 : 180)
}
StyledText {
text: "•"
font.pixelSize: Theme.fontSizeSmall
color: Theme.outlineButton
anchors.verticalCenter: parent.verticalCenter
visible: appText.text && titleText.text
}
StyledText {
id: titleText
text: FocusedWindowService.focusedWindowTitle || ""
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
maximumLineCount: 1
width: Math.min(implicitWidth, compactMode ? 180 : 250)
visible: text.length > 0
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
+56
View File
@@ -0,0 +1,56 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
property bool isActive: false
signal clicked()
width: 40
height: 30
radius: Theme.cornerRadius
color: {
const baseColor = launcherArea.containsMouse || isActive ? Theme.surfaceTextPressed : Theme.surfaceTextHover;
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
}
SystemLogo {
visible: Prefs.useOSLogo
anchors.centerIn: parent
width: Theme.iconSize - 3
height: Theme.iconSize - 3
colorOverride: Prefs.osLogoColorOverride
brightnessOverride: Prefs.osLogoBrightness
contrastOverride: Prefs.osLogoContrast
}
DankIcon {
visible: !Prefs.useOSLogo
anchors.centerIn: parent
name: "apps"
size: Theme.iconSize - 6
color: Theme.surfaceText
}
MouseArea {
id: launcherArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.clicked()
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
+252
View File
@@ -0,0 +1,252 @@
import QtQuick
import Quickshell.Services.Mpris
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
readonly property MprisPlayer activePlayer: MprisController.activePlayer
readonly property bool playerAvailable: activePlayer !== null
property bool compactMode: false
readonly property int baseContentWidth: mediaRow.implicitWidth + Theme.spacingS * 2
readonly property int normalContentWidth: Math.min(280, baseContentWidth)
readonly property int compactContentWidth: Math.min(120, baseContentWidth)
signal clicked()
height: 30
radius: Theme.cornerRadius
color: {
const baseColor = Theme.surfaceTextHover;
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
}
states: [
State {
name: "shown"
when: playerAvailable
PropertyChanges {
target: root
opacity: 1
width: compactMode ? compactContentWidth : normalContentWidth
}
},
State {
name: "hidden"
when: !playerAvailable
PropertyChanges {
target: root
opacity: 0
width: 0
}
}
]
transitions: [
Transition {
from: "shown"
to: "hidden"
SequentialAnimation {
PauseAnimation {
duration: 500
}
NumberAnimation {
properties: "opacity,width"
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
},
Transition {
from: "hidden"
to: "shown"
NumberAnimation {
properties: "opacity,width"
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
]
Row {
id: mediaRow
anchors.centerIn: parent
spacing: Theme.spacingXS
Row {
id: mediaInfo
spacing: Theme.spacingXS
AudioVisualization {
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
id: mediaText
anchors.verticalCenter: parent.verticalCenter
width: compactMode ? 60 : 140
visible: !compactMode
text: {
if (!activePlayer || !activePlayer.trackTitle)
return "";
let identity = activePlayer.identity || "";
let isWebMedia = identity.toLowerCase().includes("firefox") || identity.toLowerCase().includes("chrome") || identity.toLowerCase().includes("chromium") || identity.toLowerCase().includes("edge") || identity.toLowerCase().includes("safari");
let title = "";
let subtitle = "";
if (isWebMedia && activePlayer.trackTitle) {
title = activePlayer.trackTitle;
subtitle = activePlayer.trackArtist || identity;
} else {
title = activePlayer.trackTitle || "Unknown Track";
subtitle = activePlayer.trackArtist || "";
}
return subtitle.length > 0 ? title + " • " + subtitle : title;
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
wrapMode: Text.NoWrap
maximumLineCount: 1
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.clicked()
}
}
}
Row {
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
Rectangle {
width: 20
height: 20
radius: 10
anchors.verticalCenter: parent.verticalCenter
color: prevArea.containsMouse ? Theme.primaryHover : "transparent"
visible: root.playerAvailable
opacity: (activePlayer && activePlayer.canGoPrevious) ? 1 : 0.3
DankIcon {
anchors.centerIn: parent
name: "skip_previous"
size: 12
color: Theme.surfaceText
}
MouseArea {
id: prevArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (activePlayer)
activePlayer.previous();
}
}
}
Rectangle {
width: 24
height: 24
radius: 12
anchors.verticalCenter: parent.verticalCenter
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
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (activePlayer)
activePlayer.togglePlaying();
}
}
}
Rectangle {
width: 20
height: 20
radius: 10
anchors.verticalCenter: parent.verticalCenter
color: nextArea.containsMouse ? Theme.primaryHover : "transparent"
visible: playerAvailable
opacity: (activePlayer && activePlayer.canGoNext) ? 1 : 0.3
DankIcon {
anchors.centerIn: parent
name: "skip_next"
size: 12
color: Theme.surfaceText
}
MouseArea {
id: nextArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (activePlayer)
activePlayer.next();
}
}
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
@@ -0,0 +1,59 @@
import QtQuick
import qs.Common
import qs.Widgets
Rectangle {
id: root
property bool hasUnread: false
property bool isActive: false
signal clicked()
width: 40
height: 30
radius: Theme.cornerRadius
color: {
const baseColor = notificationArea.containsMouse || root.isActive ? Theme.primaryPressed : Theme.secondaryHover;
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
}
DankIcon {
anchors.centerIn: parent
name: Prefs.doNotDisturb ? "notifications_off" : "notifications"
size: Theme.iconSize - 6
color: Prefs.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: 6
anchors.topMargin: 6
visible: root.hasUnread
}
MouseArea {
id: notificationArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.clicked();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
+71
View File
@@ -0,0 +1,71 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
property bool showPercentage: true
property bool showIcon: true
property var toggleProcessList
width: 55
height: 30
radius: Theme.cornerRadius
color: {
const baseColor = ramArea.containsMouse ? Theme.primaryPressed : Theme.secondaryHover;
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
}
Component.onCompleted: {
SysMonitorService.addRef();
}
Component.onDestruction: {
SysMonitorService.removeRef();
}
MouseArea {
id: ramArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
SysMonitorService.setSortBy("memory");
if (root.toggleProcessList)
root.toggleProcessList();
}
}
Row {
anchors.centerIn: parent
spacing: 3
DankIcon {
name: "developer_board"
size: Theme.iconSize - 8
color: {
if (SysMonitorService.memoryUsage > 90)
return Theme.error;
if (SysMonitorService.memoryUsage > 75)
return Theme.warning;
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: (SysMonitorService.memoryUsage || 0).toFixed(0) + "%"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
}
+107
View File
@@ -0,0 +1,107 @@
import QtQuick
import Quickshell.Services.SystemTray
import qs.Common
Rectangle {
id: root
readonly property int calculatedWidth: SystemTray.items.values.length > 0 ? SystemTray.items.values.length * 24 + (SystemTray.items.values.length - 1) * Theme.spacingXS + Theme.spacingS * 2 : 0
signal menuRequested(var menu, var item, real x, real y)
width: calculatedWidth
height: 30
radius: Theme.cornerRadius
color: {
if (SystemTray.items.values.length === 0)
return "transparent";
const baseColor = Theme.secondaryHover;
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
}
visible: SystemTray.items.values.length > 0
Row {
id: systemTrayRow
anchors.centerIn: parent
spacing: Theme.spacingXS
Repeater {
model: SystemTray.items.values
delegate: Item {
property var trayItem: modelData
property string iconSource: {
let icon = trayItem && trayItem.icon;
if (typeof icon === 'string' || icon instanceof String) {
if (icon.includes("?path=")) {
const [name, path] = icon.split("?path=");
const fileName = name.substring(name.lastIndexOf("/") + 1);
return `file://${path}/${fileName}`;
}
return icon;
}
return "";
}
width: 24
height: 24
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadiusSmall
color: trayItemArea.containsMouse ? Theme.primaryHover : "transparent"
Behavior on color {
enabled: trayItemArea.containsMouse !== undefined
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Image {
anchors.centerIn: parent
width: 18
height: 18
source: parent.iconSource
asynchronous: true
smooth: true
fillMode: Image.PreserveAspectFit
}
MouseArea {
id: trayItemArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: (mouse) => {
if (!trayItem)
return ;
if (mouse.button === Qt.LeftButton) {
if (!trayItem.onlyMenu)
trayItem.activate();
} else if (mouse.button === Qt.RightButton) {
if (trayItem && trayItem.hasMenu)
root.menuRequested(null, trayItem, mouse.x, mouse.y);
}
}
}
}
}
}
}
+181
View File
@@ -0,0 +1,181 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Widgets
PanelWindow {
id: root
property bool showContextMenu: false
property real contextMenuX: 0
property real contextMenuY: 0
property var currentTrayMenu: null
property var currentTrayItem: null
visible: showContextMenu
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
Rectangle {
id: menuContainer
x: contextMenuX
y: contextMenuY
width: Math.max(180, Math.min(300, menuList.maxTextWidth + Theme.spacingL * 2))
height: Math.max(60, menuList.contentHeight + Theme.spacingS * 2)
color: Theme.popupBackground()
radius: Theme.cornerRadiusLarge
border.color: Theme.outlineMedium
border.width: 1
opacity: showContextMenu ? 1 : 0
scale: showContextMenu ? 1 : 0.85
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
}
Item {
anchors.fill: parent
anchors.margins: Theme.spacingS
QsMenuOpener {
id: menuOpener
menu: currentTrayItem && currentTrayItem.hasMenu ? currentTrayItem.menu : null
}
ListView {
id: menuList
property real maxTextWidth: {
let maxWidth = 0;
if (model && model.values) {
for (let i = 0; i < model.values.length; i++) {
const item = model.values[i];
if (item && item.text) {
const textWidth = textMetrics.advanceWidth * item.text.length * 0.6;
maxWidth = Math.max(maxWidth, textWidth);
}
}
}
return Math.min(maxWidth, 280); // Cap at reasonable width
}
anchors.fill: parent
spacing: 1
model: menuOpener.children
TextMetrics {
id: textMetrics
font.pixelSize: Theme.fontSizeSmall
text: "M"
}
delegate: Rectangle {
width: ListView.view.width
height: modelData.isSeparator ? 5 : 28
radius: modelData.isSeparator ? 0 : Theme.cornerRadiusSmall
color: modelData.isSeparator ? "transparent" : (menuItemArea.containsMouse ? Theme.primaryHover : "transparent")
Rectangle {
visible: modelData.isSeparator
anchors.centerIn: parent
width: parent.width - Theme.spacingS * 2
height: 1
color: Theme.surfaceVariantAlpha
}
Row {
visible: !modelData.isSeparator
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
StyledText {
text: modelData.text || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
elide: Text.ElideRight
maximumLineCount: 1
}
}
MouseArea {
id: menuItemArea
anchors.fill: parent
hoverEnabled: true
cursorShape: modelData.isSeparator ? Qt.ArrowCursor : Qt.PointingHandCursor
enabled: !modelData.isSeparator
onClicked: {
if (modelData.triggered)
modelData.triggered();
showContextMenu = false;
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
MouseArea {
anchors.fill: parent
z: -1
onClicked: {
showContextMenu = false;
}
}
}
+669
View File
@@ -0,0 +1,669 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Io
import Quickshell.Services.Mpris
import Quickshell.Services.Notifications
import Quickshell.Services.SystemTray
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Modules
import qs.Services
import qs.Widgets
PanelWindow {
id: root
property var modelData
property string screenName: modelData.name
property real backgroundTransparency: Prefs.topBarTransparency
readonly property int notificationCount: NotificationService.notifications.length
screen: modelData
implicitHeight: Theme.barHeight - 4
color: "transparent"
Component.onCompleted: {
let fonts = Qt.fontFamilies();
if (fonts.indexOf("Material Symbols Rounded") === -1)
ToastService.showError("Please install Material Symbols Rounded and Restart your Shell. See README.md for instructions");
Prefs.forceTopBarLayoutRefresh.connect(function() {
Qt.callLater(() => {
leftSection.visible = false;
centerSection.visible = false;
rightSection.visible = false;
Qt.callLater(() => {
leftSection.visible = true;
centerSection.visible = true;
rightSection.visible = true;
});
});
});
}
Connections {
function onTopBarTransparencyChanged() {
root.backgroundTransparency = Prefs.topBarTransparency;
}
target: Prefs
}
Connections {
target: root.screen
function onGeometryChanged() {
// Re-layout center widgets when screen geometry changes
if (centerSection && centerSection.width > 0) {
Qt.callLater(centerSection.updateLayout);
}
}
}
QtObject {
id: notificationHistory
property int count: 0
}
anchors {
top: true
left: true
right: true
}
Item {
anchors.fill: parent
anchors.margins: 2
anchors.topMargin: 6
anchors.bottomMargin: 0
anchors.leftMargin: 8
anchors.rightMargin: 8
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadiusXLarge
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, root.backgroundTransparency)
layer.enabled: true
Rectangle {
anchors.fill: parent
color: "transparent"
border.color: Theme.outlineMedium
border.width: 1
radius: parent.radius
}
Rectangle {
anchors.fill: parent
color: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g, Theme.surfaceTint.b, 0.04)
radius: parent.radius
SequentialAnimation on opacity {
running: false
loops: Animation.Infinite
NumberAnimation {
to: 0.08
duration: Theme.extraLongDuration
easing.type: Theme.standardEasing
}
NumberAnimation {
to: 0.02
duration: Theme.extraLongDuration
easing.type: Theme.standardEasing
}
}
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 4
shadowBlur: 0.5 // radius/32, adjusted for visual match
shadowColor: Qt.rgba(0, 0, 0, 0.15)
shadowOpacity: 0.15
}
}
Item {
id: topBarContent
readonly property int availableWidth: width
readonly property int launcherButtonWidth: 40
readonly property int workspaceSwitcherWidth: 120 // Approximate
readonly property int focusedAppMaxWidth: 456 // Fixed width since we don't have focusedApp reference
readonly property int estimatedLeftSectionWidth: launcherButtonWidth + workspaceSwitcherWidth + focusedAppMaxWidth + (Theme.spacingXS * 2)
readonly property int rightSectionWidth: rightSection.width
readonly property int clockWidth: 120 // Approximate clock width
readonly property int mediaMaxWidth: 280 // Normal max width
readonly property int weatherWidth: 80 // Approximate weather width
readonly property bool validLayout: availableWidth > 100 && estimatedLeftSectionWidth > 0 && rightSectionWidth > 0
readonly property int clockLeftEdge: (availableWidth - clockWidth) / 2
readonly property int clockRightEdge: clockLeftEdge + clockWidth
readonly property int leftSectionRightEdge: estimatedLeftSectionWidth
readonly property int mediaLeftEdge: clockLeftEdge - mediaMaxWidth - Theme.spacingS
readonly property int rightSectionLeftEdge: availableWidth - rightSectionWidth
readonly property int leftToClockGap: Math.max(0, clockLeftEdge - leftSectionRightEdge)
readonly property int leftToMediaGap: mediaMaxWidth > 0 ? Math.max(0, mediaLeftEdge - leftSectionRightEdge) : leftToClockGap
readonly property int mediaToClockGap: mediaMaxWidth > 0 ? Theme.spacingS : 0
readonly property int clockToRightGap: validLayout ? Math.max(0, rightSectionLeftEdge - clockRightEdge) : 1000
readonly property bool spacingTight: validLayout && (leftToMediaGap < 150 || clockToRightGap < 100)
readonly property bool overlapping: validLayout && (leftToMediaGap < 100 || clockToRightGap < 50)
function getWidgetEnabled(enabled) {
return enabled !== undefined ? enabled : true;
}
function getWidgetVisible(widgetId) {
switch (widgetId) {
case "launcherButton":
return true;
case "workspaceSwitcher":
return true;
case "focusedWindow":
return true;
case "clock":
return true;
case "music":
return true;
case "weather":
return true;
case "systemTray":
return true;
case "clipboard":
return true;
case "systemResources":
return true;
case "notificationButton":
return true;
case "battery":
return true;
case "controlCenterButton":
return true;
case "spacer":
return true;
case "separator":
return true;
default:
return false;
}
}
function getWidgetComponent(widgetId) {
switch (widgetId) {
case "launcherButton":
return launcherButtonComponent;
case "workspaceSwitcher":
return workspaceSwitcherComponent;
case "focusedWindow":
return focusedWindowComponent;
case "clock":
return clockComponent;
case "music":
return mediaComponent;
case "weather":
return weatherComponent;
case "systemTray":
return systemTrayComponent;
case "clipboard":
return clipboardComponent;
case "systemResources":
return systemResourcesComponent;
case "notificationButton":
return notificationButtonComponent;
case "battery":
return batteryComponent;
case "controlCenterButton":
return controlCenterButtonComponent;
case "spacer":
return spacerComponent;
case "separator":
return separatorComponent;
default:
return null;
}
}
anchors.fill: parent
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.topMargin: Theme.spacingXS
anchors.bottomMargin: Theme.spacingXS
clip: true
Row {
id: leftSection
height: parent.height
spacing: Theme.spacingXS
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
Repeater {
model: Prefs.topBarLeftWidgetsModel
Loader {
property string widgetId: model.widgetId
property var widgetData: model
property int spacerSize: model.size || 20
anchors.verticalCenter: parent ? parent.verticalCenter : undefined
active: topBarContent.getWidgetVisible(model.widgetId)
sourceComponent: topBarContent.getWidgetComponent(model.widgetId)
opacity: topBarContent.getWidgetEnabled(model.enabled) ? 1 : 0
}
}
}
Item {
id: centerSection
property var centerWidgets: []
property int totalWidgets: 0
property real totalWidth: 0
property real spacing: Theme.spacingS
function updateLayout() {
// Defer layout if dimensions are invalid
if (width <= 0 || height <= 0 || !visible) {
Qt.callLater(updateLayout);
return;
}
centerWidgets = [];
totalWidgets = 0;
totalWidth = 0;
for (let i = 0; i < centerRepeater.count; i++) {
let item = centerRepeater.itemAt(i);
if (item && item.active && item.item) {
centerWidgets.push(item.item);
totalWidgets++;
totalWidth += item.item.width;
}
}
if (totalWidgets > 1)
totalWidth += spacing * (totalWidgets - 1);
positionWidgets();
}
function positionWidgets() {
if (totalWidgets === 0 || width <= 0)
return ;
let parentCenterX = width / 2;
if (totalWidgets % 2 === 1) {
let middleIndex = Math.floor(totalWidgets / 2);
let currentX = parentCenterX - (centerWidgets[middleIndex].width / 2);
centerWidgets[middleIndex].x = currentX;
centerWidgets[middleIndex].anchors.horizontalCenter = undefined;
currentX = centerWidgets[middleIndex].x;
for (let i = middleIndex - 1; i >= 0; i--) {
currentX -= (spacing + centerWidgets[i].width);
centerWidgets[i].x = currentX;
centerWidgets[i].anchors.horizontalCenter = undefined;
}
currentX = centerWidgets[middleIndex].x + centerWidgets[middleIndex].width;
for (let i = middleIndex + 1; i < totalWidgets; i++) {
currentX += spacing;
centerWidgets[i].x = currentX;
centerWidgets[i].anchors.horizontalCenter = undefined;
currentX += centerWidgets[i].width;
}
} else {
let leftMiddleIndex = (totalWidgets / 2) - 1;
let rightMiddleIndex = totalWidgets / 2;
let gapCenter = parentCenterX;
let halfSpacing = spacing / 2;
centerWidgets[leftMiddleIndex].x = gapCenter - halfSpacing - centerWidgets[leftMiddleIndex].width;
centerWidgets[leftMiddleIndex].anchors.horizontalCenter = undefined;
centerWidgets[rightMiddleIndex].x = gapCenter + halfSpacing;
centerWidgets[rightMiddleIndex].anchors.horizontalCenter = undefined;
let currentX = centerWidgets[leftMiddleIndex].x;
for (let i = leftMiddleIndex - 1; i >= 0; i--) {
currentX -= (spacing + centerWidgets[i].width);
centerWidgets[i].x = currentX;
centerWidgets[i].anchors.horizontalCenter = undefined;
}
currentX = centerWidgets[rightMiddleIndex].x + centerWidgets[rightMiddleIndex].width;
for (let i = rightMiddleIndex + 1; i < totalWidgets; i++) {
currentX += spacing;
centerWidgets[i].x = currentX;
centerWidgets[i].anchors.horizontalCenter = undefined;
currentX += centerWidgets[i].width;
}
}
}
height: parent.height
width: parent.width
anchors.centerIn: parent
Component.onCompleted: {
Qt.callLater(() => {
Qt.callLater(updateLayout);
});
}
onWidthChanged: {
if (width > 0) {
Qt.callLater(updateLayout);
}
}
onVisibleChanged: {
if (visible && width > 0) {
Qt.callLater(updateLayout);
}
}
Repeater {
id: centerRepeater
model: Prefs.topBarCenterWidgetsModel
Loader {
property string widgetId: model.widgetId
property var widgetData: model
property int spacerSize: model.size || 20
anchors.verticalCenter: parent ? parent.verticalCenter : undefined
active: topBarContent.getWidgetVisible(model.widgetId)
sourceComponent: topBarContent.getWidgetComponent(model.widgetId)
opacity: topBarContent.getWidgetEnabled(model.enabled) ? 1 : 0
onLoaded: {
if (item) {
item.onWidthChanged.connect(centerSection.updateLayout);
if (model.widgetId === "spacer")
item.spacerSize = Qt.binding(() => {
return model.size || 20;
});
Qt.callLater(centerSection.updateLayout);
}
}
onActiveChanged: {
Qt.callLater(centerSection.updateLayout);
}
}
}
Connections {
function onCountChanged() {
Qt.callLater(centerSection.updateLayout);
}
target: Prefs.topBarCenterWidgetsModel
}
}
Row {
id: rightSection
height: parent.height
spacing: Theme.spacingXS
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
Repeater {
model: Prefs.topBarRightWidgetsModel
Loader {
property string widgetId: model.widgetId
property var widgetData: model
property int spacerSize: model.size || 20
anchors.verticalCenter: parent ? parent.verticalCenter : undefined
active: topBarContent.getWidgetVisible(model.widgetId)
sourceComponent: topBarContent.getWidgetComponent(model.widgetId)
opacity: topBarContent.getWidgetEnabled(model.enabled) ? 1 : 0
}
}
}
Component {
id: launcherButtonComponent
LauncherButton {
isActive: appDrawerPopout ? appDrawerPopout.isVisible : false
onClicked: {
if (appDrawerPopout)
appDrawerPopout.toggle();
}
}
}
Component {
id: workspaceSwitcherComponent
WorkspaceSwitcher {
screenName: root.screenName
}
}
Component {
id: focusedWindowComponent
FocusedApp {
compactMode: topBarContent.spacingTight
availableWidth: topBarContent.leftToMediaGap
}
}
Component {
id: clockComponent
Clock {
compactMode: topBarContent.overlapping
onClockClicked: {
centcomPopout.calendarVisible = !centcomPopout.calendarVisible;
}
}
}
Component {
id: mediaComponent
Media {
compactMode: topBarContent.spacingTight || topBarContent.overlapping
onClicked: {
centcomPopout.calendarVisible = !centcomPopout.calendarVisible;
}
}
}
Component {
id: weatherComponent
Weather {
onClicked: {
centcomPopout.calendarVisible = !centcomPopout.calendarVisible;
}
}
}
Component {
id: systemTrayComponent
SystemTrayBar {
onMenuRequested: (menu, item, x, y) => {
systemTrayContextMenu.currentTrayMenu = menu;
systemTrayContextMenu.currentTrayItem = item;
systemTrayContextMenu.contextMenuX = rightSection.x + rightSection.width - 400 - Theme.spacingL;
systemTrayContextMenu.contextMenuY = Theme.barHeight - Theme.spacingXS;
systemTrayContextMenu.showContextMenu = true;
menu.menuVisible = true;
}
}
}
Component {
id: clipboardComponent
Rectangle {
width: 40
height: 30
radius: Theme.cornerRadius
color: {
const baseColor = clipboardArea.containsMouse ? Theme.primaryHover : Theme.secondaryHover;
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
}
DankIcon {
anchors.centerIn: parent
name: "content_paste"
size: Theme.iconSize - 6
color: Theme.surfaceText
}
MouseArea {
id: clipboardArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
clipboardHistoryModalPopup.toggle();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
Component {
id: systemResourcesComponent
Row {
spacing: Theme.spacingXS
CpuMonitor {
toggleProcessList: () => {
return processListPopout.toggle();
}
}
RamMonitor {
toggleProcessList: () => {
return processListPopout.toggle();
}
}
}
}
Component {
id: notificationButtonComponent
NotificationCenterButton {
hasUnread: root.notificationCount > 0
isActive: notificationCenter.notificationHistoryVisible
onClicked: {
notificationCenter.notificationHistoryVisible = !notificationCenter.notificationHistoryVisible;
}
}
}
Component {
id: batteryComponent
Battery {
batteryPopupVisible: batteryPopout.batteryPopupVisible
onToggleBatteryPopup: {
batteryPopout.batteryPopupVisible = !batteryPopout.batteryPopupVisible;
}
}
}
Component {
id: controlCenterButtonComponent
ControlCenterButton {
isActive: controlCenterPopout.controlCenterVisible
onClicked: {
controlCenterPopout.controlCenterVisible = !controlCenterPopout.controlCenterVisible;
if (controlCenterPopout.controlCenterVisible) {
if (NetworkService.wifiEnabled)
NetworkService.scanWifi();
}
}
}
}
Component {
id: spacerComponent
Item {
width: parent.spacerSize || 20
height: 30
Rectangle {
anchors.fill: parent
color: "transparent"
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
border.width: 1
radius: 2
visible: false
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: parent.visible = true
onExited: parent.visible = false
}
}
}
}
Component {
id: separatorComponent
Rectangle {
width: 1
height: 20
color: Theme.outline
opacity: 0.3
}
}
}
}
}
+70
View File
@@ -0,0 +1,70 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
signal clicked()
width: visible ? Math.min(100, weatherRow.implicitWidth + Theme.spacingS * 2) : 0
height: 30
radius: Theme.cornerRadius
color: {
const baseColor = weatherArea.containsMouse ? Theme.primaryHover : Theme.surfaceTextHover;
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
}
Ref {
service: WeatherService
}
Row {
id: weatherRow
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: WeatherService.getWeatherIcon(WeatherService.weather.wCode)
size: Theme.iconSize - 4
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: (Prefs.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp) + "°" + (Prefs.useFahrenheit ? "F" : "C")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: weatherArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.clicked()
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
+160
View File
@@ -0,0 +1,160 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
property string screenName: ""
property int currentWorkspace: getDisplayActiveWorkspace()
property var workspaceList: {
var baseList = getDisplayWorkspaces();
return Prefs.showWorkspacePadding ? padWorkspaces(baseList) : baseList;
}
function padWorkspaces(list) {
var padded = list.slice();
while (padded.length < 3)padded.push(-1) // Use -1 as a placeholder
return padded;
}
function getDisplayWorkspaces() {
if (!NiriService.niriAvailable || NiriService.allWorkspaces.length === 0)
return [1, 2];
if (!root.screenName)
return NiriService.getCurrentOutputWorkspaceNumbers();
var displayWorkspaces = [];
for (var i = 0; i < NiriService.allWorkspaces.length; i++) {
var ws = NiriService.allWorkspaces[i];
if (ws.output === root.screenName)
displayWorkspaces.push(ws.idx + 1);
}
return displayWorkspaces.length > 0 ? displayWorkspaces : [1, 2];
}
function getDisplayActiveWorkspace() {
if (!NiriService.niriAvailable || NiriService.allWorkspaces.length === 0)
return 1;
if (!root.screenName)
return NiriService.getCurrentWorkspaceNumber();
for (var i = 0; i < NiriService.allWorkspaces.length; i++) {
var ws = NiriService.allWorkspaces[i];
if (ws.output === root.screenName && ws.is_active)
return ws.idx + 1;
}
return 1;
}
width: Prefs.showWorkspacePadding ? Math.max(120, workspaceRow.implicitWidth + Theme.spacingL * 2) : workspaceRow.implicitWidth + Theme.spacingL * 2
height: 30
radius: Theme.cornerRadiusLarge
color: {
const baseColor = Theme.surfaceTextHover;
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
}
visible: NiriService.niriAvailable
Connections {
function onAllWorkspacesChanged() {
root.workspaceList = Prefs.showWorkspacePadding ? root.padWorkspaces(root.getDisplayWorkspaces()) : root.getDisplayWorkspaces();
root.currentWorkspace = root.getDisplayActiveWorkspace();
}
function onFocusedWorkspaceIndexChanged() {
root.currentWorkspace = root.getDisplayActiveWorkspace();
}
function onNiriAvailableChanged() {
if (NiriService.niriAvailable) {
root.workspaceList = Prefs.showWorkspacePadding ? root.padWorkspaces(root.getDisplayWorkspaces()) : root.getDisplayWorkspaces();
root.currentWorkspace = root.getDisplayActiveWorkspace();
}
}
target: NiriService
}
Connections {
function onShowWorkspacePaddingChanged() {
var baseList = root.getDisplayWorkspaces();
root.workspaceList = Prefs.showWorkspacePadding ? root.padWorkspaces(baseList) : baseList;
}
target: Prefs
}
Row {
id: workspaceRow
anchors.centerIn: parent
spacing: Theme.spacingS
Repeater {
model: root.workspaceList
Rectangle {
property bool isActive: modelData === root.currentWorkspace
property bool isPlaceholder: modelData === -1
property bool isHovered: mouseArea.containsMouse
property int sequentialNumber: index + 1
width: isActive ? Theme.spacingXL + Theme.spacingM : Theme.spacingL + Theme.spacingXS
height: Theme.spacingL
radius: height / 2
color: isActive ? Theme.primary : isPlaceholder ? Theme.surfaceTextLight : isHovered ? Theme.outlineButton : Theme.surfaceTextAlpha
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: !isPlaceholder
cursorShape: isPlaceholder ? Qt.ArrowCursor : Qt.PointingHandCursor
enabled: !isPlaceholder
onClicked: {
if (!isPlaceholder)
Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", (modelData - 1).toString()]);
}
}
StyledText {
visible: Prefs.showWorkspaceIndex
anchors.centerIn: parent
text: isPlaceholder ? sequentialNumber : sequentialNumber
color: isActive ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
font.pixelSize: Theme.fontSizeSmall
font.weight: isActive && !isPlaceholder ? Font.DemiBold : Font.Normal
}
Behavior on width {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on color {
ColorAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
}
}
+219
View File
@@ -0,0 +1,219 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
PanelWindow {
id: root
property var modelData
property bool volumePopupVisible: false
function show() {
root.volumePopupVisible = true;
hideTimer.restart();
}
function resetHideTimer() {
if (root.volumePopupVisible)
hideTimer.restart();
}
screen: modelData
visible: volumePopupVisible
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
Timer {
id: hideTimer
interval: 3000
repeat: false
onTriggered: {
if (!volumePopup.containsMouse)
root.volumePopupVisible = false;
else
hideTimer.restart();
}
}
Connections {
function onVolumeChanged() {
root.show();
}
function onSinkChanged() {
if (root.volumePopupVisible)
root.show();
}
target: AudioService
}
Rectangle {
id: volumePopup
property bool containsMouse: popupMouseArea.containsMouse
width: Math.min(260, Screen.width - Theme.spacingM * 2)
height: volumeContent.height + Theme.spacingS * 2
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: Theme.spacingM
color: Theme.popupBackground()
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
opacity: root.volumePopupVisible ? 1 : 0
scale: root.volumePopupVisible ? 1 : 0.9
layer.enabled: true
Column {
id: volumeContent
anchors.centerIn: parent
width: parent.width - Theme.spacingS * 2
spacing: Theme.spacingXS
Item {
property int gap: Theme.spacingS
width: parent.width
height: 40
Rectangle {
width: Theme.iconSize
height: Theme.iconSize
radius: Theme.iconSize / 2
color: "transparent"
x: parent.gap
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
name: AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.muted ? "volume_off" : "volume_up"
size: Theme.iconSize
color: muteButton.containsMouse ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: muteButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
AudioService.toggleMute();
root.resetHideTimer();
}
}
}
DankSlider {
id: volumeSlider
width: parent.width - Theme.iconSize - parent.gap * 3
height: 40
x: parent.gap * 2 + Theme.iconSize
anchors.verticalCenter: parent.verticalCenter
minimum: 0
maximum: 100
enabled: AudioService.sink && AudioService.sink.audio
showValue: true
unit: "%"
Component.onCompleted: {
if (AudioService.sink && AudioService.sink.audio)
value = Math.round(AudioService.sink.audio.volume * 100);
}
onSliderValueChanged: function(newValue) {
if (AudioService.sink && AudioService.sink.audio) {
AudioService.sink.audio.volume = newValue / 100;
root.resetHideTimer();
}
}
Connections {
function onVolumeChanged() {
volumeSlider.value = Math.round(AudioService.sink.audio.volume * 100);
}
target: AudioService.sink && AudioService.sink.audio ? AudioService.sink.audio : null
}
}
}
}
MouseArea {
id: popupMouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
propagateComposedEvents: true
z: -1
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 4
shadowBlur: 0.8
shadowColor: Qt.rgba(0, 0, 0, 0.3)
shadowOpacity: 0.3
}
transform: Translate {
y: root.volumePopupVisible ? 0 : 20
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on transform {
PropertyAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
mask: Region {
item: volumePopup
}
}
+137
View File
@@ -0,0 +1,137 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Widgets
LazyLoader {
active: Prefs.wallpaperPath !== ""
Variants {
model: Quickshell.screens
PanelWindow {
id: wallpaperWindow
required property var modelData
screen: modelData
WlrLayershell.layer: WlrLayer.Background
WlrLayershell.exclusionMode: ExclusionMode.Ignore
anchors.top: true
anchors.bottom: true
anchors.left: true
anchors.right: true
color: "black"
Item {
id: root
anchors.fill: parent
property string source: Prefs.wallpaperPath || ""
property Image current: one
onSourceChanged: {
if (!source)
current = null;
else if (current === one)
two.update();
else
one.update();
}
Loader {
anchors.fill: parent
active: !root.source
asynchronous: true
sourceComponent: Rectangle {
color: Theme.surface
Row {
anchors.centerIn: parent
spacing: Theme.spacingL
DankIcon {
name: "sentiment_stressed"
color: Theme.surfaceVariantText
size: Theme.iconSize * 5
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
StyledText {
text: "Wallpaper missing?"
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeXLarge * 2
font.weight: Font.Bold
}
StyledText {
text: "Set wallpaper in Settings"
color: Theme.primary
font.pixelSize: Theme.fontSizeLarge
}
}
}
}
}
Img {
id: one
}
Img {
id: two
}
component Img: Image {
id: img
function update(): void {
source = "";
source = root.source;
}
anchors.fill: parent
fillMode: Image.PreserveAspectCrop
smooth: true
asynchronous: true
cache: true
opacity: 0
onStatusChanged: {
if (status === Image.Ready)
root.current = this;
}
states: State {
name: "visible"
when: root.current === img
PropertyChanges {
img.opacity: 1
}
}
transitions: Transition {
NumberAnimation {
target: img
properties: "opacity"
duration: Theme.mediumDuration
easing.type: Easing.OutCubic
}
}
}
}
}
}
}
+320
View File
@@ -0,0 +1,320 @@
# DankMaterialShell
<div align=center>
![GitHub last commit](https://img.shields.io/github/last-commit/bbedward/DankMaterialShell?style=for-the-badge&labelColor=101418&color=9ccbfb)
![GitHub License](https://img.shields.io/github/license/bbedward/DankMaterialShell?style=for-the-badge&labelColor=101418&color=b9c8da)
![GitHub repo size](https://img.shields.io/github/repo-size/bbedward/DankMaterialShell?style=for-the-badge&labelColor=101418&color=d3bfe6)
</div>
A modern Wayland desktop shell built with [Quickshell](https://quickshell.org/) and designed specifically for the [niri](https://github.com/YaLTeR/niri) compositor. Features Material 3 design principles with a heavy focus on functionality and customizability.
## Screenshots
> WIP
## What's Inside
**Core Widgets:**
- **TopBar**: fully customizable bar where widgets can be added, removed, and re-arranged.
- **App Launcher** with fuzzy search, categories, and auto-sorting by most used apps.
- **Workspace Switcher** Dynamically resizing niri workspace switcher.
- **Focused Window** Displays the currently focused window app name and title.
- **Media Player** Short form media player with equalizer, song title, and controls.
- **Clock** Clock and date widget
- **Weather** Weather widget with customizable location
- **System Tray** System tray applets with context menus.
- **Process Monitor** CPU/Ram usage indicators - with a detailed process list PopUp
- **Power/Battery** Power/Battery widget for battery metrics and power profile changing.
- **Notifications** Notification bell with a notification center popup
- **Control Center** High-level view of network, bluetooth, and audio status
- **Spotlight Launcher** A central app launcher/search that can be triggered via an IPC keybinding.
- **Central Command** A combined music, weather, calendar, and events PopUp.
- **Process List** A process list, with system metrics and information. More detailed modal available via IPC.
- **Notification Center** A center for notifications that has support for grouping.
- **Dock** A dock with pinned apps support, recent apps support, and currently running application support.
- **Control Center** A full control center with user profile information, network, bluetooth, audio input/output, and display controls.
- **Lock Screen** Using quickshell's WlSessionLock
**Features:**
- Dynamic wallpaper-based theming with matugen integration
- Numerous IPCs to trigger actions and open various modals.
- Calendar integration with [khal](https://github.com/pimutils/khal)
- Audio/media controls
- Grouped notifications
- Brightness control for internal and external displays
- Qt and GTK app theming synchronization, as well as [Ghostty](https://ghostty.org/) auto-theme support.
## Installation
### Quick Start
**Dependencies:**
```bash
# Arch Linux
paru -S quickshell-git ttf-material-symbols-variable-git inter-font ttf-fira-code
# Fedora
sudo dnf copr enable errornointernet/quickshell && sudo dnf install quickshell-git
# Install fonts manually (see instructions below)
```
**Get the shell:**
```bash
# Arch linux available via AUR
paru -S dankmaterialshell-git
# Manual install
mkdir -p ~/.config/quickshell
git clone https://github.com/bbedward/DankMaterialShell.git ~/.config/quickshell/DankMaterialShell
qs -c DankMaterialShell
```
### Detailed Setup
<details><summary>Font Installation</summary>
**Material Symbols (Required):**
```bash
# Manual installation
mkdir -p ~/.local/share/fonts
curl -L "https://github.com/google/material-design-icons/raw/master/variablefont/MaterialSymbolsRounded%5BFILL%2CGRAD%2Copsz%2Cwght%5D.ttf" -o ~/.local/share/fonts/MaterialSymbolsRounded.ttf
fc-cache -f
# Arch Linux
paru -S ttf-material-symbols-variable-git
```
**Typography (Recommended):**
```bash
# Inter Variable Font
curl -L "https://github.com/rsms/inter/releases/download/v4.0/Inter-4.0.zip" -o /tmp/Inter.zip
unzip -j /tmp/Inter.zip "InterVariable.ttf" "InterVariable-Italic.ttf" -d ~/.local/share/fonts/
rm /tmp/Inter.zip && fc-cache -f
# Fira Code
curl -L "https://github.com/tonsky/FiraCode/releases/download/6.2/Fira_Code_v6.2.zip" -o /tmp/FiraCode.zip
unzip -j /tmp/FiraCode.zip "ttf/*.ttf" -d ~/.local/share/fonts/
rm /tmp/FiraCode.zip && fc-cache -f
```
</details>
<details><summary>Optional Features</summary>
**Enhanced Functionality:**
```bash
# Arch Linux
pacman -S cava wl-clipboard cliphist ddcutil brightnessctl qt5ct qt6ct
paru -S matugen
# Fedora
sudo dnf install cava wl-clipboard ddcutil brightnessctl qt5ct qt6ct
sudo dnf copr enable wef/cliphist && sudo dnf install cliphist
sudo dnf copr enable heus-sueh/packages && sudo dnf install matugen
```
**What you get:**
- `matugen`: Wallpaper-based dynamic theming
- `ddcutil`: External monitor brightness control
- `brightnessctl`: Laptop display brightness
- `wl-clipboard`: Required for copying various elements to clipboard.
- `qt5ct/qt6ct`: Qt application theming
- `cava`: Audio visualizer
- `cliphist`: Clipboard history
</details>
## Usage
### Niri Integration
Add to your niri config
```bash
spawn-at-startup "qs" "-c" "DankMaterialShell"
// Dank keybinds
binds {
Mod+Space hotkey-overlay-title="Application Launcher" {
spawn "qs" "-c" "DankMaterialShell" "ipc" "call" "spotlight" "toggle";
}
Mod+V hotkey-overlay-title="Clipboard Manager" {
spawn "qs" "-c" "DankMaterialShell" "ipc" "call" "clipboard" "toggle";
}
Mod+M hotkey-overlay-title="Task Manager" {
spawn "qs" "-c" "DankMaterialShell" "ipc" "call" "processlist" "toggle";
}
Mod+Comma hotkey-overlay-title="Settings" {
spawn "qs" "-c" "DankMaterialShell" "ipc" "call" "settings" "toggle";
}
Super+Alt+L hotkey-overlay-title="Lock Screen" {
spawn "qs" "-c" "DankMaterialShell" "ipc" "call" "lock" "lock";
}
XF86AudioRaiseVolume allow-when-locked=true {
spawn "qs" "-c" "DankMaterialShell" "ipc" "call" "audio" "increment" "3";
}
XF86AudioLowerVolume allow-when-locked=true {
spawn "qs" "-c" "DankMaterialShell" "ipc" "call" "audio" "decrement" "3";
}
XF86AudioMute allow-when-locked=true {
spawn "qs" "-c" "DankMaterialShell" "ipc" "call" "audio" "mute";
}
XF86AudioMicMute allow-when-locked=true {
spawn "qs" "-c" "DankMaterialShell" "ipc" "call" "audio" "micmute";
}
}
```
### IPC Commands
Control everything from the command line, or via keybinds:
```bash
# Audio control
qs -c DankMaterialShell ipc call audio setvolume 50
qs -c DankMaterialShell ipc call audio mute
# Launch applications
qs -c DankMaterialShell ipc call spotlight toggle
qs -c DankMaterialShell ipc call processlist toggle
# System control
qs -c DankMaterialShell ipc call wallpaper set /path/to/image.jpg
qs -c DankMaterialShell ipc call theme toggle
qs -c DankMaterialShell ipc call lock lock
# Media control
qs -c DankMaterialShell ipc call mpris playPause
qs -c DankMaterialShell ipc call mpris next
```
## Theming
### System App Integration
**GTK Apps:**
Install [Colloid](https://github.com/vinceliuice/Colloid-gtk-theme) or similar Material theme:
```bash
./install.sh -s standard -l --tweaks normal
```
Configure in `~/.config/gtk-3.0/settings.ini`:
```ini
[Settings]
gtk-theme-name=Colloid
```
**Qt Apps:**
```bash
# Install Breeze
pacman -S breeze breeze5 # Arch
sudo dnf install breeze # Fedora
# Configure qt5ct/qt6ct
echo 'style=Breeze' >> ~/.config/qt5ct/qt5ct.conf
```
**Dynamic Theming:**
Enable wallpaper-based theming in **Settings → Appearance → System App Theming** after installing matugen.
### Terminal Integration
**Ghostty users** can add automatic color theming:
```bash
echo "config-file = ./config-dankcolors" >> ~/.config/ghostty/config
```
## Calendar Setup
Sync your Google Calendar for dashboard integration:
<details><summary>Configuration Steps</summary>
**Install dependencies:**
```bash
# Arch
pacman -S vdirsyncer khal python-aiohttp-oauthlib
# Fedora
sudo dnf install python3-vdirsyncer khal python3-aiohttp-oauthlib
```
**Configure vdirsyncer** (`~/.vdirsyncer/config`):
```ini
[general]
status_path = "~/.calendars/status"
[pair personal_sync]
a = "personal"
b = "personallocal"
collections = ["from a", "from b"]
conflict_resolution = "a wins"
metadata = ["color"]
[storage personal]
type = "google_calendar"
token_file = "~/.vdirsyncer/google_calendar_token"
client_id = "your_client_id"
client_secret = "your_client_secret"
[storage personallocal]
type = "filesystem"
path = "~/.calendars/Personal"
fileext = ".ics"
```
**Setup sync:**
```bash
vdirsyncer sync
khal configure
# Auto-sync every 5 minutes
crontab -e
# Add: */5 * * * * /usr/bin/vdirsyncer sync
```
</details>
## Configuration
All settings are configurable in `~/.config/DankMaterialShell/settings.json`, or more intuitively the built-in settings modal.
**Key configuration areas:**
- Widget positioning and behavior
- Theme and color preferences
- Time format, weather units and location
- Light/Dark modes
- Wallpaper and Profile picture
- Dock enable/disable and various tunes.
## Troubleshooting
**Common issues:**
- **Missing icons:** Verify Material Symbols font installation with `fc-list | grep Material`
- **No dynamic theming:** Install matugen and enable in settings
- **Qt apps not themed:** Configure qt5ct/qt6ct and set QT_QPA_PLATFORMTHEME
- **Calendar not syncing:** Check vdirsyncer credentials and network connectivity
**Getting help:**
- Check the [issues](https://github.com/bbedward/DankMaterialShell/issues) for known problems
- Share logs from `qs -c DankMaterialShell` for debugging
- Join the niri community for compositor-specific questions
## Contributing
DankMaterialShell welcomes contributions! Whether it's bug fixes, new widgets, theme improvements, or documentation updates - all help is appreciated.
**Areas that need attention:**
- More widget options and customization
- Additional compositor compatibility
- Performance optimizations
- Documentation and examples
## Credits
- [quickshell](https://quickshell.org/) the core of what makes a shell like this possible.
- [niri](https://github.com/YaLTeR/niri) for the awesome scrolling compositor.
- [soramanew](https://github.com/soramanew) who built [caelestia](https://github.com/caelestia-dots/shell) which served as inspiration and guidance for many dank widgets.
- [end-4](https://github.com/end-4) for [dots-hyprland](https://github.com/end-4/dots-hyprland) which also served as inspiration and guidance for many dank widgets.
+154
View File
@@ -0,0 +1,154 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import "../Common/fuzzysort.js" as Fuzzy
Singleton {
id: root
property var applications: DesktopEntries.applications.values
property var preppedApps: applications.map(app => ({
name: Fuzzy.prepare(app.name || ""),
comment: Fuzzy.prepare(app.comment || ""),
entry: app
}))
function searchApplications(query) {
if (!query || query.length === 0) {
return applications
}
if (preppedApps.length === 0) {
return []
}
var results = Fuzzy.go(query, preppedApps, {
all: false,
keys: ["name", "comment"],
scoreFn: r => {
var nameScore = r[0] ? r[0].score : 0
var commentScore = r[1] ? r[1].score : 0
var appName = r.obj.entry.name || ""
var finalScore = 0
if (nameScore > 0) {
var queryLower = query.toLowerCase()
var nameLower = appName.toLowerCase()
if (nameLower === queryLower) {
finalScore = nameScore * 100
}
else if (nameLower.startsWith(queryLower)) {
finalScore = nameScore * 50
}
else if (nameLower.includes(" " + queryLower) || nameLower.includes(queryLower + " ") || nameLower.endsWith(" " + queryLower)) {
finalScore = nameScore * 25
}
else if (nameLower.includes(queryLower)) {
finalScore = nameScore * 10
}
else {
finalScore = nameScore * 2 + commentScore * 0.1
}
} else {
finalScore = commentScore * 0.1
}
return finalScore
},
limit: 50
})
return results.map(r => r.obj.entry)
}
function getCategoriesForApp(app) {
if (!app || !app.categories) return []
var categoryMap = {
"AudioVideo": "Media",
"Audio": "Media",
"Video": "Media",
"Development": "Development",
"TextEditor": "Development",
"IDE": "Development",
"Education": "Education",
"Game": "Games",
"Graphics": "Graphics",
"Photography": "Graphics",
"Network": "Internet",
"WebBrowser": "Internet",
"Email": "Internet",
"Office": "Office",
"WordProcessor": "Office",
"Spreadsheet": "Office",
"Presentation": "Office",
"Science": "Science",
"Settings": "Settings",
"System": "System",
"Utility": "Utilities",
"Accessories": "Utilities",
"FileManager": "Utilities",
"TerminalEmulator": "Utilities"
}
var mappedCategories = new Set()
for (var i = 0; i < app.categories.length; i++) {
var cat = app.categories[i]
if (categoryMap[cat]) {
mappedCategories.add(categoryMap[cat])
}
}
return Array.from(mappedCategories)
}
// Category icon mappings
property var categoryIcons: ({
"All": "apps",
"Media": "music_video",
"Development": "code",
"Games": "sports_esports",
"Graphics": "photo_library",
"Internet": "web",
"Office": "content_paste",
"Settings": "settings",
"System": "host",
"Utilities": "build"
})
function getCategoryIcon(category) {
return categoryIcons[category] || "folder"
}
function getAllCategories() {
var categories = new Set(["All"])
for (var i = 0; i < applications.length; i++) {
var appCategories = getCategoriesForApp(applications[i])
appCategories.forEach(cat => categories.add(cat))
}
return Array.from(categories).sort()
}
function getAppsInCategory(category) {
if (category === "All") {
return applications
}
return applications.filter(app => {
var appCategories = getCategoriesForApp(app)
return appCategories.includes(category)
})
}
}

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