1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-29 07:52:50 -05:00

Compare commits

...

173 Commits

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

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

* Multi batteries support DMS_PREFERRED_BATTERY fix

---------

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

* removed console log

* minor: tabs
2025-10-13 14:25:25 -04:00
Bruno Cesar Rocha
7317024da5 feat: Launcher Plugin Component (#408)
Load launcher items from plugin.
2025-10-13 14:24:41 -04:00
Body
9b9fbabc3f tweak popout margins for consistency (#399) 2025-10-13 14:17:02 -04:00
bbedward
3c5a23799f Flip light/dark mode and icon themes properly 2025-10-13 14:16:36 -04:00
bbedward
3bfdc6163c Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-10-13 13:54:54 -04:00
purian23
cf2f74a38d Stock workflow release 2025-10-12 15:23:00 -04:00
bbedward
2a7f52c67e Update readme 2025-10-12 07:46:43 -04:00
Body
50fde1e308 Add niri overview toggle on launcher button rightclick. (#394) 2025-10-12 07:38:55 -04:00
max72bra
46fd0ae413 Hyprland: filter all spacial workspaces (#398)
Filters all workspaces with IDs less than 0, regardless of their name (which is user-defined)
2025-10-12 07:38:39 -04:00
bokicoder
65e32dc429 Fix keyboard focus issue in clipboard (#391) 2025-10-12 07:38:15 -04:00
Body
413675dfc1 Close ControlCenter popout on Settings open. (#396) 2025-10-12 07:34:06 -04:00
purian23
a17343f40e Enable Copr stable builds 2025-10-12 01:08:10 -04:00
bokicoder
5d023804c1 update flake.lock (#390) 2025-10-12 00:12:41 -04:00
bbedward
fa07a846b9 Merge branch 'master' of github.com:bbedward/DankMaterialShell 2025-10-11 23:48:36 -04:00
bbedward
5df46b605e Call lockerReady to releasse inhibitor whenlock screen is drawn 2025-10-11 23:48:13 -04:00
bokicoder
77cf371a21 update flake.lock (#388) 2025-10-11 23:42:55 -04:00
bbedward
89802dd040 Presever user configs 2025-10-11 23:08:11 -04:00
bbedward
59b95e9dd6 Bundle distro package with dms releases 2025-10-11 22:21:55 -04:00
bbedward
b836db5252 Update copr-related build opts 2025-10-11 21:59:08 -04:00
purian23
4dc4b15925 Revert "Update spec to SRPM by default"
This reverts commit 5c3062e699.
2025-10-11 18:19:21 -04:00
purian23
5c3062e699 Update spec to SRPM by default 2025-10-11 17:52:26 -04:00
162 changed files with 17844 additions and 7204 deletions

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

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

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

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

View File

@@ -29,22 +29,18 @@ jobs:
git config user.name "github-actions[bot]" git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com" git config user.email "github-actions[bot]@users.noreply.github.com"
# create/update VERSION file to match incoming tag
echo "${TAG}" > VERSION echo "${TAG}" > VERSION
if ! git diff --quiet -- VERSION; then
git add VERSION git add -A VERSION
git commit -m "Add VERSION file for ${TAG} (from DMS)"
if ! git diff --cached --quiet; then
git commit -m "Update VERSION to ${TAG} (from DMS)"
fi fi
# If tag doesn't exist (or differs), (re)create it git tag -f "${TAG}"
if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
echo "Tag ${TAG} already exists"
else
git tag "${TAG}"
fi
# Push commit (if any) and tag git push origin HEAD
git push --follow-tags origin HEAD git push -f origin "${TAG}"
- name: Generate Changelog - name: Generate Changelog
id: changelog id: changelog
@@ -61,12 +57,14 @@ jobs:
## Assets ## Assets
### Complete Packages ### Complete Packages
- **`dms-full-amd64.tar.gz`** - Complete package for x86_64 systems (CLI binary + QML source + installation guide) - **`dms-full-amd64.tar.gz`** - Complete package for x86_64 systems (CLI binaries + QML source + installation guide)
- **`dms-full-arm64.tar.gz`** - Complete package for ARM64 systems (CLI binary + QML source + installation guide) - **`dms-full-arm64.tar.gz`** - Complete package for ARM64 systems (CLI binaries + QML source + installation guide)
### Individual Components ### Individual Components
- **`dms-cli-amd64.gz`** - DMS CLI binary for x86_64 systems - **`dms-cli-amd64.gz`** - DMS CLI binary for x86_64 systems
- **`dms-cli-arm64.gz`** - DMS CLI binary for ARM64 systems - **`dms-cli-arm64.gz`** - DMS CLI binary for ARM64 systems
- **`dms-distropkg-amd64.gz`** - DMS CLI binary built with distro_package tag for AMD64 systems
- **`dms-distropkg-arm64.gz`** - DMS CLI binary built with distro_package tag for ARM64 systems
- **`dms-qml.tar.gz`** - QML source code only - **`dms-qml.tar.gz`** - QML source code only
### Checksums ### Checksums
@@ -112,14 +110,16 @@ jobs:
# Download DMS CLI binaries from the danklinux repo # Download DMS CLI binaries from the danklinux repo
gh release download "${TAG}" -R "${DMS_REPO}" --dir ./_dms_assets gh release download "${TAG}" -R "${DMS_REPO}" --dir ./_dms_assets
# Rename CLI binaries to dms-cli-* format # Rename CLI binaries to dms-cli-* format and copy distropkg binaries
for file in _dms_assets/dms-*.gz*; do for file in _dms_assets/dms-*.gz*; do
if [ -f "$file" ]; then if [ -f "$file" ]; then
basename=$(basename "$file") basename=$(basename "$file")
# dms-amd64.gz -> dms-cli-amd64.gz if [[ "$basename" == dms-distropkg-* ]]; then
# dms-amd64.gz.sha256 -> dms-cli-amd64.gz.sha256 cp "$file" "_release_assets/$basename"
newname=$(echo "$basename" | sed 's/^dms-/dms-cli-/') else
cp "$file" "_release_assets/$newname" newname=$(echo "$basename" | sed 's/^dms-/dms-cli-/')
cp "$file" "_release_assets/$newname"
fi
fi fi
done done
@@ -148,6 +148,12 @@ jobs:
chmod +x _temp_full/bin/dms chmod +x _temp_full/bin/dms
fi fi
# Copy distropkg binary if it exists
if [ -f "_dms_assets/dms-distropkg-${arch}.gz" ]; then
gunzip -c "_dms_assets/dms-distropkg-${arch}.gz" > _temp_full/bin/dms-distropkg
chmod +x _temp_full/bin/dms-distropkg
fi
# Create INSTALL.md # Create INSTALL.md
cat > _temp_full/INSTALL.md << 'EOF' cat > _temp_full/INSTALL.md << 'EOF'
# DankMaterialShell Installation # DankMaterialShell Installation
@@ -166,7 +172,7 @@ jobs:
cp -r dms ~/.config/quickshell/ cp -r dms ~/.config/quickshell/
``` ```
2. **Install the DMS CLI binary:** 2. **Install the DMS CLI binaries:**
```bash ```bash
sudo install -m 755 bin/dms /usr/local/bin/dms sudo install -m 755 bin/dms /usr/local/bin/dms
# or install to a local directory: # or install to a local directory:

122
Common/CacheData.qml Normal file
View File

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

70
Common/Proc.qml Normal file
View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -12,6 +12,7 @@ PanelWindow {
property alias content: contentLoader.sourceComponent property alias content: contentLoader.sourceComponent
property alias contentLoader: contentLoader property alias contentLoader: contentLoader
property Item directContent: null
property real width: 400 property real width: 400
property real height: 300 property real height: 300
readonly property real screenWidth: screen ? screen.width : 1920 readonly property real screenWidth: screen ? screen.width : 1920
@@ -200,14 +201,44 @@ PanelWindow {
focus: root.shouldBeVisible focus: root.shouldBeVisible
clip: false clip: false
Item {
id: directContentWrapper
anchors.fill: parent
visible: root.directContent !== null
focus: true
clip: false
Component.onCompleted: {
if (root.directContent) {
root.directContent.parent = directContentWrapper
root.directContent.anchors.fill = directContentWrapper
Qt.callLater(() => root.directContent.forceActiveFocus())
}
}
Connections {
function onDirectContentChanged() {
if (root.directContent) {
root.directContent.parent = directContentWrapper
root.directContent.anchors.fill = directContentWrapper
Qt.callLater(() => root.directContent.forceActiveFocus())
}
}
target: root
}
}
Loader { Loader {
id: contentLoader id: contentLoader
anchors.fill: parent anchors.fill: parent
active: root.keepContentLoaded || root.shouldBeVisible || root.visible active: root.directContent === null && (root.keepContentLoaded || root.shouldBeVisible || root.visible)
asynchronous: false asynchronous: false
focus: true focus: true
clip: false clip: false
visible: root.directContent === null
onLoaded: { onLoaded: {
if (item) { if (item) {

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -33,8 +33,8 @@ Rectangle {
"text": I18n.tr("Theme & Colors"), "text": I18n.tr("Theme & Colors"),
"icon": "palette" "icon": "palette"
}, { }, {
"text": I18n.tr("Idle & Lock Screen"), "text": I18n.tr("Power & Security"),
"icon": "lock" "icon": "power"
}, { }, {
"text": I18n.tr("Plugins"), "text": I18n.tr("Plugins"),
"icon": "extension" "icon": "extension"

View File

@@ -12,6 +12,7 @@ Popup {
property var currentApp: null property var currentApp: null
property var appLauncher: null property var appLauncher: null
property var parentHandler: null property var parentHandler: null
readonly property var desktopEntry: (currentApp && !currentApp.isPlugin && appLauncher && appLauncher._uniqueApps && currentApp.appIndex >= 0 && currentApp.appIndex < appLauncher._uniqueApps.length) ? appLauncher._uniqueApps[currentApp.appIndex] : null
function show(x, y, app) { function show(x, y, app) {
currentApp = app currentApp = app
@@ -77,8 +78,7 @@ Popup {
spacing: 1 spacing: 1
Rectangle { Rectangle {
implicitWidth: pinRow.implicitWidth + Theme.spacingS * 2 width: parent.width
width: implicitWidth
height: 32 height: 32
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: pinMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" color: pinMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
@@ -92,10 +92,10 @@ Popup {
DankIcon { DankIcon {
name: { name: {
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry) if (!desktopEntry)
return "push_pin" return "push_pin"
const appId = contextMenu.currentApp.desktopEntry.id || contextMenu.currentApp.desktopEntry.execString || "" const appId = desktopEntry.id || desktopEntry.execString || ""
return SessionData.isPinnedApp(appId) ? "keep_off" : "push_pin" return SessionData.isPinnedApp(appId) ? "keep_off" : "push_pin"
} }
size: Theme.iconSize - 2 size: Theme.iconSize - 2
@@ -106,10 +106,10 @@ Popup {
StyledText { StyledText {
text: { text: {
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry) if (!desktopEntry)
return I18n.tr("Pin to Dock") return I18n.tr("Pin to Dock")
const appId = contextMenu.currentApp.desktopEntry.id || contextMenu.currentApp.desktopEntry.execString || "" const appId = desktopEntry.id || desktopEntry.execString || ""
return SessionData.isPinnedApp(appId) ? I18n.tr("Unpin from Dock") : I18n.tr("Pin to Dock") return SessionData.isPinnedApp(appId) ? I18n.tr("Unpin from Dock") : I18n.tr("Pin to Dock")
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
@@ -126,10 +126,10 @@ Popup {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: () => { onClicked: () => {
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry) if (!desktopEntry)
return return
const appId = contextMenu.currentApp.desktopEntry.id || contextMenu.currentApp.desktopEntry.execString || "" const appId = desktopEntry.id || desktopEntry.execString || ""
if (SessionData.isPinnedApp(appId)) if (SessionData.isPinnedApp(appId))
SessionData.removePinnedApp(appId) SessionData.removePinnedApp(appId)
else else
@@ -154,11 +154,10 @@ Popup {
} }
Repeater { Repeater {
model: contextMenu.currentApp && contextMenu.currentApp.desktopEntry && contextMenu.currentApp.desktopEntry.actions ? contextMenu.currentApp.desktopEntry.actions : [] model: desktopEntry && desktopEntry.actions ? desktopEntry.actions : []
Rectangle { Rectangle {
implicitWidth: actionRow.implicitWidth + Theme.spacingS * 2 width: parent.width
width: implicitWidth
height: 32 height: 32
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: actionMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" color: actionMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
@@ -200,9 +199,9 @@ Popup {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
if (modelData && contextMenu.currentApp && contextMenu.currentApp.desktopEntry) { if (modelData && desktopEntry) {
SessionService.launchDesktopAction(contextMenu.currentApp.desktopEntry, modelData) SessionService.launchDesktopAction(desktopEntry, modelData)
if (appLauncher) { if (appLauncher && contextMenu.currentApp) {
appLauncher.appLaunched(contextMenu.currentApp) appLauncher.appLaunched(contextMenu.currentApp)
} }
} }
@@ -213,7 +212,7 @@ Popup {
} }
Rectangle { Rectangle {
visible: contextMenu.currentApp && contextMenu.currentApp.desktopEntry && contextMenu.currentApp.desktopEntry.actions && contextMenu.currentApp.desktopEntry.actions.length > 0 visible: desktopEntry && desktopEntry.actions && desktopEntry.actions.length > 0
width: parent.width - Theme.spacingS * 2 width: parent.width - Theme.spacingS * 2
height: 5 height: 5
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
@@ -228,8 +227,7 @@ Popup {
} }
Rectangle { Rectangle {
implicitWidth: launchRow.implicitWidth + Theme.spacingS * 2 width: parent.width
width: implicitWidth
height: 32 height: 32
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: launchMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" color: launchMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
@@ -290,8 +288,7 @@ Popup {
Rectangle { Rectangle {
visible: SessionService.hasPrimeRun visible: SessionService.hasPrimeRun
implicitWidth: primeRunRow.implicitWidth + Theme.spacingS * 2 width: parent.width
width: implicitWidth
height: 32 height: 32
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: primeRunMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" color: primeRunMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
@@ -327,9 +324,9 @@ Popup {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: () => { onClicked: () => {
if (contextMenu.currentApp && contextMenu.currentApp.desktopEntry) { if (desktopEntry) {
SessionService.launchDesktopEntry(contextMenu.currentApp.desktopEntry, true) SessionService.launchDesktopEntry(desktopEntry, true)
if (appLauncher) { if (appLauncher && contextMenu.currentApp) {
appLauncher.appLaunched(contextMenu.currentApp) appLauncher.appLaunched(contextMenu.currentApp)
} }
} }

View File

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

View File

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

View File

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

View File

@@ -449,6 +449,8 @@ DankPopout {
color: Theme.surfaceText color: Theme.surfaceText
font.weight: Font.Medium font.weight: Font.Medium
elide: Text.ElideRight elide: Text.ElideRight
wrapMode: Text.NoWrap
maximumLineCount: 1
} }
StyledText { StyledText {
@@ -620,8 +622,8 @@ DankPopout {
font.weight: Font.Medium font.weight: Font.Medium
elide: Text.ElideRight elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
maximumLineCount: 2 maximumLineCount: 1
wrapMode: Text.WordWrap wrapMode: Text.NoWrap
} }
} }
@@ -665,8 +667,8 @@ DankPopout {
id: contextMenu id: contextMenu
property var currentApp: null property var currentApp: null
readonly property var desktopEntry: (currentApp && !currentApp.isPlugin && appLauncher && appLauncher._uniqueApps && currentApp.appIndex >= 0 && currentApp.appIndex < appLauncher._uniqueApps.length) ? appLauncher._uniqueApps[currentApp.appIndex] : null
readonly property string appId: (currentApp && currentApp.desktopEntry) ? (currentApp.desktopEntry.id || currentApp.desktopEntry.execString || "") : "" readonly property string appId: desktopEntry ? (desktopEntry.id || desktopEntry.execString || "") : ""
readonly property bool isPinned: appId && SessionData.isPinnedApp(appId) readonly property bool isPinned: appId && SessionData.isPinnedApp(appId)
function show(x, y, app) { function show(x, y, app) {
@@ -768,7 +770,7 @@ DankPopout {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry) { if (!contextMenu.desktopEntry) {
return return
} }
@@ -797,7 +799,7 @@ DankPopout {
} }
Repeater { Repeater {
model: contextMenu.currentApp && contextMenu.currentApp.desktopEntry && contextMenu.currentApp.desktopEntry.actions ? contextMenu.currentApp.desktopEntry.actions : [] model: contextMenu.desktopEntry && contextMenu.desktopEntry.actions ? contextMenu.desktopEntry.actions : []
Rectangle { Rectangle {
width: Math.max(parent.width, actionRow.implicitWidth + Theme.spacingS * 2) width: Math.max(parent.width, actionRow.implicitWidth + Theme.spacingS * 2)
@@ -842,9 +844,11 @@ DankPopout {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
if (modelData && contextMenu.currentApp && contextMenu.currentApp.desktopEntry) { if (modelData && contextMenu.desktopEntry) {
SessionService.launchDesktopAction(contextMenu.currentApp.desktopEntry, modelData) SessionService.launchDesktopAction(contextMenu.desktopEntry, modelData)
appLauncher.appLaunched(contextMenu.currentApp) if (contextMenu.currentApp) {
appLauncher.appLaunched(contextMenu.currentApp)
}
} }
contextMenu.hide() contextMenu.hide()
} }
@@ -853,7 +857,7 @@ DankPopout {
} }
Rectangle { Rectangle {
visible: contextMenu.currentApp && contextMenu.currentApp.desktopEntry && contextMenu.currentApp.desktopEntry.actions && contextMenu.currentApp.desktopEntry.actions.length > 0 visible: contextMenu.desktopEntry && contextMenu.desktopEntry.actions && contextMenu.desktopEntry.actions.length > 0
width: parent.width - Theme.spacingS * 2 width: parent.width - Theme.spacingS * 2
height: 5 height: 5
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
@@ -963,9 +967,11 @@ DankPopout {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
if (contextMenu.currentApp && contextMenu.currentApp.desktopEntry) { if (contextMenu.desktopEntry) {
SessionService.launchDesktopEntry(contextMenu.currentApp.desktopEntry, true) SessionService.launchDesktopEntry(contextMenu.desktopEntry, true)
appLauncher.appLaunched(contextMenu.currentApp) if (contextMenu.currentApp) {
appLauncher.appLaunched(contextMenu.currentApp)
}
} }
contextMenu.hide() contextMenu.hide()
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,7 @@
import QtQuick import QtQuick
import Quickshell.Hyprland
import qs.Common import qs.Common
import qs.Services
Item { Item {
id: root id: root
@@ -16,6 +18,33 @@ Item {
anchors.topMargin: -(SettingsData.dankBarGothCornersEnabled && !axis.isVertical && axis.edge === "bottom" ? barWindow._wingR : 0) anchors.topMargin: -(SettingsData.dankBarGothCornersEnabled && !axis.isVertical && axis.edge === "bottom" ? barWindow._wingR : 0)
anchors.bottomMargin: -(SettingsData.dankBarGothCornersEnabled && !axis.isVertical && axis.edge === "top" ? barWindow._wingR : 0) anchors.bottomMargin: -(SettingsData.dankBarGothCornersEnabled && !axis.isVertical && axis.edge === "top" ? barWindow._wingR : 0)
readonly property real dpr: {
if (CompositorService.isNiri && barWindow.screen) {
const niriScale = NiriService.displayScales[barWindow.screen.name]
if (niriScale !== undefined) return niriScale
}
if (CompositorService.isHyprland && barWindow.screen) {
const hyprlandMonitor = Hyprland.monitors.values.find(m => m.name === barWindow.screen.name)
if (hyprlandMonitor?.scale !== undefined) return hyprlandMonitor.scale
}
return barWindow.screen?.devicePixelRatio || 1
}
function requestRepaint() {
debounceTimer.restart()
}
Timer {
id: debounceTimer
interval: 50
repeat: false
onTriggered: {
barShape.requestPaint()
barTint.requestPaint()
barBorder.requestPaint()
}
}
Canvas { Canvas {
id: barShape id: barShape
anchors.fill: parent anchors.fill: parent
@@ -23,28 +52,34 @@ Item {
renderTarget: Canvas.FramebufferObject renderTarget: Canvas.FramebufferObject
renderStrategy: Canvas.Cooperative renderStrategy: Canvas.Cooperative
readonly property real correctWidth: root.width readonly property real correctWidth: Theme.px(root.width, dpr)
readonly property real correctHeight: root.height readonly property real correctHeight: Theme.px(root.height, dpr)
canvasSize: Qt.size(Math.ceil(correctWidth), Math.ceil(correctHeight)) canvasSize: Qt.size(correctWidth, correctHeight)
property real wing: SettingsData.dankBarGothCornersEnabled ? barWindow._wingR : 0 property real wing: SettingsData.dankBarGothCornersEnabled ? Theme.px(barWindow._wingR, dpr) : 0
property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.cornerRadius property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.px(Theme.cornerRadius, dpr)
onWingChanged: requestPaint() onWingChanged: root.requestRepaint()
onRtChanged: requestPaint() onRtChanged: root.requestRepaint()
onCorrectWidthChanged: requestPaint() onCorrectWidthChanged: root.requestRepaint()
onCorrectHeightChanged: requestPaint() onCorrectHeightChanged: root.requestRepaint()
onVisibleChanged: if (visible) requestPaint() onVisibleChanged: if (visible) root.requestRepaint()
Component.onCompleted: requestPaint() Component.onCompleted: root.requestRepaint()
Connections {
target: root
function onDprChanged() { root.requestRepaint() }
}
Connections { Connections {
target: barWindow target: barWindow
function on_BgColorChanged() { barShape.requestPaint() } function on_BgColorChanged() { root.requestRepaint() }
} }
Connections { Connections {
target: Theme target: Theme
function onIsLightModeChanged() { barShape.requestPaint() } function onIsLightModeChanged() { root.requestRepaint() }
function onSurfaceContainerChanged() { root.requestRepaint() }
} }
onPaint: { onPaint: {
@@ -85,7 +120,7 @@ Item {
} }
ctx.reset() ctx.reset()
ctx.clearRect(0, 0, Math.ceil(W), Math.ceil(H_raw)) ctx.clearRect(0, 0, W, H_raw)
ctx.save() ctx.save()
if (isBottom) { if (isBottom) {
@@ -114,30 +149,36 @@ Item {
renderTarget: Canvas.FramebufferObject renderTarget: Canvas.FramebufferObject
renderStrategy: Canvas.Cooperative renderStrategy: Canvas.Cooperative
readonly property real correctWidth: root.width readonly property real correctWidth: Theme.px(root.width, dpr)
readonly property real correctHeight: root.height readonly property real correctHeight: Theme.px(root.height, dpr)
canvasSize: Qt.size(Math.ceil(correctWidth), Math.ceil(correctHeight)) canvasSize: Qt.size(correctWidth, correctHeight)
property real wing: SettingsData.dankBarGothCornersEnabled ? barWindow._wingR : 0 property real wing: SettingsData.dankBarGothCornersEnabled ? Theme.px(barWindow._wingR, dpr) : 0
property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.cornerRadius property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.px(Theme.cornerRadius, dpr)
property real alphaTint: (barWindow._bgColor?.a ?? 1) < 0.99 ? (Theme.stateLayerOpacity ?? 0) : 0 property real alphaTint: (barWindow._bgColor?.a ?? 1) < 0.99 ? (Theme.stateLayerOpacity ?? 0) : 0
onWingChanged: requestPaint() onWingChanged: root.requestRepaint()
onRtChanged: requestPaint() onRtChanged: root.requestRepaint()
onAlphaTintChanged: requestPaint() onAlphaTintChanged: root.requestRepaint()
onCorrectWidthChanged: requestPaint() onCorrectWidthChanged: root.requestRepaint()
onCorrectHeightChanged: requestPaint() onCorrectHeightChanged: root.requestRepaint()
onVisibleChanged: if (visible) requestPaint() onVisibleChanged: if (visible) root.requestRepaint()
Component.onCompleted: requestPaint() Component.onCompleted: root.requestRepaint()
Connections {
target: root
function onDprChanged() { root.requestRepaint() }
}
Connections { Connections {
target: barWindow target: barWindow
function on_BgColorChanged() { barTint.requestPaint() } function on_BgColorChanged() { root.requestRepaint() }
} }
Connections { Connections {
target: Theme target: Theme
function onIsLightModeChanged() { barTint.requestPaint() } function onIsLightModeChanged() { root.requestRepaint() }
function onSurfaceChanged() { root.requestRepaint() }
} }
onPaint: { onPaint: {
@@ -178,7 +219,7 @@ Item {
} }
ctx.reset() ctx.reset()
ctx.clearRect(0, 0, Math.ceil(W), Math.ceil(H_raw)) ctx.clearRect(0, 0, W, H_raw)
ctx.save() ctx.save()
if (isBottom) { if (isBottom) {
@@ -208,34 +249,44 @@ Item {
renderTarget: Canvas.FramebufferObject renderTarget: Canvas.FramebufferObject
renderStrategy: Canvas.Cooperative renderStrategy: Canvas.Cooperative
readonly property real correctWidth: root.width readonly property real correctWidth: Theme.px(root.width, dpr)
readonly property real correctHeight: root.height readonly property real correctHeight: Theme.px(root.height, dpr)
canvasSize: Qt.size(Math.ceil(correctWidth), Math.ceil(correctHeight)) canvasSize: Qt.size(correctWidth, correctHeight)
property real wing: SettingsData.dankBarGothCornersEnabled ? barWindow._wingR : 0 property real wing: SettingsData.dankBarGothCornersEnabled ? Theme.px(barWindow._wingR, dpr) : 0
property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.cornerRadius property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.px(Theme.cornerRadius, dpr)
property bool borderEnabled: SettingsData.dankBarBorderEnabled property bool borderEnabled: SettingsData.dankBarBorderEnabled
onWingChanged: requestPaint() onWingChanged: root.requestRepaint()
onRtChanged: requestPaint() onRtChanged: root.requestRepaint()
onBorderEnabledChanged: requestPaint() onBorderEnabledChanged: root.requestRepaint()
onCorrectWidthChanged: requestPaint() onCorrectWidthChanged: root.requestRepaint()
onCorrectHeightChanged: requestPaint() onCorrectHeightChanged: root.requestRepaint()
onVisibleChanged: if (visible) requestPaint() onVisibleChanged: if (visible) root.requestRepaint()
Component.onCompleted: requestPaint() Component.onCompleted: root.requestRepaint()
Connections {
target: root
function onDprChanged() { root.requestRepaint() }
}
Connections { Connections {
target: Theme target: Theme
function onIsLightModeChanged() { barBorder.requestPaint() } function onIsLightModeChanged() { root.requestRepaint() }
function onSurfaceTextChanged() { root.requestRepaint() }
function onPrimaryChanged() { root.requestRepaint() }
function onSecondaryChanged() { root.requestRepaint() }
function onOutlineChanged() { root.requestRepaint() }
} }
Connections { Connections {
target: SettingsData target: SettingsData
function onDankBarBorderColorChanged() { barBorder.requestPaint() } function onDankBarBorderColorChanged() { root.requestRepaint() }
function onDankBarBorderOpacityChanged() { barBorder.requestPaint() } function onDankBarBorderOpacityChanged() { root.requestRepaint() }
function onDankBarBorderThicknessChanged() { barBorder.requestPaint() } function onDankBarBorderThicknessChanged() { root.requestRepaint() }
function onDankBarSpacingChanged() { barBorder.requestPaint() } function onDankBarSpacingChanged() { root.requestRepaint() }
function onDankBarSquareCornersChanged() { barBorder.requestPaint() } function onDankBarSquareCornersChanged() { root.requestRepaint() }
function onDankBarTransparencyChanged() { root.requestRepaint() }
} }
onPaint: { onPaint: {
@@ -288,7 +339,7 @@ Item {
} }
ctx.reset() ctx.reset()
ctx.clearRect(0, 0, Math.ceil(W), Math.ceil(H_raw)) ctx.clearRect(0, 0, W, H_raw)
ctx.save() ctx.save()
if (isBottom) { if (isBottom) {

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -76,9 +76,6 @@ Loader {
onLoaded: { onLoaded: {
if (item) { if (item) {
contentItemReady(item) contentItemReady(item)
if (widgetId === "spacer") {
item.spacerSize = Qt.binding(() => spacerSize)
}
if (axis && "isVertical" in item) { if (axis && "isVertical" in item) {
try { try {
item.isVertical = axis.isVertical item.isVertical = axis.isVertical

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -166,9 +166,9 @@ Item {
source: { source: {
var currentWallpaper = SessionData.getMonitorWallpaper(screenName) var currentWallpaper = SessionData.getMonitorWallpaper(screenName)
if (screenName && currentWallpaper && currentWallpaper.startsWith("we:")) { if (screenName && currentWallpaper && currentWallpaper.startsWith("we:")) {
const cacheHome = StandardPaths.writableLocation(StandardPaths.CacheLocation).toString() const cacheHome = StandardPaths.writableLocation(StandardPaths.GenericCacheLocation).toString()
const baseDir = Paths.strip(cacheHome) const baseDir = Paths.strip(cacheHome)
const screenshotPath = baseDir + "/dankshell/we_screenshots" + "/" + currentWallpaper.substring(3) + ".jpg" const screenshotPath = baseDir + "/DankMaterialShell/we_screenshots" + "/" + currentWallpaper.substring(3) + ".jpg"
return screenshotPath return screenshotPath
} }
return (currentWallpaper && !currentWallpaper.startsWith("#")) ? currentWallpaper : "" return (currentWallpaper && !currentWallpaper.startsWith("#")) ? currentWallpaper : ""

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,11 @@
pragma ComponentBehavior: Bound
import QtCore import QtCore
import QtQuick import QtQuick
import QtQuick.Controls
import QtQuick.Effects import QtQuick.Effects
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Services.Pam
import Quickshell.Services.Mpris import Quickshell.Services.Mpris
import qs.Common import qs.Common
import qs.Services import qs.Services
@@ -42,6 +42,16 @@ Item {
updateHyprlandLayout() updateHyprlandLayout()
hyprlandLayoutUpdateTimer.start() hyprlandLayoutUpdateTimer.start()
} }
if (SessionService.loginctlAvailable && DMSService.apiVersion >= 2) {
DMSService.sendRequest("loginctl.lockerReady", null, response => {
if (response.error) {
console.warn("LockScreenContent: Failed to signal locker ready:", response.error)
} else {
console.log("LockScreenContent: Locker ready signaled, inhibitor released")
}
})
}
} }
onDemoModeChanged: { onDemoModeChanged: {
if (demoMode) { if (demoMode) {
@@ -122,9 +132,9 @@ Item {
source: { source: {
var currentWallpaper = SessionData.getMonitorWallpaper(screenName) var currentWallpaper = SessionData.getMonitorWallpaper(screenName)
if (screenName && currentWallpaper && currentWallpaper.startsWith("we:")) { if (screenName && currentWallpaper && currentWallpaper.startsWith("we:")) {
const cacheHome = StandardPaths.writableLocation(StandardPaths.CacheLocation).toString() const cacheHome = StandardPaths.writableLocation(StandardPaths.GenericCacheLocation).toString()
const baseDir = Paths.strip(cacheHome) const baseDir = Paths.strip(cacheHome)
const screenshotPath = baseDir + "/dankshell/we_screenshots" + "/" + currentWallpaper.substring(3) + ".jpg" const screenshotPath = baseDir + "/DankMaterialShell/we_screenshots" + "/" + currentWallpaper.substring(3) + ".jpg"
return screenshotPath return screenshotPath
} }
return (currentWallpaper && !currentWallpaper.startsWith("#")) ? currentWallpaper : "" return (currentWallpaper && !currentWallpaper.startsWith("#")) ? currentWallpaper : ""
@@ -180,8 +190,7 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top anchors.top: parent.top
text: { text: {
const format = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP" return systemClock.date.toLocaleTimeString(Qt.locale(), SettingsData.getEffectiveTimeFormat())
return systemClock.date.toLocaleTimeString(Qt.locale(), format)
} }
font.pixelSize: 120 font.pixelSize: 120
font.weight: Font.Light font.weight: Font.Light
@@ -242,22 +251,50 @@ Item {
border.color: passwordField.activeFocus ? Theme.primary : Qt.rgba(1, 1, 1, 0.3) border.color: passwordField.activeFocus ? Theme.primary : Qt.rgba(1, 1, 1, 0.3)
border.width: passwordField.activeFocus ? 2 : 1 border.width: passwordField.activeFocus ? 2 : 1
DankIcon { Item {
id: lockIcon id: lockIconContainer
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: Theme.spacingM anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
name: "lock" width: 20
size: 20 height: 20
color: passwordField.activeFocus ? Theme.primary : Theme.surfaceVariantText
DankIcon {
id: lockIcon
anchors.centerIn: parent
name: {
if (pam.fprint.tries >= SettingsData.maxFprintTries)
return "fingerprint_off";
if (pam.fprint.active)
return "fingerprint";
return "lock";
}
size: 20
color: pam.fprint.tries >= SettingsData.maxFprintTries ? Theme.error : (passwordField.activeFocus ? Theme.primary : Theme.surfaceVariantText)
opacity: pam.passwd.active ? 0 : 1
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
} }
TextInput { TextInput {
id: passwordField id: passwordField
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: lockIcon.width + Theme.spacingM * 2 anchors.leftMargin: lockIconContainer.width + Theme.spacingM * 2
anchors.rightMargin: { anchors.rightMargin: {
let margin = Theme.spacingM let margin = Theme.spacingM
if (loadingSpinner.visible) { if (loadingSpinner.visible) {
@@ -285,9 +322,12 @@ Item {
} }
} }
onAccepted: { onAccepted: {
if (!demoMode && !pam.active) { if (!demoMode && !pam.passwd.active) {
console.log("Enter pressed, starting PAM authentication") console.log("Enter pressed, starting PAM authentication")
pam.start() if (pam.fprint.active) {
pam.fprint.abort()
}
pam.passwd.start()
} }
} }
Keys.onPressed: event => { Keys.onPressed: event => {
@@ -295,7 +335,7 @@ Item {
return return
} }
if (pam.active) { if (pam.passwd.active) {
console.log("PAM is active, ignoring input") console.log("PAM is active, ignoring input")
event.accepted = true event.accepted = true
return return
@@ -344,7 +384,7 @@ Item {
StyledText { StyledText {
id: placeholder id: placeholder
anchors.left: lockIcon.right anchors.left: lockIconContainer.right
anchors.leftMargin: Theme.spacingM anchors.leftMargin: Theme.spacingM
anchors.right: (revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right)))) anchors.right: (revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right))))
anchors.rightMargin: 2 anchors.rightMargin: 2
@@ -356,12 +396,12 @@ Item {
if (root.unlocking) { if (root.unlocking) {
return "Unlocking..." return "Unlocking..."
} }
if (pam.active) { if (pam.passwd.active) {
return "Authenticating..." return "Authenticating..."
} }
return "Password..." return "Password..."
} }
color: root.unlocking ? Theme.primary : (pam.active ? Theme.primary : Theme.outline) color: root.unlocking ? Theme.primary : (pam.passwd.active ? Theme.primary : Theme.outline)
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
opacity: (demoMode || root.passwordBuffer.length === 0) ? 1 : 0 opacity: (demoMode || root.passwordBuffer.length === 0) ? 1 : 0
@@ -381,7 +421,7 @@ Item {
} }
StyledText { StyledText {
anchors.left: lockIcon.right anchors.left: lockIconContainer.right
anchors.leftMargin: Theme.spacingM anchors.leftMargin: Theme.spacingM
anchors.right: (revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right)))) anchors.right: (revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right))))
anchors.rightMargin: 2 anchors.rightMargin: 2
@@ -416,7 +456,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
iconName: parent.showPassword ? "visibility_off" : "visibility" iconName: parent.showPassword ? "visibility_off" : "visibility"
buttonSize: 32 buttonSize: 32
visible: !demoMode && root.passwordBuffer.length > 0 && !pam.active && !root.unlocking visible: !demoMode && root.passwordBuffer.length > 0 && !pam.passwd.active && !root.unlocking
enabled: visible enabled: visible
onClicked: parent.showPassword = !parent.showPassword onClicked: parent.showPassword = !parent.showPassword
} }
@@ -428,7 +468,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
iconName: "keyboard" iconName: "keyboard"
buttonSize: 32 buttonSize: 32
visible: !demoMode && !pam.active && !root.unlocking visible: !demoMode && !pam.passwd.active && !root.unlocking
enabled: visible enabled: visible
onClicked: { onClicked: {
if (keyboardController.isKeyboardActive) { if (keyboardController.isKeyboardActive) {
@@ -449,7 +489,7 @@ Item {
height: 24 height: 24
radius: 12 radius: 12
color: "transparent" color: "transparent"
visible: !demoMode && (pam.active || root.unlocking) visible: !demoMode && (pam.passwd.active || root.unlocking)
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
@@ -481,7 +521,7 @@ Item {
Item { Item {
anchors.fill: parent anchors.fill: parent
visible: pam.active && !root.unlocking visible: pam.passwd.active && !root.unlocking
Rectangle { Rectangle {
width: 20 width: 20
@@ -511,7 +551,7 @@ Item {
} }
RotationAnimation on rotation { RotationAnimation on rotation {
running: pam.active && !root.unlocking running: pam.passwd.active && !root.unlocking
loops: Animation.Infinite loops: Animation.Infinite
duration: Anims.durLong duration: Anims.durLong
from: 0 from: 0
@@ -529,12 +569,15 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
iconName: "keyboard_return" iconName: "keyboard_return"
buttonSize: 36 buttonSize: 36
visible: (demoMode || (!pam.active && !root.unlocking)) visible: (demoMode || (!pam.passwd.active && !root.unlocking))
enabled: !demoMode enabled: !demoMode
onClicked: { onClicked: {
if (!demoMode) { if (!demoMode) {
console.log("Enter button clicked, starting PAM authentication") console.log("Enter button clicked, starting PAM authentication")
pam.start() if (pam.fprint.active) {
pam.fprint.abort()
}
pam.passwd.start()
} }
} }
@@ -722,8 +765,9 @@ Item {
Repeater { Repeater {
model: 6 model: 6
delegate: Rectangle {
required property int index
Rectangle {
width: 2 width: 2
height: { height: {
if (MprisController.activePlayer?.playbackState === MprisPlaybackState.Playing && CavaService.values.length > index) { if (MprisController.activePlayer?.playbackState === MprisPlaybackState.Playing && CavaService.values.length > index) {
@@ -1081,52 +1125,29 @@ Item {
} }
} }
FileView { Pam {
id: pamConfigWatcher id: pam
lockSecured: !demoMode
path: "/etc/pam.d/dankshell" onUnlockRequested: {
printErrors: false root.unlocking = true
passwordField.text = ""
root.passwordBuffer = ""
root.unlockRequested()
}
onStateChanged: {
root.pamState = state
if (state !== "") {
placeholderDelay.restart()
passwordField.text = ""
root.passwordBuffer = ""
}
}
} }
PamContext { Binding {
id: pam target: pam
property: "buffer"
config: pamConfigWatcher.loaded ? "dankshell" : "login" value: root.passwordBuffer
onResponseRequiredChanged: {
if (demoMode)
return
console.log("PAM response required:", responseRequired)
if (!responseRequired)
return
console.log("Responding to PAM with password buffer length:", root.passwordBuffer.length)
respond(root.passwordBuffer)
}
onCompleted: res => {
if (demoMode)
return
console.log("PAM authentication completed with result:", res)
if (res === PamResult.Success) {
console.log("Authentication successful, unlocking")
root.unlocking = true
passwordField.text = ""
root.passwordBuffer = ""
root.unlockRequested()
return
}
console.log("Authentication failed:", res)
passwordField.text = ""
root.passwordBuffer = ""
if (res === PamResult.Error)
root.pamState = "error"
else if (res === PamResult.MaxTries)
root.pamState = "max"
else if (res === PamResult.Failed)
root.pamState = "fail"
placeholderDelay.restart()
}
} }
Timer { Timer {

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1154,6 +1154,105 @@ Item {
} }
} }
Column {
width: parent.width
spacing: Theme.spacingM
DankToggle {
width: parent.width
text: I18n.tr("Auto Popup Gaps")
description: I18n.tr("Automatically calculate popup distance from bar edge.")
checked: SettingsData.popupGapsAuto
onToggled: checked => {
SettingsData.setPopupGapsAuto(checked)
}
}
Column {
width: parent.width
leftPadding: Theme.spacingM
spacing: Theme.spacingM
visible: !SettingsData.popupGapsAuto
Rectangle {
width: parent.width - parent.leftPadding
height: 1
color: Theme.outline
opacity: 0.2
}
Column {
width: parent.width - parent.leftPadding
spacing: Theme.spacingS
Row {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: I18n.tr("Manual Gap Size")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - manualGapSizeText.implicitWidth - resetManualGapSizeBtn.width - Theme.spacingS - Theme.spacingM
height: 1
StyledText {
id: manualGapSizeText
visible: false
text: I18n.tr("Manual Gap Size")
font.pixelSize: Theme.fontSizeSmall
}
}
DankActionButton {
id: resetManualGapSizeBtn
buttonSize: 20
iconName: "refresh"
iconSize: 12
backgroundColor: Theme.surfaceContainerHigh
iconColor: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
onClicked: {
SettingsData.setPopupGapsManual(4)
}
}
Item {
width: Theme.spacingS
height: 1
}
}
DankSlider {
id: popupGapsManualSlider
width: parent.width
height: 24
value: SettingsData.popupGapsManual
minimum: 0
maximum: 50
unit: ""
showValue: true
wheelEnabled: false
thumbOutlineColor: Theme.surfaceContainerHigh
onSliderValueChanged: newValue => {
SettingsData.setPopupGapsManual(newValue)
}
Binding {
target: popupGapsManualSlider
property: "value"
value: SettingsData.popupGapsManual
restoreMode: Binding.RestoreBinding
}
}
}
}
}
DankToggle { DankToggle {
width: parent.width width: parent.width

View File

@@ -184,7 +184,6 @@ Item {
width: parent.width width: parent.width
spacing: Theme.spacingS spacing: Theme.spacingS
visible: SessionData.nightModeAutoEnabled visible: SessionData.nightModeAutoEnabled
leftPadding: Theme.spacingM
Connections { Connections {
target: SessionData target: SessionData
@@ -194,13 +193,14 @@ Item {
} }
Item { Item {
width: 200 width: parent.width
height: 45 + Theme.spacingM height: 45 + Theme.spacingM
DankTabBar { DankTabBar {
id: modeTabBarNight id: modeTabBarNight
width: 200 width: 200
height: 45 height: 45
anchors.horizontalCenter: parent.horizontalCenter
model: [{ model: [{
"text": "Time", "text": "Time",
"icon": "access_time" "icon": "access_time"
@@ -231,126 +231,124 @@ Item {
} }
Column { Column {
property bool isTimeMode: SessionData.nightModeAutoMode === "time" width: parent.width
visible: isTimeMode
spacing: Theme.spacingM spacing: Theme.spacingM
visible: SessionData.nightModeAutoMode === "time"
Row { Column {
spacing: Theme.spacingM spacing: Theme.spacingXS
height: 20 anchors.horizontalCenter: parent.horizontalCenter
leftPadding: 45
StyledText { Row {
text: I18n.tr("Hour") spacing: Theme.spacingM
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: 50
horizontalAlignment: Text.AlignHCenter
anchors.bottom: parent.bottom
}
StyledText { StyledText {
text: I18n.tr("Minute") text: ""
font.pixelSize: Theme.fontSizeSmall width: 50
color: Theme.surfaceVariantText height: 20
width: 50
horizontalAlignment: Text.AlignHCenter
anchors.bottom: parent.bottom
}
}
Row {
spacing: Theme.spacingM
height: 32
StyledText {
id: startLabel
text: I18n.tr("Start")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
width: 50
anchors.verticalCenter: parent.verticalCenter
}
DankDropdown {
width: 60
height: 32
text: ""
currentValue: SessionData.nightModeStartHour.toString()
options: {
var hours = []
for (var i = 0; i < 24; i++) {
hours.push(i.toString())
}
return hours
} }
onValueChanged: value => {
SessionData.setNightModeStartHour(parseInt(value))
}
}
DankDropdown { StyledText {
width: 60 text: I18n.tr("Hour")
height: 32 font.pixelSize: Theme.fontSizeSmall
text: "" color: Theme.surfaceVariantText
currentValue: SessionData.nightModeStartMinute.toString().padStart(2, '0') width: 70
options: { horizontalAlignment: Text.AlignHCenter
var minutes = []
for (var i = 0; i < 60; i += 5) {
minutes.push(i.toString().padStart(2, '0'))
}
return minutes
} }
onValueChanged: value => {
SessionData.setNightModeStartMinute(parseInt(value))
}
}
}
Row { StyledText {
spacing: Theme.spacingM text: I18n.tr("Minute")
height: 32 font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
StyledText { width: 70
text: I18n.tr("End") horizontalAlignment: Text.AlignHCenter
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
width: startLabel.width
anchors.verticalCenter: parent.verticalCenter
}
DankDropdown {
width: 60
height: 32
text: ""
currentValue: SessionData.nightModeEndHour.toString()
options: {
var hours = []
for (var i = 0; i < 24; i++) {
hours.push(i.toString())
}
return hours
} }
onValueChanged: value => {
SessionData.setNightModeEndHour(parseInt(value))
}
} }
DankDropdown { Row {
width: 60 spacing: Theme.spacingM
height: 32
text: "" StyledText {
currentValue: SessionData.nightModeEndMinute.toString().padStart(2, '0') text: I18n.tr("Start")
options: { font.pixelSize: Theme.fontSizeMedium
var minutes = [] color: Theme.surfaceText
for (var i = 0; i < 60; i += 5) { width: 50
minutes.push(i.toString().padStart(2, '0')) height: 40
} verticalAlignment: Text.AlignVCenter
return minutes }
DankDropdown {
dropdownWidth: 70
currentValue: SessionData.nightModeStartHour.toString()
options: {
var hours = []
for (var i = 0; i < 24; i++) {
hours.push(i.toString())
}
return hours
}
onValueChanged: value => {
SessionData.setNightModeStartHour(parseInt(value))
}
}
DankDropdown {
dropdownWidth: 70
currentValue: SessionData.nightModeStartMinute.toString().padStart(2, '0')
options: {
var minutes = []
for (var i = 0; i < 60; i += 5) {
minutes.push(i.toString().padStart(2, '0'))
}
return minutes
}
onValueChanged: value => {
SessionData.setNightModeStartMinute(parseInt(value))
}
}
}
Row {
spacing: Theme.spacingM
StyledText {
text: I18n.tr("End")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
width: 50
height: 40
verticalAlignment: Text.AlignVCenter
}
DankDropdown {
dropdownWidth: 70
currentValue: SessionData.nightModeEndHour.toString()
options: {
var hours = []
for (var i = 0; i < 24; i++) {
hours.push(i.toString())
}
return hours
}
onValueChanged: value => {
SessionData.setNightModeEndHour(parseInt(value))
}
}
DankDropdown {
dropdownWidth: 70
currentValue: SessionData.nightModeEndMinute.toString().padStart(2, '0')
options: {
var minutes = []
for (var i = 0; i < 60; i += 5) {
minutes.push(i.toString().padStart(2, '0'))
}
return minutes
}
onValueChanged: value => {
SessionData.setNightModeEndMinute(parseInt(value))
}
} }
onValueChanged: value => {
SessionData.setNightModeEndMinute(parseInt(value))
}
} }
} }
} }
@@ -378,71 +376,76 @@ Item {
} }
} }
StyledText { Column {
text: I18n.tr("Manual Coordinates") width: parent.width
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
visible: SessionData.nightModeLocationProvider !== "geoclue2"
}
Row {
spacing: Theme.spacingM spacing: Theme.spacingM
visible: SessionData.nightModeLocationProvider !== "geoclue2" visible: SessionData.nightModeLocationProvider !== "geoclue2"
leftPadding: Theme.spacingM
Column { StyledText {
spacing: Theme.spacingXS text: I18n.tr("Manual Coordinates")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
}
StyledText { Row {
text: I18n.tr("Latitude") spacing: Theme.spacingL
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText Column {
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Latitude")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
width: 120
height: 40
text: SessionData.latitude.toString()
placeholderText: "0.0"
onTextChanged: {
const lat = parseFloat(text) || 0.0
if (lat >= -90 && lat <= 90) {
SessionData.setLatitude(lat)
}
}
}
} }
DankTextField { Column {
width: 120 spacing: Theme.spacingXS
height: 40
text: SessionData.latitude.toString() StyledText {
placeholderText: "0.0" text: I18n.tr("Longitude")
onTextChanged: { font.pixelSize: Theme.fontSizeSmall
const lat = parseFloat(text) || 0.0 color: Theme.surfaceVariantText
if (lat >= -90 && lat <= 90) { }
SessionData.setLatitude(lat)
DankTextField {
width: 120
height: 40
text: SessionData.longitude.toString()
placeholderText: "0.0"
onTextChanged: {
const lon = parseFloat(text) || 0.0
if (lon >= -180 && lon <= 180) {
SessionData.setLongitude(lon)
}
} }
} }
} }
} }
Column { StyledText {
spacing: Theme.spacingXS text: I18n.tr("Uses sunrise/sunset times to automatically adjust night mode based on your location.")
font.pixelSize: Theme.fontSizeSmall
StyledText { color: Theme.surfaceVariantText
text: I18n.tr("Longitude") width: parent.width - parent.leftPadding
font.pixelSize: Theme.fontSizeSmall wrapMode: Text.WordWrap
color: Theme.surfaceVariantText
}
DankTextField {
width: 120
height: 40
text: SessionData.longitude.toString()
placeholderText: "0.0"
onTextChanged: {
const lon = parseFloat(text) || 0.0
if (lon >= -180 && lon <= 180) {
SessionData.setLongitude(lon)
}
}
}
} }
} }
StyledText {
text: I18n.tr("Uses sunrise/sunset times to automatically adjust night mode based on your location.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.WordWrap
}
} }
} }
} }

View File

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

View File

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

View File

@@ -61,6 +61,9 @@ Item {
Component.onCompleted: { Component.onCompleted: {
WallpaperCyclingService.cyclingActive WallpaperCyclingService.cyclingActive
fontEnumerationTimer.start() fontEnumerationTimer.start()
if (AudioService.gsettingsAvailable) {
AudioService.scanSoundThemes()
}
} }
DankFlickable { DankFlickable {
@@ -521,7 +524,7 @@ Item {
} }
Column { Column {
width: parent.width width: parent.width - (Theme.iconSize + Theme.spacingM)
spacing: Theme.spacingS spacing: Theme.spacingS
visible: SessionData.perMonitorWallpaper visible: SessionData.perMonitorWallpaper
leftPadding: Theme.iconSize + Theme.spacingM leftPadding: Theme.iconSize + Theme.spacingM
@@ -693,6 +696,7 @@ Item {
property var intervalOptions: ["1 minute", "5 minutes", "15 minutes", "30 minutes", "1 hour", "1.5 hours", "2 hours", "3 hours", "4 hours", "6 hours", "8 hours", "12 hours"] property var intervalOptions: ["1 minute", "5 minutes", "15 minutes", "30 minutes", "1 hour", "1.5 hours", "2 hours", "3 hours", "4 hours", "6 hours", "8 hours", "12 hours"]
property var intervalValues: [60, 300, 900, 1800, 3600, 5400, 7200, 10800, 14400, 21600, 28800, 43200] property var intervalValues: [60, 300, 900, 1800, 3600, 5400, 7200, 10800, 14400, 21600, 28800, 43200]
width: parent.width - parent.leftPadding
visible: { visible: {
if (SessionData.perMonitorWallpaper) { if (SessionData.perMonitorWallpaper) {
return SessionData.getMonitorCyclingSettings(selectedMonitorName).mode === "interval" return SessionData.getMonitorCyclingSettings(selectedMonitorName).mode === "interval"
@@ -1190,6 +1194,249 @@ Item {
} }
} }
} }
StyledRect {
width: parent.width
height: soundsSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0
visible: AudioService.soundsAvailable
Column {
id: soundsSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "volume_up"
size: Theme.iconSize
color: SettingsData.soundsEnabled ? Theme.primary : Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM - soundsToggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("Enable System Sounds")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Play sounds for system events")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
}
}
DankToggle {
id: soundsToggle
anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.soundsEnabled
onToggled: checked => {
SettingsData.setSoundsEnabled(checked)
}
}
}
Column {
width: parent.width
spacing: Theme.spacingM
visible: SettingsData.soundsEnabled
leftPadding: Theme.iconSize + Theme.spacingM
Rectangle {
width: parent.width - parent.leftPadding
height: 1
color: Theme.outline
opacity: 0.2
}
Row {
width: parent.width - parent.leftPadding
spacing: Theme.spacingM
visible: AudioService.gsettingsAvailable
Column {
width: parent.width - useSystemSoundThemeToggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("Use System Theme")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Use sound theme from system settings")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
}
}
DankToggle {
id: useSystemSoundThemeToggle
anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.useSystemSoundTheme
onToggled: checked => {
SettingsData.setUseSystemSoundTheme(checked)
}
}
}
DankDropdown {
id: soundThemeDropdown
width: parent.width - parent.leftPadding
text: I18n.tr("Sound Theme")
description: I18n.tr("Select system sound theme")
visible: SettingsData.useSystemSoundTheme && AudioService.availableSoundThemes.length > 0
enabled: SettingsData.useSystemSoundTheme && AudioService.availableSoundThemes.length > 0
options: AudioService.availableSoundThemes
currentValue: {
const theme = AudioService.currentSoundTheme
if (theme && AudioService.availableSoundThemes.includes(theme)) {
return theme
}
return AudioService.availableSoundThemes.length > 0 ? AudioService.availableSoundThemes[0] : ""
}
onValueChanged: value => {
if (value && value !== AudioService.currentSoundTheme) {
AudioService.setSoundTheme(value)
}
}
}
Rectangle {
width: parent.width - parent.leftPadding
height: 1
color: Theme.outline
opacity: 0.2
visible: AudioService.gsettingsAvailable
}
Row {
width: parent.width - parent.leftPadding
spacing: Theme.spacingM
Column {
width: parent.width - notificationSoundToggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("New Notification")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Play sound when new notification arrives")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
}
}
DankToggle {
id: notificationSoundToggle
anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.soundNewNotification
onToggled: checked => {
SettingsData.setSoundNewNotification(checked)
}
}
}
Row {
width: parent.width - parent.leftPadding
spacing: Theme.spacingM
Column {
width: parent.width - volumeSoundToggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("Volume Changed")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Play sound when volume is adjusted")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
}
}
DankToggle {
id: volumeSoundToggle
anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.soundVolumeChanged
onToggled: checked => {
SettingsData.setSoundVolumeChanged(checked)
}
}
}
Row {
width: parent.width - parent.leftPadding
spacing: Theme.spacingM
visible: BatteryService.batteryAvailable
Column {
width: parent.width - pluggedInSoundToggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("Plugged In")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Play sound when power cable is connected")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
}
}
DankToggle {
id: pluggedInSoundToggle
anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.soundPluggedIn
onToggled: checked => {
SettingsData.setSoundPluggedIn(checked)
}
}
}
}
}
}
} }
} }

View File

@@ -14,70 +14,42 @@ Item {
property var cachedFontFamilies: [] property var cachedFontFamilies: []
property var cachedMonoFamilies: [] property var cachedMonoFamilies: []
property var cachedIconThemes: []
property var cachedMatugenSchemes: []
property bool fontsEnumerated: false property bool fontsEnumerated: false
function enumerateFonts() { function enumerateFonts() {
var fonts = ["Default"] var fonts = []
var availableFonts = Qt.fontFamilies() var availableFonts = Qt.fontFamilies()
var rootFamilies = []
var seenFamilies = new Set()
for (var i = 0; i < availableFonts.length; i++) { for (var i = 0; i < availableFonts.length; i++) {
var fontName = availableFonts[i] var fontName = availableFonts[i]
if (fontName.startsWith(".")) if (fontName.startsWith("."))
continue continue
fonts.push(fontName)
if (fontName === SettingsData.defaultFontFamily)
continue
var rootName = fontName.replace(
/ (Thin|Extra Light|Light|Regular|Medium|Semi Bold|Demi Bold|Bold|Extra Bold|Black|Heavy)$/i,
"").replace(
/ (Italic|Oblique|Condensed|Extended|Narrow|Wide)$/i,
"").replace(/ (UI|Display|Text|Mono|Sans|Serif)$/i,
function (match, suffix) {
return match
}).trim()
if (!seenFamilies.has(rootName) && rootName !== "") {
seenFamilies.add(rootName)
rootFamilies.push(rootName)
}
} }
cachedFontFamilies = fonts.concat(rootFamilies.sort()) fonts.sort()
var monoFonts = ["Default"] fonts.unshift("Default")
var monoFamilies = [] cachedFontFamilies = fonts
var seenMonoFamilies = new Set()
var monoFonts = []
for (var j = 0; j < availableFonts.length; j++) { for (var j = 0; j < availableFonts.length; j++) {
var fontName2 = availableFonts[j] var fontName2 = availableFonts[j]
if (fontName2.startsWith(".")) if (fontName2.startsWith("."))
continue continue
if (fontName2 === SettingsData.defaultMonoFontFamily)
continue
var lowerName = fontName2.toLowerCase() var lowerName = fontName2.toLowerCase()
if (lowerName.includes("mono") || lowerName.includes( if (lowerName.includes("mono") || lowerName.includes("code") ||
"code") || lowerName.includes( lowerName.includes("console") || lowerName.includes("terminal") ||
"console") || lowerName.includes( lowerName.includes("courier") || lowerName.includes("jetbrains") ||
"terminal") || lowerName.includes( lowerName.includes("fira") || lowerName.includes("hack") ||
"courier") || lowerName.includes( lowerName.includes("source code") || lowerName.includes("cascadia")) {
"dejavu sans mono") || lowerName.includes( monoFonts.push(fontName2)
"jetbrains") || lowerName.includes(
"fira") || lowerName.includes(
"hack") || lowerName.includes(
"source code") || lowerName.includes(
"ubuntu mono") || lowerName.includes("cascadia")) {
var rootName2 = fontName2.replace(
/ (Thin|Extra Light|Light|Regular|Medium|Semi Bold|Demi Bold|Bold|Extra Bold|Black|Heavy)$/i,
"").replace(
/ (Italic|Oblique|Condensed|Extended|Narrow|Wide)$/i,
"").trim()
if (!seenMonoFamilies.has(rootName2) && rootName2 !== "") {
seenMonoFamilies.add(rootName2)
monoFamilies.push(rootName2)
}
} }
} }
cachedMonoFamilies = monoFonts.concat(monoFamilies.sort()) monoFonts.sort()
monoFonts.unshift("Default")
cachedMonoFamilies = monoFonts
} }
Component.onCompleted: { Component.onCompleted: {
@@ -85,6 +57,9 @@ Item {
enumerateFonts() enumerateFonts()
fontsEnumerated = true fontsEnumerated = true
} }
SettingsData.detectAvailableIconThemes()
cachedIconThemes = SettingsData.availableIconThemes
cachedMatugenSchemes = Theme.availableMatugenSchemes.map(function (option) { return option.label })
} }
DankFlickable { DankFlickable {
@@ -653,7 +628,7 @@ Item {
id: matugenPaletteDropdown id: matugenPaletteDropdown
text: I18n.tr("Matugen Palette") text: I18n.tr("Matugen Palette")
description: "Select the palette algorithm used for wallpaper-based colors" description: "Select the palette algorithm used for wallpaper-based colors"
options: Theme.availableMatugenSchemes.map(function (option) { return option.label }) options: cachedMatugenSchemes
currentValue: Theme.getMatugenScheme(SettingsData.matugenScheme).label currentValue: Theme.getMatugenScheme(SettingsData.matugenScheme).label
enabled: Theme.matugenAvailable enabled: Theme.matugenAvailable
opacity: enabled ? 1 : 0.4 opacity: enabled ? 1 : 0.4
@@ -1064,7 +1039,7 @@ Item {
enableFuzzySearch: true enableFuzzySearch: true
popupWidthOffset: 100 popupWidthOffset: 100
maxPopupHeight: 400 maxPopupHeight: 400
options: cachedFontFamilies options: cachedMonoFamilies
onValueChanged: value => { onValueChanged: value => {
if (value === "Default") if (value === "Default")
SettingsData.setMonoFontFamily(SettingsData.defaultMonoFontFamily) SettingsData.setMonoFontFamily(SettingsData.defaultMonoFontFamily)
@@ -1161,6 +1136,62 @@ Item {
} }
} }
StyledRect {
width: parent.width
height: portalSyncSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
Row {
id: portalSyncSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
DankIcon {
name: "sync"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM - syncToggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("Sync Mode with Portal")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Sync dark mode with settings portals for system-wide theme hints")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
DankToggle {
id: syncToggle
width: 48
height: 32
checked: SettingsData.syncModeWithPortal
anchors.verticalCenter: parent.verticalCenter
onToggled: checked => SettingsData.setSyncModeWithPortal(checked)
}
}
}
// System Configuration Warning // System Configuration Warning
Rectangle { Rectangle {
width: parent.width width: parent.width
@@ -1232,10 +1263,7 @@ Item {
enableFuzzySearch: true enableFuzzySearch: true
popupWidthOffset: 100 popupWidthOffset: 100
maxPopupHeight: 236 maxPopupHeight: 236
options: { options: cachedIconThemes
SettingsData.detectAvailableIconThemes()
return SettingsData.availableIconThemes
}
onValueChanged: value => { onValueChanged: value => {
SettingsData.setIconTheme(value) SettingsData.setIconTheme(value)
if (Quickshell.env("QT_QPA_PLATFORMTHEME") != "gtk3" && if (Quickshell.env("QT_QPA_PLATFORMTHEME") != "gtk3" &&

View File

@@ -85,6 +85,69 @@ Item {
} }
} }
StyledRect {
width: parent.width
height: timeSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
Column {
id: secondsSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "schedule"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM
- toggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("Show seconds")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Clock show seconds")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
DankToggle {
id: toggleSec
anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.showSeconds
onToggled: checked => {
return SettingsData.setTimeFormat(
checked)
}
}
}
}
}
StyledRect { StyledRect {
width: parent.width width: parent.width
height: dateSection.implicitHeight + Theme.spacingL * 2 height: dateSection.implicitHeight + Theme.spacingL * 2

View File

@@ -65,6 +65,16 @@ Item {
checked) checked)
} }
} }
DankToggle {
width: parent.width
text: I18n.tr("Window Scrolling")
description: "Scroll through windows, rather than workspaces"
checked: SettingsData.workspaceScrolling
visible: CompositorService.isNiri
onToggled: checked => {
return SettingsData.setWorkspaceScrolling(checked)
}
}
DankToggle { DankToggle {
width: parent.width width: parent.width
@@ -184,7 +194,126 @@ Item {
description: "Use animated wave progress bars for media playback" description: "Use animated wave progress bars for media playback"
checked: SettingsData.waveProgressEnabled checked: SettingsData.waveProgressEnabled
onToggled: checked => { onToggled: checked => {
return SettingsData.setWaveProgressEnabled(checked) return SettingsData.setWaveProgressEnabled(checked);
}
}
}
}
StyledRect {
width: parent.width
height: updaterSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0
Column {
id: updaterSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "refresh"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("System Updater")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
width: parent.width
text: I18n.tr("Use Custom Command")
description: I18n.tr("Use custom command for update your system")
checked: SettingsData.updaterUseCustomCommand
onToggled: checked => {
if (!checked) {
updaterCustomCommand.text = "";
updaterTerminalCustomClass.text = "";
SettingsData.setUpdaterCustomCommand("");
SettingsData.setUpdaterTerminalAdditionalParams("");
}
return SettingsData.setUpdaterUseCustomCommandEnabled(checked);
}
}
Column {
width: parent.width - Theme.spacingM * 2
spacing: Theme.spacingXS
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
StyledText {
text: I18n.tr("System update custom command")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: updaterCustomCommand
width: parent.width
height: 48
placeholderText: "myPkgMngr --sysupdate"
backgroundColor: Theme.surfaceVariant
normalBorderColor: Theme.primarySelected
focusedBorderColor: Theme.primary
Component.onCompleted: {
if (SettingsData.updaterCustomCommand) {
text = SettingsData.updaterCustomCommand;
}
}
onTextEdited: {
SettingsData.setUpdaterCustomCommand(text.trim());
}
}
}
Column {
width: parent.width - Theme.spacingM * 2
spacing: Theme.spacingXS
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
StyledText {
text: I18n.tr("Terminal custom additional parameters")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: updaterTerminalCustomClass
width: parent.width
height: 48
placeholderText: "-T udpClass"
backgroundColor: Theme.surfaceVariant
normalBorderColor: Theme.primarySelected
focusedBorderColor: Theme.primary
Component.onCompleted: {
if (SettingsData.updaterTerminalAdditionalParams) {
text = SettingsData.updaterTerminalAdditionalParams;
}
}
onTextEdited: {
SettingsData.setUpdaterTerminalAdditionalParams(text.trim());
}
} }
} }
} }

View File

@@ -22,6 +22,10 @@ DankPopout {
triggerScreen = screen; triggerScreen = screen;
} }
Ref {
service: SystemUpdateService
}
popupWidth: 400 popupWidth: 400
popupHeight: 500 popupHeight: 500
triggerX: Screen.width - 600 - Theme.spacingL triggerX: Screen.width - 600 - Theme.spacingL
@@ -216,7 +220,7 @@ DankPopout {
Column { Column {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: parent.width - 24 - Theme.spacingM width: parent.width - Theme.spacingM
spacing: 2 spacing: 2
StyledText { StyledText {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,9 +4,10 @@
"description": "Example widget demonstrating PopoutService usage with pillClickAction", "description": "Example widget demonstrating PopoutService usage with pillClickAction",
"version": "1.0.0", "version": "1.0.0",
"author": "DankMaterialShell", "author": "DankMaterialShell",
"icon": "widgets",
"type": "widget", "type": "widget",
"capabilities": ["dankbar-widget"],
"component": "./PopoutControlWidget.qml", "component": "./PopoutControlWidget.qml",
"icon": "widgets",
"settings": "./PopoutControlSettings.qml", "settings": "./PopoutControlSettings.qml",
"permissions": ["settings_read", "settings_write"] "permissions": ["settings_read", "settings_write"]
} }

View File

@@ -2,6 +2,10 @@
Create widgets for DankBar and Control Center using dynamically-loaded QML components. Create widgets for DankBar and Control Center using dynamically-loaded QML components.
## Plugin Registry
Browse and discover community plugins at **https://plugins.danklinux.com/**
## Overview ## Overview
Plugins let you add custom widgets to DankBar and Control Center. They're discovered from `~/.config/DankMaterialShell/plugins/` and managed via PluginService. Plugins let you add custom widgets to DankBar and Control Center. They're discovered from `~/.config/DankMaterialShell/plugins/` and managed via PluginService.
@@ -45,7 +49,9 @@ $CONFIGPATH/DankMaterialShell/plugins/YourPlugin/
### Plugin Manifest (plugin.json) ### Plugin Manifest (plugin.json)
The manifest file defines plugin metadata and configuration: The manifest file defines plugin metadata and configuration.
**JSON Schema:** See `plugin-schema.json` for the complete specification and validation schema.
```json ```json
{ {
@@ -54,9 +60,13 @@ The manifest file defines plugin metadata and configuration:
"description": "Brief description of what your plugin does", "description": "Brief description of what your plugin does",
"version": "1.0.0", "version": "1.0.0",
"author": "Your Name", "author": "Your Name",
"icon": "material_icon_name", "type": "widget",
"capabilities": ["thing-my-plugin-does"],
"component": "./YourWidget.qml", "component": "./YourWidget.qml",
"icon": "material_icon_name",
"settings": "./YourSettings.qml", "settings": "./YourSettings.qml",
"requires_dms": ">=0.1.0",
"requires": ["some-system-tool"],
"permissions": [ "permissions": [
"settings_read", "settings_read",
"settings_write" "settings_write"
@@ -67,15 +77,22 @@ The manifest file defines plugin metadata and configuration:
**Required Fields:** **Required Fields:**
- `id`: Unique plugin identifier (camelCase, no spaces) - `id`: Unique plugin identifier (camelCase, no spaces)
- `name`: Human-readable plugin name - `name`: Human-readable plugin name
- `component`: Relative path to widget QML file - `description`: Short description of plugin functionality (displayed in UI)
- `version`: Semantic version string (e.g., "1.0.0")
- `author`: Plugin creator name or email
- `type`: Plugin type - "widget", "daemon", or "launcher"
- `capabilities`: Array of plugin capabilities (e.g., ["dankbar-widget"], ["control-center"], ["monitoring"])
- `component`: Relative path to main QML component file
**Required for Launcher Type:**
- `trigger`: Trigger string for launcher activation (e.g., "=", "#", "!")
**Optional Fields:** **Optional Fields:**
- `description`: Short description of plugin functionality (displayed in UI)
- `version`: Semantic version string (displayed in UI)
- `author`: Plugin creator name (displayed in UI)
- `icon`: Material Design icon name (displayed in UI) - `icon`: Material Design icon name (displayed in UI)
- `settings`: Path to settings component (enables settings UI) - `settings`: Path to settings component (enables settings UI)
- `permissions`: Required capabilities (enforced by PluginSettings component) - `requires_dms`: Minimum DMS version requirement (e.g., ">=0.1.18", ">0.1.0")
- `requires`: Array of required system tools/dependencies (e.g., ["wl-copy", "curl"])
- `permissions`: Required DMS permissions (e.g., ["settings_read", "settings_write"])
**Permissions:** **Permissions:**
@@ -544,6 +561,10 @@ PluginService.getWidgetComponents(): object
// Data Persistence // Data Persistence
PluginService.savePluginData(pluginId: string, key: string, value: any): bool PluginService.savePluginData(pluginId: string, key: string, value: any): bool
PluginService.loadPluginData(pluginId: string, key: string, defaultValue: any): any PluginService.loadPluginData(pluginId: string, key: string, defaultValue: any): any
// Global Variables - Shared state across all plugin instances
PluginService.getGlobalVar(pluginId: string, varName: string, defaultValue: any): any
PluginService.setGlobalVar(pluginId: string, varName: string, value: any): void
``` ```
### Signals ### Signals
@@ -552,8 +573,124 @@ PluginService.loadPluginData(pluginId: string, key: string, defaultValue: any):
PluginService.pluginLoaded(pluginId: string) PluginService.pluginLoaded(pluginId: string)
PluginService.pluginUnloaded(pluginId: string) PluginService.pluginUnloaded(pluginId: string)
PluginService.pluginLoadFailed(pluginId: string, error: string) PluginService.pluginLoadFailed(pluginId: string, error: string)
PluginService.globalVarChanged(pluginId: string, varName: string)
``` ```
## Plugin Global Variables
Plugins can share state across multiple instances using global variables. This is useful when you have the same widget displayed on multiple monitors or multiple instances of the same widget on different bars.
### Why Use Global Variables?
Unlike regular properties which are scoped to each component instance, global variables are synchronized across all instances of your plugin. This enables:
- **Multi-monitor consistency**: Same data displayed across all monitors
- **Multi-instance widgets**: Multiple instances of the same widget sharing state
- **Cross-component communication**: Share data between widget and settings components
### Using PluginGlobalVar
The `PluginGlobalVar` helper component provides reactive global variable access:
```qml
import QtQuick
import qs.Widgets
import qs.Modules.Plugins
PluginComponent {
PluginGlobalVar {
id: globalCounter
varName: "counter"
defaultValue: 0
}
horizontalBarPill: Component {
StyledRect {
width: content.implicitWidth + Theme.spacingM * 2
height: parent.widgetThickness
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
StyledText {
id: content
anchors.centerIn: parent
text: "Count: " + globalCounter.value
color: Theme.surfaceText
}
MouseArea {
anchors.fill: parent
onClicked: globalCounter.set(globalCounter.value + 1)
}
}
}
}
```
**PluginGlobalVar Properties:**
- `varName` (required): Name of the global variable
- `defaultValue` (optional): Default value if not set
- `value` (readonly): Current value of the global variable
**PluginGlobalVar Methods:**
- `set(newValue)`: Update the global variable (triggers reactivity across all instances)
### Using PluginService API Directly
For more control, use the PluginService API directly:
```qml
import QtQuick
import qs.Services
import qs.Modules.Plugins
PluginComponent {
property int counter: PluginService.getGlobalVar("myPlugin", "counter", 0)
Connections {
target: PluginService
function onGlobalVarChanged(pluginId, varName) {
if (pluginId === "myPlugin" && varName === "counter") {
counter = PluginService.getGlobalVar("myPlugin", "counter", 0)
}
}
}
horizontalBarPill: Component {
StyledRect {
MouseArea {
anchors.fill: parent
onClicked: {
const current = PluginService.getGlobalVar("myPlugin", "counter", 0)
PluginService.setGlobalVar("myPlugin", "counter", current + 1)
}
}
}
}
}
```
### Global Variables vs Settings
**Global Variables** (`getGlobalVar`/`setGlobalVar`):
- Runtime state only (not persisted to disk)
- Synchronized across all plugin instances
- Changes trigger `globalVarChanged` signal for reactivity
- Use for: counters, current selection, temporary UI state
**Settings** (`savePluginData`/`loadPluginData`):
- Persisted to `settings.json` across sessions
- Loaded once per plugin instance
- Use for: user preferences, API keys, configuration
### Important Notes
1. **Reactivity**: Global variables are reactive - all instances update when a value changes
2. **Namespacing**: Variables are namespaced by plugin ID to avoid conflicts
3. **Type Safety**: Values can be any QML/JavaScript type (numbers, strings, objects, arrays)
4. **Not Persistent**: Global variables are cleared when the shell restarts (use settings for persistence)
5. **Performance**: Efficient for frequent updates - changes only trigger updates for the specific variable
## Creating a Plugin ## Creating a Plugin
### Step 1: Create Plugin Directory ### Step 1: Create Plugin Directory
@@ -574,9 +711,12 @@ Create `plugin.json`:
"description": "A sample plugin", "description": "A sample plugin",
"version": "1.0.0", "version": "1.0.0",
"author": "Your Name", "author": "Your Name",
"icon": "extension", "type": "widget",
"capabilities": ["my-functionality"],
"component": "./MyWidget.qml", "component": "./MyWidget.qml",
"icon": "extension",
"settings": "./MySettings.qml", "settings": "./MySettings.qml",
"requires_dms": ">=0.1.0",
"permissions": ["settings_read", "settings_write"] "permissions": ["settings_read", "settings_write"]
} }
``` ```
@@ -709,6 +849,231 @@ Or edit `$CONFIGPATH/quickshell/dms/config.json`:
8. **Versioning**: Use semantic versioning for updates 8. **Versioning**: Use semantic versioning for updates
9. **Dependencies**: Document external library requirements 9. **Dependencies**: Document external library requirements
## Clipboard Access
Plugins that need to copy text to the clipboard **must** use the Wayland clipboard utility `wl-copy` through Quickshell's `execDetached` function.
### Correct Method
Import Quickshell and use `execDetached` with `wl-copy`:
```qml
import QtQuick
import Quickshell
Item {
function copyToClipboard(text) {
Quickshell.execDetached(["sh", "-c", "echo -n '" + text + "' | wl-copy"])
}
}
```
### Example Usage
From the ExampleEmojiPlugin (EmojiWidget.qml:136):
```qml
MouseArea {
onClicked: {
Quickshell.execDetached(["sh", "-c", "echo -n '" + modelData + "' | wl-copy"])
ToastService.showInfo("Copied " + modelData + " to clipboard")
popoutColumn.closePopout()
}
}
```
### Important Notes
1. **Do NOT** use `globalThis.clipboard` or similar JavaScript APIs - they don't exist in the QML runtime
2. **Always** import `Quickshell` at the top of your QML file
3. **Use** `echo -n` to prevent adding a trailing newline to the clipboard content
4. The `-c` flag for `sh` is required to execute the pipe command properly
5. Consider showing a toast notification to confirm the copy action to users
### Dependencies
This method requires `wl-copy` from the `wl-clipboard` package, which is standard on Wayland systems.
## Running External Commands
Plugins that need to execute external commands and capture their output should use the `Proc` singleton, which provides debounced command execution with automatic cleanup.
### Correct Method
Import the `Proc` singleton from `qs.Common` and use `runCommand`:
```qml
import QtQuick
import qs.Common
Item {
function fetchData() {
Proc.runCommand(
"myPlugin.fetchData",
["curl", "-s", "https://api.example.com/data"],
(stdout, exitCode) => {
if (exitCode === 0) {
console.log("Success:", stdout)
processData(stdout)
} else {
console.error("Command failed with exit code:", exitCode)
}
},
100
)
}
}
```
### Function Signature
```qml
Proc.runCommand(id, command, callback, debounceMs)
```
**Parameters:**
- `id` (string): Unique identifier for this command. Used for debouncing - multiple calls with the same ID within the debounce window will only execute the last one
- `command` (array): Command and arguments as an array (e.g., `["sh", "-c", "echo hello"]`)
- `callback` (function): Callback function receiving `(stdout, exitCode)` when the command completes
- `stdout` (string): Captured standard output from the command
- `exitCode` (number): Exit code of the process (0 typically means success)
- `debounceMs` (number, optional): Debounce delay in milliseconds. Defaults to 50ms if not specified
### Key Features
1. **Automatic Cleanup**: Process objects are automatically destroyed after completion
2. **Debouncing**: Rapid successive calls with the same ID are debounced, only executing the last one
3. **Output Capture**: Automatically captures stdout for processing
4. **Error Handling**: Exit codes are passed to the callback for error detection
### Example Usage
#### Simple Command Execution
```qml
import QtQuick
import qs.Common
Item {
function checkNetwork() {
Proc.runCommand(
"myPlugin.ping",
["ping", "-c", "1", "8.8.8.8"],
(output, exitCode) => {
if (exitCode === 0) {
console.log("Network is up")
} else {
console.log("Network is down")
}
}
)
}
}
```
#### Parsing Command Output
```qml
import QtQuick
import qs.Common
Item {
property var diskUsage: ({})
function updateDiskUsage() {
Proc.runCommand(
"myPlugin.df",
["df", "-h", "/home"],
(output, exitCode) => {
if (exitCode === 0) {
const lines = output.trim().split("\n")
if (lines.length > 1) {
const parts = lines[1].split(/\s+/)
diskUsage = {
total: parts[1],
used: parts[2],
available: parts[3],
percent: parts[4]
}
}
}
}
)
}
}
```
#### Shell Commands with Pipes
```qml
import QtQuick
import qs.Common
Item {
function getTopProcess() {
Proc.runCommand(
"myPlugin.topProcess",
["sh", "-c", "ps aux | sort -nrk 3,3 | head -n 1"],
(output, exitCode) => {
if (exitCode === 0) {
console.log("Top process:", output)
}
}
)
}
}
```
#### Debouncing Rapid Updates
```qml
import QtQuick
import qs.Common
import qs.Widgets
Item {
DankTextField {
id: searchField
placeholderText: "Search files..."
onTextChanged: {
Proc.runCommand(
"myPlugin.search",
["find", "/home", "-name", "*" + text + "*"],
(output, exitCode) => {
if (exitCode === 0) {
updateSearchResults(output)
}
},
500
)
}
}
}
```
### Important Notes
1. **Unique IDs**: Use descriptive, namespaced IDs (e.g., `"myPlugin.actionName"`) to avoid conflicts
2. **Debouncing**: Use appropriate debounce delays for your use case:
- Fast updates (50-100ms): System monitoring, real-time data
- User input (300-500ms): Search fields, text input processing
- Network requests (500-1000ms): API calls, web scraping
3. **Error Handling**: Always check the exit code in your callback before processing output
4. **Shell Commands**: Use `["sh", "-c", "command"]` for complex shell commands with pipes or redirects
5. **Security**: Sanitize user input before passing to commands to prevent command injection
6. **Performance**: Avoid running expensive commands too frequently - use debouncing wisely
### Comparison with Other Methods
**Proc.runCommand** vs **Quickshell.execDetached**:
- Use `Proc.runCommand` when you need to capture output or check exit codes
- Use `Quickshell.execDetached` for fire-and-forget operations (like clipboard copy)
**Proc.runCommand** vs **Process component**:
- Use `Proc.runCommand` for simple, one-off command executions with automatic cleanup
- Use `Process` component for long-running processes or when you need fine-grained control
## Debugging ## Debugging
### Console Logging ### Console Logging
@@ -771,12 +1136,273 @@ The plugin API is currently **experimental**. Breaking changes may occur in mino
- Plugin update notifications - Plugin update notifications
- Inter-plugin communication - Inter-plugin communication
## Launcher Plugins
Launcher plugins extend the DMS application launcher by adding custom searchable items with trigger-based filtering.
### Overview
Launcher plugins enable you to:
- Add custom items to the launcher/app drawer
- Use trigger strings for quick filtering (e.g., `!`, `#`, `@`)
- Execute custom actions when items are selected
- Provide searchable, categorized content
- Integrate seamlessly with the existing launcher
### Plugin Type Configuration
To create a launcher plugin, set the plugin type in `plugin.json`:
```json
{
"id": "myLauncher",
"name": "My Launcher Plugin",
"description": "A custom launcher plugin for quick actions",
"version": "1.0.0",
"author": "Your Name",
"type": "launcher",
"capabilities": ["show-thing"],
"component": "./MyLauncher.qml",
"trigger": "#",
"icon": "search",
"settings": "./MySettings.qml",
"requires_dms": ">=0.1.18",
"permissions": ["settings_read", "settings_write"]
}
```
### Launcher Component Contract
Create `MyLauncher.qml` with the following interface:
```qml
import QtQuick
import qs.Services
Item {
id: root
// Required properties
property var pluginService: null
property string trigger: "#"
// Required signals
signal itemsChanged()
// Required: Return array of launcher items
function getItems(query) {
return [
{
name: "Item Name",
icon: "icon_name",
comment: "Description",
action: "type:data",
categories: ["MyLauncher"]
}
]
}
// Required: Execute item action
function executeItem(item) {
const [type, data] = item.action.split(":", 2)
// Handle action based on type
}
Component.onCompleted: {
if (pluginService) {
trigger = pluginService.loadPluginData("myLauncher", "trigger", "#")
}
}
}
```
### Item Structure
Each item returned by `getItems()` must include:
- `name` (string): Display name shown in launcher
- `icon` (string): Material Design icon name
- `comment` (string): Description/subtitle text
- `action` (string): Action identifier in `type:data` format
- `categories` (array): Array containing your plugin name
### Trigger System
Triggers control when your plugin's items appear in the launcher:
**Empty Trigger Mode** (No trigger):
- Items always visible alongside regular apps
- Search includes your items automatically
- Configure by saving empty trigger: `trigger: ""`
**Custom Trigger Mode**:
- Items only appear when trigger is typed
- Example: Type `#` to show only your plugin's items
- Type `# query` to search within your plugin
- Configure any string: `#`, `!`, `@`, `!custom`, etc.
### Trigger Configuration in Settings
Provide a settings component with trigger configuration:
```qml
import QtQuick
import QtQuick.Controls
import qs.Widgets
FocusScope {
id: root
property var pluginService: null
Column {
spacing: 12
CheckBox {
id: noTriggerToggle
text: "No trigger (always show)"
checked: loadSettings("noTrigger", false)
onCheckedChanged: {
saveSettings("noTrigger", checked)
if (checked) {
saveSettings("trigger", "")
} else {
saveSettings("trigger", triggerField.text || "#")
}
}
}
DankTextField {
id: triggerField
visible: !noTriggerToggle.checked
text: loadSettings("trigger", "#")
placeholderText: "#"
onTextEdited: {
saveSettings("trigger", text || "#")
}
}
}
function saveSettings(key, value) {
if (pluginService) {
pluginService.savePluginData("myLauncher", key, value)
}
}
function loadSettings(key, defaultValue) {
if (pluginService) {
return pluginService.loadPluginData("myLauncher", key, defaultValue)
}
return defaultValue
}
}
```
### Action Execution
Handle different action types in `executeItem()`:
```qml
function executeItem(item) {
const actionParts = item.action.split(":")
const actionType = actionParts[0]
const actionData = actionParts.slice(1).join(":")
switch (actionType) {
case "toast":
if (typeof ToastService !== "undefined") {
ToastService.showInfo("Plugin", actionData)
}
break
case "copy":
// Copy to clipboard
break
case "script":
// Execute command
break
default:
console.warn("Unknown action:", actionType)
}
}
```
### Search and Filtering
The launcher automatically handles search when:
**With empty trigger**:
- Your items appear in all searches
- No prefix needed
**With custom trigger**:
- Type trigger alone: Shows all your items
- Type trigger + query: Filters your items by query
- The query parameter is passed to your `getItems(query)` function
Example `getItems()` implementation:
```qml
function getItems(query) {
const allItems = [
{name: "Item 1", ...},
{name: "Item 2", ...},
{name: "Test Item", ...}
]
if (!query || query.length === 0) {
return allItems
}
const lowerQuery = query.toLowerCase()
return allItems.filter(item => {
return item.name.toLowerCase().includes(lowerQuery) ||
item.comment.toLowerCase().includes(lowerQuery)
})
}
```
### Integration Flow
1. User opens launcher
2. If empty trigger: Your items appear alongside apps
3. If custom trigger: User types trigger (e.g., `#`)
4. Launcher calls `getItems(query)` on your plugin
5. Your items displayed with your plugin's category
6. User selects item and presses Enter
7. Launcher calls `executeItem(item)` on your plugin
### Best Practices
1. **Unique Triggers**: Choose non-conflicting trigger strings
2. **Fast Response**: Return results quickly from `getItems()`
3. **Clear Names**: Use descriptive item names and comments
4. **Error Handling**: Gracefully handle failures in `executeItem()`
5. **Cleanup**: Destroy temporary objects after use
6. **Empty Trigger Support**: Consider if your plugin benefits from always being visible
### Example Plugin
See `PLUGINS/LauncherExample/` for a complete working example demonstrating:
- Trigger configuration (including empty trigger mode)
- Multiple action types (toast, copy, script)
- Search/filtering implementation
- Settings integration
- Proper error handling
## Resources ## Resources
- **Example Plugins**: [Emoji Picker](./ExampleEmojiPlugin/) [WorldClock](https://github.com/rochacbruno/WorldClock) - **Plugin Schema**: `plugin-schema.json` - JSON Schema for validation
- **Example Plugins**:
- [Emoji Picker](./ExampleEmojiPlugin/)
- [WorldClock](https://github.com/rochacbruno/WorldClock)
- [LauncherExample](./LauncherExample/)
- [Calculator](https://github.com/rochacbruno/DankCalculator)
- **PluginService**: `Services/PluginService.qml` - **PluginService**: `Services/PluginService.qml`
- **Settings UI**: `Modules/Settings/PluginsTab.qml` - **Settings UI**: `Modules/Settings/PluginsTab.qml`
- **DankBar Integration**: `Modules/DankBar/DankBar.qml` - **DankBar Integration**: `Modules/DankBar/DankBar.qml`
- **Launcher Integration**: `Modules/AppDrawer/AppLauncher.qml`
- **Theme Reference**: `Common/Theme.qml` - **Theme Reference**: `Common/Theme.qml`
- **Widget Library**: `Widgets/` - **Widget Library**: `Widgets/`
@@ -785,7 +1411,8 @@ The plugin API is currently **experimental**. Breaking changes may occur in mino
Share your plugins with the community: Share your plugins with the community:
1. Create a public repository with your plugin 1. Create a public repository with your plugin
2. Include comprehensive README.md 2. Validate your `plugin.json` against `plugin-schema.json`
3. Include comprehensive README.md
4. Add example screenshots 4. Add example screenshots
5. Document dependencies and permissions 5. Document dependencies and permissions

View File

@@ -4,9 +4,10 @@
"description": "Background daemon that monitors wallpaper changes and logs them", "description": "Background daemon that monitors wallpaper changes and logs them",
"version": "1.0.0", "version": "1.0.0",
"author": "DankMaterialShell", "author": "DankMaterialShell",
"icon": "wallpaper",
"type": "daemon", "type": "daemon",
"capabilities": ["wallpaper", "monitoring"],
"component": "./WallpaperWatcherDaemon.qml", "component": "./WallpaperWatcherDaemon.qml",
"icon": "wallpaper",
"settings": "./WallpaperWatcherSettings.qml", "settings": "./WallpaperWatcherSettings.qml",
"permissions": [ "permissions": [
"settings_read", "settings_read",

115
PLUGINS/plugin-schema.json Normal file
View File

@@ -0,0 +1,115 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://danklinux.com/schemas/plugin.json",
"title": "DankMaterialShell Plugin Manifest",
"description": "Schema for DankMaterialShell plugin.json manifest files",
"type": "object",
"required": [
"id",
"name",
"description",
"version",
"author",
"type",
"capabilities",
"component"
],
"properties": {
"id": {
"type": "string",
"description": "Unique plugin identifier (camelCase, no spaces)",
"pattern": "^[a-zA-Z][a-zA-Z0-9]*$"
},
"name": {
"type": "string",
"description": "Human-readable plugin name",
"minLength": 1
},
"description": {
"type": "string",
"description": "Short description of plugin functionality",
"minLength": 1
},
"version": {
"type": "string",
"description": "Semantic version string (e.g., '1.0.0')",
"pattern": "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?(\\+[a-zA-Z0-9.-]+)?$"
},
"author": {
"type": "string",
"description": "Plugin creator name or email",
"minLength": 1
},
"type": {
"type": "string",
"description": "Plugin type",
"enum": ["widget", "daemon", "launcher"]
},
"capabilities": {
"type": "array",
"description": "Array of plugin capabilities",
"items": {
"type": "string"
},
"minItems": 1
},
"component": {
"type": "string",
"description": "Relative path to main QML component file",
"pattern": "^\\./.*\\.qml$"
},
"trigger": {
"type": "string",
"description": "Trigger string for launcher activation (required for launcher type)"
},
"icon": {
"type": "string",
"description": "Material Design icon name"
},
"settings": {
"type": "string",
"description": "Path to settings component QML file",
"pattern": "^\\./.*\\.qml$"
},
"requires_dms": {
"type": "string",
"description": "Minimum DMS version requirement (e.g., '>=0.1.18', '>0.1.0')",
"pattern": "^(>=?|<=?|=|>|<)\\d+\\.\\d+\\.\\d+$"
},
"requires": {
"type": "array",
"description": "Array of required system tools/dependencies",
"items": {
"type": "string"
}
},
"permissions": {
"type": "array",
"description": "Required capabilities",
"items": {
"type": "string",
"enum": [
"settings_read",
"settings_write",
"process",
"network"
]
}
}
},
"allOf": [
{
"if": {
"properties": {
"type": {
"const": "launcher"
}
}
},
"then": {
"required": ["trigger"]
}
}
],
"additionalProperties": true
}

View File

@@ -139,7 +139,7 @@ DankMaterialShell particularly aims at supporting the **niri** and **Hyprland**
**Niri**: **Niri**:
```bash ```bash
# Arch Linux # Arch Linux
paru -S niri-git sudo pacman -S niri
# Fedora # Fedora
sudo dnf copr enable yalter/niri && sudo dnf install niri sudo dnf copr enable yalter/niri && sudo dnf install niri
@@ -171,23 +171,45 @@ For detailed Hyprland installation instructions, see the [Hyprland wiki](https:/
#### Arch Linux - via AUR #### Arch Linux - via AUR
```bash ```bash
# Stable release
paru -S dms-shell-bin
# Latest -git
paru -S dms-shell-git paru -S dms-shell-git
``` ```
#### nixOS - via flake #### Fedora - via COPR
```bash
# Stable release
sudo dnf copr enable avengemedia/dms && sudo dnf install dms
# Latest -git
sudo dnf copr enable avengemedia/dms-git && sudo dnf install dms
```
#### NixOS - via flake
```bash ```bash
nix profile install github:AvengeMedia/DankMaterialShell nix profile install github:AvengeMedia/DankMaterialShell
``` ```
#### nixOS - via home-manager #### NixOS - via home-manager
To install using home-manager, you need to add this repo into your flake inputs: To install using home-manager, you need to add this repo into your flake inputs:
``` nix ``` nix
dgop = {
url = "github:AvengeMedia/dgop";
inputs.nixpkgs.follows = "nixpkgs";
};
dms-cli = {
url = "github:AvengeMedia/danklinux";
inputs.nixpkgs.follows = "nixpkgs";
};
dankMaterialShell = { dankMaterialShell = {
url = "github:AvengeMedia/DankMaterialShell"; url = "github:AvengeMedia/DankMaterialShell";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
inputs.dgop.follows = "dgop";
inputs.dms-cli.follows = "dms-cli";
}; };
``` ```
@@ -241,7 +263,7 @@ There are a lot of possible configurations that you can enable/disable in the fl
# Arch # Arch
paru -S quickshell-git paru -S quickshell-git
# Fedora # Fedora
sudo dnf copr enable errornointernet/quickshell && sudo dnf install quickshell-git sudo dnf copr enable avengemedia/danklinux && sudo dnf install quickshell-git
# ! TODO - document other distros # ! TODO - document other distros
``` ```
@@ -284,14 +306,14 @@ sudo sh -c "curl -L https://github.com/AvengeMedia/danklinux/releases/latest/dow
**4.1 Core optional dependencies** **4.1 Core optional dependencies**
```bash ```bash
# Arch Linux # Arch Linux
sudo pacman -S cava wl-clipboard cliphist brightnessctl sudo pacman -S cava wl-clipboard cliphist brightnessctl qt6-multimedia
paru -S matugen-bin dgop paru -S matugen-bin dgop
# Fedora # Fedora
sudo dnf install cava wl-clipboard brightnessctl sudo dnf install cava wl-clipboard brightnessctl qt6-qtmultimedia
sudo dnf copr enable wef/cliphist && sudo dnf install cliphist sudo dnf copr enable avengemedia/danklinux && sudo dnf install cliphist ghostty hyprpicker material-symbols-fonts matugen
sudo dnf copr enable heus-sueh/packages && sudo dnf install matugen
``` ```
Note: by enabling and installing the avengemedia/dms copr above, these core dependencies will automatically be available for use.
*Other distros will just need to find sources for the above packages* *Other distros will just need to find sources for the above packages*
@@ -312,6 +334,7 @@ sudo sh -c "curl -L https://github.com/AvengeMedia/dgop/releases/latest/download
- `cava`: Audio visualizer - `cava`: Audio visualizer
- `cliphist`: Clipboard history - `cliphist`: Clipboard history
- `gammastep`: Night mode support - `gammastep`: Night mode support
- `qt6-multimedia`: System sound support
## Compositor Configuration ## Compositor Configuration
@@ -661,7 +684,7 @@ cp -R ./PLUGINS/ExampleEmojiPlugin ~/.config/DankMaterialShell/plugins
**Only install plugins from TRUSTED sources.** Plugins execute QML and javascript at runtime, plugins from third parties should be reviewed before enabling them in dms. **Only install plugins from TRUSTED sources.** Plugins execute QML and javascript at runtime, plugins from third parties should be reviewed before enabling them in dms.
### nixOS - via home-manager ### NixOS - via home-manager
Add the following to your home-manager config to install a plugin: Add the following to your home-manager config to install a plugin:

View File

@@ -12,9 +12,12 @@ Singleton {
property var applications: DesktopEntries.applications.values.filter(app => !app.noDisplay && !app.runInTerminal) property var applications: DesktopEntries.applications.values.filter(app => !app.noDisplay && !app.runInTerminal)
function searchApplications(query) { function searchApplications(query) {
if (!query || query.length === 0) if (!query || query.length === 0) {
return applications return applications
}
if (applications.length === 0) if (applications.length === 0)
return [] return []
@@ -202,6 +205,11 @@ Singleton {
}) })
function getCategoryIcon(category) { function getCategoryIcon(category) {
// Check if it's a plugin category
const pluginIcon = getPluginCategoryIcon(category)
if (pluginIcon) {
return pluginIcon
}
return categoryIcons[category] || "folder" return categoryIcons[category] || "folder"
} }
@@ -213,7 +221,12 @@ Singleton {
appCategories.forEach(cat => categories.add(cat)) appCategories.forEach(cat => categories.add(cat))
} }
return Array.from(categories).sort() // Add plugin categories
const pluginCategories = getPluginCategories()
pluginCategories.forEach(cat => categories.add(cat))
const result = Array.from(categories).sort()
return result
} }
function getAppsInCategory(category) { function getAppsInCategory(category) {
@@ -221,9 +234,146 @@ Singleton {
return applications return applications
} }
// Check if it's a plugin category
const pluginItems = getPluginItems(category, "")
if (pluginItems.length > 0) {
return pluginItems
}
return applications.filter(app => { return applications.filter(app => {
const appCategories = getCategoriesForApp(app) const appCategories = getCategoriesForApp(app)
return appCategories.includes(category) return appCategories.includes(category)
}) })
} }
// Plugin launcher support functions
function getPluginCategories() {
if (typeof PluginService === "undefined") {
return []
}
const categories = []
const launchers = PluginService.getLauncherPlugins()
for (const pluginId in launchers) {
const plugin = launchers[pluginId]
const categoryName = plugin.name || pluginId
categories.push(categoryName)
}
return categories
}
function getPluginCategoryIcon(category) {
if (typeof PluginService === "undefined") return null
const launchers = PluginService.getLauncherPlugins()
for (const pluginId in launchers) {
const plugin = launchers[pluginId]
if ((plugin.name || pluginId) === category) {
return plugin.icon || "extension"
}
}
return null
}
function getAllPluginItems() {
if (typeof PluginService === "undefined") {
return []
}
let allItems = []
const launchers = PluginService.getLauncherPlugins()
for (const pluginId in launchers) {
const categoryName = launchers[pluginId].name || pluginId
const items = getPluginItems(categoryName, "")
allItems = allItems.concat(items)
}
return allItems
}
function getPluginItems(category, query) {
if (typeof PluginService === "undefined") return []
const launchers = PluginService.getLauncherPlugins()
for (const pluginId in launchers) {
const plugin = launchers[pluginId]
if ((plugin.name || pluginId) === category) {
return getPluginItemsForPlugin(pluginId, query)
}
}
return []
}
function getPluginItemsForPlugin(pluginId, query) {
if (typeof PluginService === "undefined") {
return []
}
const component = PluginService.pluginLauncherComponents[pluginId]
if (!component) return []
try {
const instance = component.createObject(root, {
"pluginService": PluginService
})
if (instance && typeof instance.getItems === "function") {
const items = instance.getItems(query || "")
instance.destroy()
return items || []
}
if (instance) {
instance.destroy()
}
} catch (e) {
console.warn("AppSearchService: Error getting items from plugin", pluginId, ":", e)
}
return []
}
function executePluginItem(item, pluginId) {
if (typeof PluginService === "undefined") return false
const component = PluginService.pluginLauncherComponents[pluginId]
if (!component) return false
try {
const instance = component.createObject(root, {
"pluginService": PluginService
})
if (instance && typeof instance.executeItem === "function") {
instance.executeItem(item)
instance.destroy()
return true
}
if (instance) {
instance.destroy()
}
} catch (e) {
console.warn("AppSearchService: Error executing item from plugin", pluginId, ":", e)
}
return false
}
function searchPluginItems(query) {
if (typeof PluginService === "undefined") return []
let allItems = []
const launchers = PluginService.getLauncherPlugins()
for (const pluginId in launchers) {
const items = getPluginItemsForPlugin(pluginId, query)
allItems = allItems.concat(items)
}
return allItems
}
} }

View File

@@ -6,6 +6,7 @@ import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Services.Pipewire import Quickshell.Services.Pipewire
import qs.Common
Singleton { Singleton {
id: root id: root
@@ -14,6 +15,17 @@ Singleton {
readonly property PwNode source: Pipewire.defaultAudioSource readonly property PwNode source: Pipewire.defaultAudioSource
property bool suppressOSD: true property bool suppressOSD: true
property bool soundsAvailable: false
property bool gsettingsAvailable: false
property var availableSoundThemes: []
property string currentSoundTheme: ""
property var soundFilePaths: ({})
property var volumeChangeSound: null
property var powerPlugSound: null
property var powerUnplugSound: null
property var normalNotificationSound: null
property var criticalNotificationSound: null
signal micMuteChanged signal micMuteChanged
@@ -25,6 +37,343 @@ Singleton {
onTriggered: root.suppressOSD = false onTriggered: root.suppressOSD = false
} }
function detectSoundsAvailability() {
try {
const testObj = Qt.createQmlObject(`
import QtQuick
import QtMultimedia
Item {}
`, root, "AudioService.TestComponent")
if (testObj) {
testObj.destroy()
}
soundsAvailable = true
return true
} catch (e) {
soundsAvailable = false
return false
}
}
function checkGsettings() {
Proc.runCommand("checkGsettings", ["sh", "-c", "gsettings get org.gnome.desktop.sound theme-name 2>/dev/null"], (output, exitCode) => {
gsettingsAvailable = (exitCode === 0)
if (gsettingsAvailable) {
scanSoundThemes()
getCurrentSoundTheme()
}
}, 0)
}
function scanSoundThemes() {
const xdgDataDirs = Quickshell.env("XDG_DATA_DIRS")
const searchPaths = xdgDataDirs && xdgDataDirs.trim() !== ""
? xdgDataDirs.split(":")
: ["/usr/share", "/usr/local/share", StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.local/share"]
const basePaths = searchPaths.map(p => p + "/sounds").join(" ")
const script = `
for base_dir in ${basePaths}; do
[ -d "$base_dir" ] || continue
for theme_dir in "$base_dir"/*; do
[ -d "$theme_dir/stereo" ] || continue
basename "$theme_dir"
done
done | sort -u
`
Proc.runCommand("scanSoundThemes", ["sh", "-c", script], (output, exitCode) => {
if (exitCode === 0 && output.trim()) {
const themes = output.trim().split('\n').filter(t => t && t.length > 0)
availableSoundThemes = themes
} else {
availableSoundThemes = []
}
}, 0)
}
function getCurrentSoundTheme() {
Proc.runCommand("getCurrentSoundTheme", ["sh", "-c", "gsettings get org.gnome.desktop.sound theme-name 2>/dev/null | sed \"s/'//g\""], (output, exitCode) => {
if (exitCode === 0 && output.trim()) {
currentSoundTheme = output.trim()
console.log("AudioService: Current system sound theme:", currentSoundTheme)
if (SettingsData.useSystemSoundTheme) {
discoverSoundFiles(currentSoundTheme)
}
} else {
currentSoundTheme = ""
console.log("AudioService: No system sound theme found")
}
}, 0)
}
function setSoundTheme(themeName) {
if (!themeName || themeName === currentSoundTheme) {
return
}
Proc.runCommand("setSoundTheme", ["sh", "-c", `gsettings set org.gnome.desktop.sound theme-name '${themeName}'`], (output, exitCode) => {
if (exitCode === 0) {
currentSoundTheme = themeName
if (SettingsData.useSystemSoundTheme) {
discoverSoundFiles(themeName)
}
}
}, 0)
}
function discoverSoundFiles(themeName) {
if (!themeName) {
soundFilePaths = {}
if (soundsAvailable) {
destroySoundPlayers()
createSoundPlayers()
}
return
}
const xdgDataDirs = Quickshell.env("XDG_DATA_DIRS")
const searchPaths = xdgDataDirs && xdgDataDirs.trim() !== ""
? xdgDataDirs.split(":")
: ["/usr/share", "/usr/local/share", StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.local/share"]
const extensions = ["oga", "ogg", "wav", "mp3", "flac"]
const themesToSearch = themeName !== "freedesktop" ? `${themeName} freedesktop` : themeName
const script = `
for event_key in audio-volume-change power-plug power-unplug message message-new-instant; do
found=0
case "$event_key" in
message)
names="dialog-information message message-lowpriority bell"
;;
message-new-instant)
names="dialog-warning message-new-instant message-highlight"
;;
*)
names="$event_key"
;;
esac
for theme in ${themesToSearch}; do
for event_name in $names; do
for base_path in ${searchPaths.join(" ")}; do
sounds_path="$base_path/sounds"
for ext in ${extensions.join(" ")}; do
file_path="$sounds_path/$theme/stereo/$event_name.$ext"
if [ -f "$file_path" ]; then
echo "$event_key=$file_path"
found=1
break
fi
done
[ $found -eq 1 ] && break
done
[ $found -eq 1 ] && break
done
[ $found -eq 1 ] && break
done
done
`
Proc.runCommand("discoverSoundFiles", ["sh", "-c", script], (output, exitCode) => {
const paths = {}
if (exitCode === 0 && output.trim()) {
const lines = output.trim().split('\n')
for (let line of lines) {
const parts = line.split('=')
if (parts.length === 2) {
paths[parts[0]] = "file://" + parts[1]
}
}
}
soundFilePaths = paths
if (soundsAvailable) {
destroySoundPlayers()
createSoundPlayers()
}
}, 0)
}
function getSoundPath(soundEvent) {
const soundMap = {
"audio-volume-change": "../assets/sounds/freedesktop/audio-volume-change.wav",
"power-plug": "../assets/sounds/plasma/power-plug.wav",
"power-unplug": "../assets/sounds/plasma/power-unplug.wav",
"message": "../assets/sounds/freedesktop/message.wav",
"message-new-instant": "../assets/sounds/freedesktop/message-new-instant.wav"
}
const specialConditions = {
"smooth": ["audio-volume-change"]
}
const themeLower = currentSoundTheme.toLowerCase()
if (SettingsData.useSystemSoundTheme && specialConditions[themeLower]?.includes(soundEvent)) {
const bundledPath = Qt.resolvedUrl(soundMap[soundEvent] || "../assets/sounds/freedesktop/message.wav")
console.log("AudioService: Using bundled sound (special condition) for", soundEvent, ":", bundledPath)
return bundledPath
}
if (SettingsData.useSystemSoundTheme && soundFilePaths[soundEvent]) {
console.log("AudioService: Using system sound for", soundEvent, ":", soundFilePaths[soundEvent])
return soundFilePaths[soundEvent]
}
const bundledPath = Qt.resolvedUrl(soundMap[soundEvent] || "../assets/sounds/freedesktop/message.wav")
console.log("AudioService: Using bundled sound for", soundEvent, ":", bundledPath)
return bundledPath
}
function reloadSounds() {
console.log("AudioService: Reloading sounds, useSystemSoundTheme:", SettingsData.useSystemSoundTheme, "currentSoundTheme:", currentSoundTheme)
if (SettingsData.useSystemSoundTheme && currentSoundTheme) {
discoverSoundFiles(currentSoundTheme)
} else {
soundFilePaths = {}
if (soundsAvailable) {
destroySoundPlayers()
createSoundPlayers()
}
}
}
function destroySoundPlayers() {
if (volumeChangeSound) {
volumeChangeSound.destroy()
volumeChangeSound = null
}
if (powerPlugSound) {
powerPlugSound.destroy()
powerPlugSound = null
}
if (powerUnplugSound) {
powerUnplugSound.destroy()
powerUnplugSound = null
}
if (normalNotificationSound) {
normalNotificationSound.destroy()
normalNotificationSound = null
}
if (criticalNotificationSound) {
criticalNotificationSound.destroy()
criticalNotificationSound = null
}
}
function createSoundPlayers() {
if (!soundsAvailable) {
return
}
try {
const volumeChangePath = getSoundPath("audio-volume-change")
volumeChangeSound = Qt.createQmlObject(`
import QtQuick
import QtMultimedia
MediaPlayer {
source: "${volumeChangePath}"
audioOutput: AudioOutput { volume: 1.0 }
}
`, root, "AudioService.VolumeChangeSound")
const powerPlugPath = getSoundPath("power-plug")
powerPlugSound = Qt.createQmlObject(`
import QtQuick
import QtMultimedia
MediaPlayer {
source: "${powerPlugPath}"
audioOutput: AudioOutput { volume: 1.0 }
}
`, root, "AudioService.PowerPlugSound")
const powerUnplugPath = getSoundPath("power-unplug")
powerUnplugSound = Qt.createQmlObject(`
import QtQuick
import QtMultimedia
MediaPlayer {
source: "${powerUnplugPath}"
audioOutput: AudioOutput { volume: 1.0 }
}
`, root, "AudioService.PowerUnplugSound")
const messagePath = getSoundPath("message")
normalNotificationSound = Qt.createQmlObject(`
import QtQuick
import QtMultimedia
MediaPlayer {
source: "${messagePath}"
audioOutput: AudioOutput { volume: 1.0 }
}
`, root, "AudioService.NormalNotificationSound")
const messageNewInstantPath = getSoundPath("message-new-instant")
criticalNotificationSound = Qt.createQmlObject(`
import QtQuick
import QtMultimedia
MediaPlayer {
source: "${messageNewInstantPath}"
audioOutput: AudioOutput { volume: 1.0 }
}
`, root, "AudioService.CriticalNotificationSound")
} catch (e) {
console.warn("AudioService: Error creating sound players:", e)
}
}
function playVolumeChangeSound() {
if (soundsAvailable && volumeChangeSound) {
volumeChangeSound.play()
}
}
function playPowerPlugSound() {
if (soundsAvailable && powerPlugSound) {
powerPlugSound.play()
}
}
function playPowerUnplugSound() {
if (soundsAvailable && powerUnplugSound) {
powerUnplugSound.play()
}
}
function playNormalNotificationSound() {
if (soundsAvailable && normalNotificationSound && !SessionData.doNotDisturb) {
normalNotificationSound.play()
}
}
function playCriticalNotificationSound() {
if (soundsAvailable && criticalNotificationSound && !SessionData.doNotDisturb) {
criticalNotificationSound.play()
}
}
Timer {
id: volumeSoundDebounce
interval: 50
repeat: false
onTriggered: {
if (!root.suppressOSD && SettingsData.soundsEnabled && SettingsData.soundVolumeChanged) {
root.playVolumeChangeSound()
}
}
}
Connections {
target: root.sink && root.sink.audio ? root.sink.audio : null
enabled: root.sink && root.sink.audio
ignoreUnknownSignals: true
function onVolumeChanged() {
volumeSoundDebounce.restart()
}
}
function displayName(node) { function displayName(node) {
if (!node) { if (!node) {
return "" return ""
@@ -212,4 +561,21 @@ Singleton {
return result return result
} }
} }
Connections {
target: SettingsData
function onUseSystemSoundThemeChanged() {
reloadSounds()
}
}
Component.onCompleted: {
if (!detectSoundsAvailability()) {
console.warn("AudioService: QtMultimedia not available - sound effects disabled")
} else {
console.log("AudioService: Sound effects enabled")
checkGsettings()
Qt.callLater(createSoundPlayers)
}
}
} }

View File

@@ -6,47 +6,118 @@ import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Services.UPower import Quickshell.Services.UPower
import qs.Common
Singleton { Singleton {
id: root id: root
property bool suppressSound: true
property bool previousPluggedState: false
Timer {
id: startupTimer
interval: 500
repeat: false
running: true
onTriggered: root.suppressSound = false
}
readonly property string preferredBatteryOverride: Quickshell.env("DMS_PREFERRED_BATTERY") readonly property string preferredBatteryOverride: Quickshell.env("DMS_PREFERRED_BATTERY")
// List of laptop batteries
readonly property var batteries: UPower.devices.values.filter(dev => dev.isLaptopBattery)
readonly property bool usePreferred: preferredBatteryOverride && preferredBatteryOverride.length > 0
// Main battery (for backward compatibility)
readonly property UPowerDevice device: { readonly property UPowerDevice device: {
var preferredDev var preferredDev
if (preferredBatteryOverride && preferredBatteryOverride.length > 0) { if (usePreferred) {
preferredDev = UPower.devices.values.find(dev => dev.nativePath.toLowerCase().includes(preferredBatteryOverride.toLowerCase())) preferredDev = batteries.find(dev => dev.nativePath.toLowerCase().includes(preferredBatteryOverride.toLowerCase()))
} }
return preferredDev || UPower.devices.values.find(dev => dev.isLaptopBattery) || null return preferredDev || batteries[0] || null
} }
readonly property bool batteryAvailable: device && device.ready // Whether at least one battery is available
readonly property real batteryLevel: batteryAvailable ? Math.round(device.percentage * 100) : 0 readonly property bool batteryAvailable: batteries.length > 0
readonly property bool isCharging: batteryAvailable && device.state === UPowerDeviceState.Charging && device.changeRate > 0 // Aggregated charge level (percentage)
readonly property bool isPluggedIn: batteryAvailable && (device.state !== UPowerDeviceState.Discharging && device.state !== UPowerDeviceState.Empty) readonly property real batteryLevel: {
if (!batteryAvailable)
return 0
return Math.round((batteryEnergy * 100) / batteryCapacity)
}
// Is any battery charging (at least one has changeRate > 0)
readonly property bool isCharging: batteryAvailable && batteries.some(b => b.state === UPowerDeviceState.Charging && b.changeRate > 0)
// Is the system plugged in (none of the batteries are discharging or empty)
readonly property bool isPluggedIn: batteryAvailable && batteries.every(b => b.state !== UPowerDeviceState.Discharging)
readonly property bool isLowBattery: batteryAvailable && batteryLevel <= 20 readonly property bool isLowBattery: batteryAvailable && batteryLevel <= 20
readonly property string batteryHealth: {
if (!batteryAvailable) { onIsPluggedInChanged: {
return "N/A" if (suppressSound || !batteryAvailable) {
previousPluggedState = isPluggedIn
return
} }
if (device.healthSupported && device.healthPercentage > 0) { if (SettingsData.soundsEnabled && SettingsData.soundPluggedIn) {
return `${Math.round(device.healthPercentage)}%` if (isPluggedIn && !previousPluggedState) {
AudioService.playPowerPlugSound()
} else if (!isPluggedIn && previousPluggedState) {
AudioService.playPowerUnplugSound()
}
} }
return "N/A" previousPluggedState = isPluggedIn
} }
readonly property real batteryCapacity: batteryAvailable && device.energyCapacity > 0 ? device.energyCapacity : 0
// Aggregated charge/discharge rate
readonly property real changeRate: {
if (!batteryAvailable) return 0
if (usePreferred && device && device.ready) return device.changeRate
return batteries.length > 0 ? batteries.reduce((sum, b) => sum + b.changeRate, 0) : 0
}
// Aggregated battery health
readonly property string batteryHealth: {
if (!batteryAvailable) return "N/A"
// If a preferred battery is selected and ready
if (usePreferred && device && device.ready && device.healthSupported) return `${Math.round(device.healthPercentage)}%`
// Otherwise, calculate the average health of all laptop batteries
const validBatteries = batteries.filter(b => b.healthSupported && b.healthPercentage > 0)
if (validBatteries.length === 0) return "N/A"
const avgHealth = validBatteries.reduce((sum, b) => sum + b.healthPercentage, 0) / validBatteries.length
return `${Math.round(avgHealth)}%`
}
readonly property real batteryEnergy: {
if (!batteryAvailable) return 0
if (usePreferred && device && device.ready) return device.energy
return batteries.length > 0 ? batteries.reduce((sum, b) => sum + b.energy, 0) : 0
}
// Total battery capacity (Wh)
readonly property real batteryCapacity: {
if (!batteryAvailable) return 0
if (usePreferred && device && device.ready) return device.energyCapacity
return batteries.length > 0 ? batteries.reduce((sum, b) => sum + b.energyCapacity, 0) : 0
}
// Aggregated battery status
readonly property string batteryStatus: { readonly property string batteryStatus: {
if (!batteryAvailable) { if (!batteryAvailable) {
return "No Battery" return "No Battery"
} }
if (device.state === UPowerDeviceState.Charging && device.changeRate <= 0) { if (isCharging && !batteries.some(b => b.changeRate > 0)) return "Plugged In"
return "Plugged In"
}
return UPowerDeviceState.toString(device.state) const states = batteries.map(b => b.state)
if (states.every(s => s === states[0])) return UPowerDeviceState.toString(states[0])
return isCharging ? "Charging" : (isPluggedIn ? "Plugged In" : "Discharging")
} }
readonly property bool suggestPowerSaver: batteryAvailable && isLowBattery && UPower.onBattery && (typeof PowerProfiles !== "undefined" && PowerProfiles.profile !== PowerProfile.PowerSaver) readonly property bool suggestPowerSaver: batteryAvailable && isLowBattery && UPower.onBattery && (typeof PowerProfiles !== "undefined" && PowerProfiles.profile !== PowerProfile.PowerSaver)
readonly property var bluetoothDevices: { readonly property var bluetoothDevices: {
@@ -66,25 +137,20 @@ Singleton {
return btDevices return btDevices
} }
// Format time remaining for charge/discharge
function formatTimeRemaining() { function formatTimeRemaining() {
if (!batteryAvailable) { if (!batteryAvailable) {
return "Unknown" return "Unknown"
} }
const timeSeconds = isCharging ? device.timeToFull : device.timeToEmpty let totalTime = 0
totalTime = (isCharging) ? ((batteryCapacity - batteryEnergy) / changeRate) : (batteryEnergy / changeRate)
const avgTime = Math.abs(totalTime * 3600)
if (!avgTime || avgTime <= 0 || avgTime > 86400) return "Unknown"
if (!timeSeconds || timeSeconds <= 0 || timeSeconds > 86400) { const hours = Math.floor(avgTime / 3600)
return "Unknown" const minutes = Math.floor((avgTime % 3600) / 60)
} return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`
const hours = Math.floor(timeSeconds / 3600)
const minutes = Math.floor((timeSeconds % 3600) / 60)
if (hours > 0) {
return `${hours}h ${minutes}m`
}
return `${minutes}m`
} }
function getBatteryIcon() { function getBatteryIcon() {

View File

@@ -174,17 +174,42 @@ Singleton {
// Parse time if available and not all-day // Parse time if available and not all-day
let timeStr = event['start-time'] let timeStr = event['start-time']
if (timeStr) { if (timeStr) {
let timeParts = timeStr.match(/(\d+):(\d+)/) // Match time with optional seconds and AM/PM
let timeParts = timeStr.match(/(\d+):(\d+)(?::\d+)?\s*(AM|PM)?/i)
if (timeParts) { if (timeParts) {
startTime.setHours(parseInt(timeParts[1]), let hours = parseInt(timeParts[1])
parseInt(timeParts[2])) let minutes = parseInt(timeParts[2])
// Handle AM/PM conversion if present
if (timeParts[3]) {
let period = timeParts[3].toUpperCase()
if (period === 'PM' && hours !== 12) {
hours += 12
} else if (period === 'AM' && hours === 12) {
hours = 0
}
}
startTime.setHours(hours, minutes)
if (event['end-time']) { if (event['end-time']) {
let endTimeParts = event['end-time'].match( let endTimeParts = event['end-time'].match(
/(\d+):(\d+)/) /(\d+):(\d+)(?::\d+)?\s*(AM|PM)?/i)
if (endTimeParts) if (endTimeParts) {
endTime.setHours( let endHours = parseInt(endTimeParts[1])
parseInt(endTimeParts[1]), let endMinutes = parseInt(endTimeParts[2])
parseInt(endTimeParts[2]))
// Handle AM/PM conversion if present
if (endTimeParts[3]) {
let endPeriod = endTimeParts[3].toUpperCase()
if (endPeriod === 'PM' && endHours !== 12) {
endHours += 12
} else if (endPeriod === 'AM' && endHours === 12) {
endHours = 0
}
}
endTime.setHours(endHours, endMinutes)
}
} else { } else {
// Default to 1 hour duration on same day // Default to 1 hour duration on same day
endTime = new Date(startTime) endTime = new Date(startTime)

View File

@@ -4,9 +4,9 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Hyprland import Quickshell.Hyprland
import qs.Common
Singleton { Singleton {
id: root id: root
@@ -160,7 +160,20 @@ Singleton {
} }
if (niriSocket && niriSocket.length > 0) { if (niriSocket && niriSocket.length > 0) {
niriSocketCheck.running = true Proc.runCommand("niriSocketCheck", ["test", "-S", root.niriSocket], (output, exitCode) => {
if (exitCode === 0) {
root.isNiri = true
root.isHyprland = false
root.compositor = "niri"
console.log("CompositorService: Detected Niri with socket:", root.niriSocket)
NiriService.generateNiriBinds()
} else {
root.isHyprland = false
root.isNiri = true
root.compositor = "niri"
console.warn("CompositorService: Niri socket check failed, defaulting to Niri anyway")
}
}, 0)
} else { } else {
isHyprland = false isHyprland = false
isNiri = false isNiri = false
@@ -188,24 +201,4 @@ Singleton {
} }
console.warn("CompositorService: Cannot power on monitors, unknown compositor") console.warn("CompositorService: Cannot power on monitors, unknown compositor")
} }
Process {
id: niriSocketCheck
command: ["test", "-S", root.niriSocket]
onExited: exitCode => {
if (exitCode === 0) {
root.isNiri = true
root.isHyprland = false
root.compositor = "niri"
console.log("CompositorService: Detected Niri with socket:", root.niriSocket)
NiriService.generateNiriBinds()
} else {
root.isHyprland = false
root.isNiri = true
root.compositor = "niri"
console.warn("CompositorService: Niri socket check failed, defaulting to Niri anyway")
}
}
}
} }

View File

@@ -40,6 +40,7 @@ Singleton {
signal networkStateUpdate(var data) signal networkStateUpdate(var data)
signal loginctlStateUpdate(var data) signal loginctlStateUpdate(var data)
signal loginctlEvent(var event) signal loginctlEvent(var event)
signal capabilitiesReceived()
Component.onCompleted: { Component.onCompleted: {
if (socketPath && socketPath.length > 0) { if (socketPath && socketPath.length > 0) {
@@ -256,6 +257,8 @@ Singleton {
if (apiVersion < expectedApiVersion) { if (apiVersion < expectedApiVersion) {
ToastService.showError("DMS server is outdated (API v" + apiVersion + ", expected v" + expectedApiVersion + ")") ToastService.showError("DMS server is outdated (API v" + apiVersion + ", expected v" + expectedApiVersion + ")")
} }
capabilitiesReceived()
} else if (service === "network") { } else if (service === "network") {
networkStateUpdate(data) networkStateUpdate(data)
} else if (service === "loginctl") { } else if (service === "loginctl") {
@@ -394,4 +397,12 @@ Singleton {
} }
}) })
} }
function lockSession(callback) {
sendRequest("loginctl.lock", null, callback)
}
function unlockSession(callback) {
sendRequest("loginctl.unlock", null, callback)
}
} }

View File

@@ -0,0 +1,39 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import QtCore
import Quickshell
import Quickshell.Io
import qs.Common
Singleton {
id: root
readonly property string shellDir: Paths.strip(Qt.resolvedUrl(".").toString()).replace("/Services/", "")
property string scriptPath: `${shellDir}/scripts/hyprland_keybinds.py`
readonly property string _configUrl: StandardPaths.writableLocation(StandardPaths.ConfigLocation)
readonly property string _configDir: Paths.strip(_configUrl)
property string hyprConfigPath: `${_configDir}/hypr`
property var keybinds: ({"children": [], "keybinds": []})
Process {
id: getKeybinds
running: true
command: [root.scriptPath, "--path", root.hyprConfigPath]
stdout: SplitParser {
onRead: data => {
try {
root.keybinds = JSON.parse(data)
} catch (e) {
console.error("[HyprKeybindsService] Error parsing keybinds:", e)
}
}
}
}
function reload() {
getKeybinds.running = true
}
}

View File

@@ -23,10 +23,10 @@ Singleton {
property bool _enableGate: true property bool _enableGate: true
readonly property bool isOnBattery: BatteryService.batteryAvailable && !BatteryService.isPluggedIn readonly property bool isOnBattery: BatteryService.batteryAvailable && !BatteryService.isPluggedIn
readonly property int monitorTimeout: isOnBattery ? SessionData.batteryMonitorTimeout : SessionData.acMonitorTimeout readonly property int monitorTimeout: isOnBattery ? SettingsData.batteryMonitorTimeout : SettingsData.acMonitorTimeout
readonly property int lockTimeout: isOnBattery ? SessionData.batteryLockTimeout : SessionData.acLockTimeout readonly property int lockTimeout: isOnBattery ? SettingsData.batteryLockTimeout : SettingsData.acLockTimeout
readonly property int suspendTimeout: isOnBattery ? SessionData.batterySuspendTimeout : SessionData.acSuspendTimeout readonly property int suspendTimeout: isOnBattery ? SettingsData.batterySuspendTimeout : SettingsData.acSuspendTimeout
readonly property int hibernateTimeout: isOnBattery ? SessionData.batteryHibernateTimeout : SessionData.acHibernateTimeout readonly property int hibernateTimeout: isOnBattery ? SettingsData.batteryHibernateTimeout : SettingsData.acHibernateTimeout
onMonitorTimeoutChanged: _rearmIdleMonitors() onMonitorTimeoutChanged: _rearmIdleMonitors()
onLockTimeoutChanged: _rearmIdleMonitors() onLockTimeoutChanged: _rearmIdleMonitors()
@@ -139,7 +139,7 @@ Singleton {
Connections { Connections {
target: SessionService target: SessionService
function onPrepareForSleep() { function onPrepareForSleep() {
if (SessionData.lockBeforeSuspend) { if (SettingsData.lockBeforeSuspend) {
root.lockRequested() root.lockRequested()
} }
} }

View File

@@ -20,6 +20,8 @@ Singleton {
property bool ethernetConnected: false property bool ethernetConnected: false
property string ethernetConnectionUuid: "" property string ethernetConnectionUuid: ""
property var wiredConnections: []
property string wifiIP: "" property string wifiIP: ""
property string wifiInterface: "" property string wifiInterface: ""
property bool wifiConnected: false property bool wifiConnected: false
@@ -74,6 +76,10 @@ Singleton {
property string networkInfoDetails: "" property string networkInfoDetails: ""
property bool networkInfoLoading: false property bool networkInfoLoading: false
property string networkWiredInfoUUID: ""
property string networkWiredInfoDetails: ""
property bool networkWiredInfoLoading: false
signal networksUpdated signal networksUpdated
signal connectionChanged signal connectionChanged

View File

@@ -13,48 +13,4 @@ Singleton {
readonly property list<MprisPlayer> availablePlayers: Mpris.players.values readonly property list<MprisPlayer> availablePlayers: Mpris.players.values
property MprisPlayer activePlayer: availablePlayers.find(p => p.isPlaying) ?? availablePlayers.find(p => p.canControl && p.canPlay) ?? null property MprisPlayer activePlayer: availablePlayers.find(p => p.isPlaying) ?? availablePlayers.find(p => p.canControl && p.canPlay) ?? null
IpcHandler {
target: "mpris"
function list(): string {
return root.availablePlayers.map(p => p.identity).join("\n")
}
function play(): void {
if (root.activePlayer && root.activePlayer.canPlay) {
root.activePlayer.play()
}
}
function pause(): void {
if (root.activePlayer && root.activePlayer.canPause) {
root.activePlayer.pause()
}
}
function playPause(): void {
if (root.activePlayer && root.activePlayer.canTogglePlaying) {
root.activePlayer.togglePlaying()
}
}
function previous(): void {
if (root.activePlayer && root.activePlayer.canGoPrevious) {
root.activePlayer.previous()
}
}
function next(): void {
if (root.activePlayer && root.activePlayer.canGoNext) {
root.activePlayer.next()
}
}
function stop(): void {
if (root.activePlayer) {
root.activePlayer.stop()
}
}
}
} }

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